]> BookStack Code Mirror - bookstack/commitdiff
Merge branch 'master' of git://github.com/rondaa/BookStack into rondaa-master
authorDan Brown <redacted>
Sat, 2 Jan 2021 16:25:59 +0000 (16:25 +0000)
committerDan Brown <redacted>
Sat, 2 Jan 2021 16:25:59 +0000 (16:25 +0000)
257 files changed:
.env.example
.env.example.complete
.github/FUNDING.yml [new file with mode: 0644]
app/Actions/Activity.php
app/Actions/ActivityService.php
app/Actions/ActivityType.php [new file with mode: 0644]
app/Actions/Comment.php
app/Actions/CommentRepo.php
app/Actions/Tag.php
app/Actions/TagRepo.php
app/Actions/ViewService.php
app/Api/ApiDocsGenerator.php
app/Api/ApiToken.php
app/Auth/Access/Guards/ExternalBaseSessionGuard.php
app/Auth/Access/Guards/Saml2SessionGuard.php
app/Auth/Access/Ldap.php
app/Auth/Access/RegistrationService.php
app/Auth/Access/Saml2Service.php
app/Auth/Access/SocialAuthService.php
app/Auth/Permissions/JointPermission.php
app/Auth/Permissions/PermissionService.php
app/Auth/Permissions/PermissionsRepo.php
app/Auth/Role.php
app/Auth/SocialAccount.php
app/Auth/User.php
app/Auth/UserRepo.php
app/Config/app.php
app/Config/filesystems.php
app/Config/session.php
app/Console/Commands/CleanupImages.php
app/Console/Commands/ClearRevisions.php
app/Console/Commands/CopyShelfPermissions.php
app/Console/Commands/CreateAdmin.php
app/Console/Commands/RegenerateSearch.php
app/Entities/BreadcrumbsViewComposer.php
app/Entities/Chapter.php [deleted file]
app/Entities/EntityProvider.php
app/Entities/Managers/TrashCan.php [deleted file]
app/Entities/Models/Book.php [moved from app/Entities/Book.php with 77% similarity]
app/Entities/Models/BookChild.php [moved from app/Entities/BookChild.php with 87% similarity]
app/Entities/Models/Bookshelf.php [moved from app/Entities/Bookshelf.php with 78% similarity]
app/Entities/Models/Chapter.php [new file with mode: 0644]
app/Entities/Models/Deletion.php [new file with mode: 0644]
app/Entities/Models/Entity.php [moved from app/Entities/Entity.php with 73% similarity]
app/Entities/Models/HasCoverImage.php [moved from app/Entities/HasCoverImage.php with 90% similarity]
app/Entities/Models/Page.php [moved from app/Entities/Page.php with 70% similarity]
app/Entities/Models/PageRevision.php [moved from app/Entities/PageRevision.php with 96% similarity]
app/Entities/Models/SearchTerm.php [moved from app/Entities/SearchTerm.php with 89% similarity]
app/Entities/Repos/BaseRepo.php
app/Entities/Repos/BookRepo.php
app/Entities/Repos/BookshelfRepo.php
app/Entities/Repos/ChapterRepo.php
app/Entities/Repos/PageRepo.php
app/Entities/Tools/BookContents.php [moved from app/Entities/Managers/BookContents.php with 92% similarity]
app/Entities/Tools/ExportFormatter.php [moved from app/Entities/ExportService.php with 94% similarity]
app/Entities/Tools/PageContent.php [moved from app/Entities/Managers/PageContent.php with 93% similarity]
app/Entities/Tools/PageEditActivity.php [moved from app/Entities/Managers/PageEditActivity.php with 95% similarity]
app/Entities/Tools/PermissionsUpdater.php [new file with mode: 0644]
app/Entities/Tools/SearchIndex.php [new file with mode: 0644]
app/Entities/Tools/SearchOptions.php [moved from app/Entities/SearchOptions.php with 98% similarity]
app/Entities/Tools/SearchRunner.php [moved from app/Entities/SearchService.php with 72% similarity]
app/Entities/Tools/ShelfContext.php [moved from app/Entities/Managers/EntityContext.php with 55% similarity]
app/Entities/Tools/SiblingFetcher.php [new file with mode: 0644]
app/Entities/Tools/SlugGenerator.php [moved from app/Entities/SlugGenerator.php with 52% similarity]
app/Entities/Tools/TrashCan.php [new file with mode: 0644]
app/Http/Controllers/Api/ApiController.php
app/Http/Controllers/Api/ApiDocsController.php
app/Http/Controllers/Api/BookApiController.php
app/Http/Controllers/Api/BookExportApiController.php
app/Http/Controllers/Api/BookshelfApiController.php
app/Http/Controllers/Api/ChapterApiController.php
app/Http/Controllers/Api/ChapterExportApiController.php
app/Http/Controllers/Api/PageApiController.php [new file with mode: 0644]
app/Http/Controllers/Api/PageExportApiController.php [new file with mode: 0644]
app/Http/Controllers/AttachmentController.php
app/Http/Controllers/AuditLogController.php
app/Http/Controllers/Auth/ConfirmEmailController.php
app/Http/Controllers/Auth/ForgotPasswordController.php
app/Http/Controllers/Auth/LoginController.php
app/Http/Controllers/Auth/RegisterController.php
app/Http/Controllers/Auth/ResetPasswordController.php
app/Http/Controllers/Auth/Saml2Controller.php
app/Http/Controllers/Auth/UserInviteController.php
app/Http/Controllers/BookController.php
app/Http/Controllers/BookExportController.php
app/Http/Controllers/BookSortController.php
app/Http/Controllers/BookshelfController.php
app/Http/Controllers/ChapterController.php
app/Http/Controllers/ChapterExportController.php
app/Http/Controllers/CommentController.php
app/Http/Controllers/Controller.php
app/Http/Controllers/HomeController.php
app/Http/Controllers/Images/DrawioImageController.php
app/Http/Controllers/Images/GalleryImageController.php
app/Http/Controllers/Images/ImageController.php
app/Http/Controllers/MaintenanceController.php
app/Http/Controllers/PageController.php
app/Http/Controllers/PageExportController.php
app/Http/Controllers/PageRevisionController.php
app/Http/Controllers/PageTemplateController.php
app/Http/Controllers/RecycleBinController.php [new file with mode: 0644]
app/Http/Controllers/RoleController.php [moved from app/Http/Controllers/PermissionController.php with 89% similarity]
app/Http/Controllers/SearchController.php
app/Http/Controllers/SettingController.php
app/Http/Controllers/TagController.php
app/Http/Controllers/UserApiTokenController.php
app/Http/Controllers/UserController.php
app/Http/Controllers/UserSearchController.php [new file with mode: 0644]
app/Http/Kernel.php
app/Http/Middleware/ControlIframeSecurity.php [new file with mode: 0644]
app/Http/Middleware/Localization.php
app/Interfaces/Loggable.php [new file with mode: 0644]
app/Ownable.php [deleted file]
app/Providers/AppServiceProvider.php
app/Providers/CustomValidationServiceProvider.php [new file with mode: 0644]
app/Traits/HasCreatorAndUpdater.php [new file with mode: 0644]
app/Traits/HasOwner.php [new file with mode: 0644]
app/Uploads/Attachment.php
app/Uploads/AttachmentService.php
app/Uploads/Image.php
app/Uploads/ImageRepo.php
app/Uploads/ImageService.php
app/Uploads/UploadService.php [deleted file]
app/Uploads/UserAvatars.php [new file with mode: 0644]
app/helpers.php
composer.json
composer.lock
database/factories/ModelFactory.php
database/migrations/2018_08_04_115700_create_bookshelves_table.php
database/migrations/2020_09_27_210059_add_entity_soft_deletes.php [new file with mode: 0644]
database/migrations/2020_09_27_210528_create_deletions_table.php [new file with mode: 0644]
database/migrations/2020_11_07_232321_simplify_activities_table.php [new file with mode: 0644]
database/migrations/2020_12_30_173528_add_owned_by_field_to_entities.php [new file with mode: 0644]
database/seeds/DummyContentSeeder.php
database/seeds/LargeContentSeeder.php
dev/api/requests/pages-create.json [new file with mode: 0644]
dev/api/requests/pages-update.json [new file with mode: 0644]
dev/api/responses/books-read.json
dev/api/responses/chapters-read.json
dev/api/responses/pages-create.json [new file with mode: 0644]
dev/api/responses/pages-list.json [new file with mode: 0644]
dev/api/responses/pages-read.json [new file with mode: 0644]
dev/api/responses/pages-update.json [new file with mode: 0644]
dev/api/responses/shelves-read.json
phpunit.xml
readme.md
resources/js/components/breadcrumb-listing.js [deleted file]
resources/js/components/dropdown-search.js [new file with mode: 0644]
resources/js/components/dropdown.js
resources/js/components/index.js
resources/js/components/markdown-editor.js
resources/js/components/user-select.js [new file with mode: 0644]
resources/js/components/wysiwyg-editor.js
resources/lang/en/activities.php
resources/lang/en/entities.php
resources/lang/en/settings.php
resources/lang/nb/activities.php [new file with mode: 0644]
resources/lang/nb/auth.php [new file with mode: 0644]
resources/lang/nb/common.php [new file with mode: 0644]
resources/lang/nb/components.php [new file with mode: 0644]
resources/lang/nb/entities.php [new file with mode: 0644]
resources/lang/nb/errors.php [new file with mode: 0644]
resources/lang/nb/pagination.php [new file with mode: 0644]
resources/lang/nb/passwords.php [new file with mode: 0644]
resources/lang/nb/settings.php [new file with mode: 0644]
resources/lang/nb/validation.php [new file with mode: 0644]
resources/sass/_blocks.scss
resources/sass/_colors.scss
resources/sass/_components.scss
resources/sass/_header.scss
resources/sass/_layout.scss
resources/sass/_text.scss
resources/sass/styles.scss
resources/views/books/export.blade.php
resources/views/books/grid-item.blade.php [deleted file]
resources/views/books/list.blade.php
resources/views/books/sort-box.blade.php
resources/views/chapters/child-menu.blade.php
resources/views/chapters/list-item.blade.php
resources/views/common/header.blade.php
resources/views/components/page-picker.blade.php
resources/views/components/user-select-list.blade.php [new file with mode: 0644]
resources/views/components/user-select.blade.php [new file with mode: 0644]
resources/views/form/entity-permissions.blade.php
resources/views/pages/edit.blade.php
resources/views/pages/markdown-editor.blade.php
resources/views/pages/show.blade.php
resources/views/pages/wysiwyg-editor.blade.php
resources/views/partials/activity-item.blade.php
resources/views/partials/book-tree.blade.php
resources/views/partials/breadcrumb-listing.blade.php
resources/views/partials/breadcrumbs.blade.php
resources/views/partials/entity-display-item.blade.php [new file with mode: 0644]
resources/views/partials/entity-grid-item.blade.php [new file with mode: 0644]
resources/views/partials/entity-meta.blade.php
resources/views/partials/table-user.blade.php [new file with mode: 0644]
resources/views/search/book.blade.php [deleted file]
resources/views/settings/audit.blade.php
resources/views/settings/maintenance.blade.php
resources/views/settings/recycle-bin/deletable-entity-list.blade.php [new file with mode: 0644]
resources/views/settings/recycle-bin/destroy.blade.php [new file with mode: 0644]
resources/views/settings/recycle-bin/index.blade.php [new file with mode: 0644]
resources/views/settings/recycle-bin/restore.blade.php [new file with mode: 0644]
resources/views/shelves/grid-item.blade.php [deleted file]
resources/views/shelves/list-item.blade.php
resources/views/shelves/list.blade.php
resources/views/shelves/show.blade.php
resources/views/users/delete.blade.php
resources/views/users/index.blade.php
routes/api.php
routes/web.php
tests/ActivityTrackingTest.php
tests/Api/ApiListingTest.php
tests/Api/BooksApiTest.php
tests/Api/ChaptersApiTest.php
tests/Api/PagesApiTest.php [new file with mode: 0644]
tests/Api/ShelvesApiTest.php
tests/AuditLogTest.php
tests/Auth/AuthTest.php
tests/BrowserKitTest.php
tests/CommandsTest.php
tests/Entity/BookShelfTest.php
tests/Entity/BookTest.php [new file with mode: 0644]
tests/Entity/ChapterTest.php [new file with mode: 0644]
tests/Entity/CommentSettingTest.php
tests/Entity/CommentTest.php
tests/Entity/EntitySearchTest.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/PageTemplateTest.php
tests/Entity/PageTest.php [new file with mode: 0644]
tests/Entity/SearchOptionsTest.php
tests/Entity/SortTest.php
tests/Entity/TagTest.php
tests/ErrorTest.php
tests/HomepageTest.php
tests/Permissions/EntityOwnerChangeTest.php [new file with mode: 0644]
tests/Permissions/EntityPermissionsTest.php [moved from tests/Permissions/RestrictionsTest.php with 96% similarity]
tests/Permissions/ExportPermissionsTest.php [new file with mode: 0644]
tests/Permissions/RolesTest.php
tests/PublicActionTest.php
tests/RecycleBinTest.php [new file with mode: 0644]
tests/SecurityHeaderTest.php [new file with mode: 0644]
tests/SharedTestHelpers.php
tests/TestCase.php
tests/TestResponse.php
tests/Uploads/AttachmentTest.php
tests/Uploads/DrawioTest.php
tests/Uploads/ImageTest.php
tests/Uploads/UsesImages.php
tests/User/UserApiTokenTest.php
tests/User/UserManagementTest.php [new file with mode: 0644]
tests/User/UserProfileTest.php

index 47f2367b08a9a3d88935d757b78acdb5e879c988..05383f04abcce2f08d732f2b08719cb5b3775a76 100644 (file)
 APP_KEY=SomeRandomString
 
 # Application URL
-# Remove the hash below and set a URL if using BookStack behind
-# a proxy or if using a third-party authentication option.
 # This must be the root URL that you want to host BookStack on.
-# All URL's in BookStack will be generated using this value.
-#APP_URL=https://example.com
+# All URLs in BookStack will be generated using this value
+# to ensure URLs generated are consistent and secure.
+# If you change this in the future you may need to run a command
+# to update stored URLs in the database. Command example:
+# php artisan bookstack:update-url https://old.example.com https://new.example.com
+APP_URL=https://example.com
 
 # Database details
 DB_HOST=localhost
@@ -28,8 +30,8 @@ DB_PASSWORD=database_user_password
 # Can be 'smtp' or 'sendmail'
 MAIL_DRIVER=smtp
 
-# Mail sender options
-MAIL_FROM_NAME=BookStack
+# Mail sender details
+MAIL_FROM_NAME="BookStack"
 
 # SMTP mail options
index 45b1e13215d475daed4e23734a294602a6119b1d..e3dbdb857ddde1c7393e944a17ab9bea0f6b7104 100644 (file)
@@ -255,6 +255,14 @@ APP_VIEWS_BOOKSHELVES=grid
 # If set to 'false' a limit will not be enforced.
 REVISION_LIMIT=50
 
+# Recycle Bin Lifetime
+# The number of days that content will remain in the recycle bin before
+# being considered for auto-removal. It is not a guarantee that content will
+# be removed after this time.
+# Set to 0 for no recycle bin functionality.
+# Set to -1 for unlimited recycle bin lifetime.
+RECYCLE_BIN_LIFETIME=30
+
 # Allow <script> tags in page content
 # Note, if set to 'true' the page editor may still escape scripts.
 ALLOW_CONTENT_SCRIPTS=false
@@ -265,6 +273,12 @@ ALLOW_CONTENT_SCRIPTS=false
 # Contents of the robots.txt file can be overridden, making this option obsolete.
 ALLOW_ROBOTS=null
 
+# A list of hosts that BookStack can be iframed within.
+# Space separated if multiple. BookStack host domain is auto-inferred.
+# For Example: ALLOWED_IFRAME_HOSTS="https://example.com https://a.example.com"
+# Setting this option will also auto-adjust cookies to be SameSite=None.
+ALLOWED_IFRAME_HOSTS=null
+
 # The default and maximum item-counts for listing API requests.
 API_DEFAULT_ITEM_COUNT=100
 API_MAX_ITEM_COUNT=500
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644 (file)
index 0000000..01b8471
--- /dev/null
@@ -0,0 +1,3 @@
+# These are supported funding model platforms
+
+github: [ssddanbrown]
index 035a9cc750ef16618ee81b8f48e0981c2442f8da..9d256c9b2918b2cdf0d7f543662bd7a7fb920f87 100644 (file)
@@ -3,18 +3,19 @@
 namespace BookStack\Actions;
 
 use BookStack\Auth\User;
-use BookStack\Entities\Entity;
+use BookStack\Entities\Models\Entity;
 use BookStack\Model;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
+use Illuminate\Support\Str;
 
 /**
- * @property string $key
+ * @property string $type
  * @property User $user
  * @property Entity $entity
- * @property string $extra
+ * @property string $detail
  * @property string $entity_type
  * @property int $entity_id
  * @property int $user_id
- * @property int $book_id
  */
 class Activity extends Model
 {
@@ -32,20 +33,28 @@ class Activity extends Model
 
     /**
      * Get the user this activity relates to.
-     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
      */
-    public function user()
+    public function user(): BelongsTo
     {
         return $this->belongsTo(User::class);
     }
 
     /**
-     * Returns text from the language files, Looks up by using the
-     * activity key.
+     * Returns text from the language files, Looks up by using the activity key.
      */
-    public function getText()
+    public function getText(): string
     {
-        return trans('activities.' . $this->key);
+        return trans('activities.' . $this->type);
+    }
+
+    /**
+     * Check if this activity is intended to be for an entity.
+     */
+    public function isForEntity(): bool
+    {
+        return Str::startsWith($this->type, [
+            'page_', 'chapter_', 'book_', 'bookshelf_'
+        ]);
     }
 
     /**
@@ -53,6 +62,6 @@ class Activity extends Model
      */
     public function isSimilarTo(Activity $activityB): bool
     {
-        return [$this->key, $this->entity_type, $this->entity_id] === [$activityB->key, $activityB->entity_type, $activityB->entity_id];
+        return [$this->type, $this->entity_type, $this->entity_id] === [$activityB->type, $activityB->entity_type, $activityB->entity_id];
     }
 }
index e6b004f01fb0bc15d3b892ba6f56c49676e31cdc..b2a35fd2a5115c7ba869a35ddbd34e69287b8e6d 100644 (file)
@@ -2,57 +2,59 @@
 
 use BookStack\Auth\Permissions\PermissionService;
 use BookStack\Auth\User;
-use BookStack\Entities\Entity;
-use Illuminate\Support\Collection;
+use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Models\Entity;
+use BookStack\Entities\Models\Page;
+use BookStack\Interfaces\Loggable;
+use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Database\Eloquent\Relations\Relation;
 use Illuminate\Support\Facades\Log;
 
 class ActivityService
 {
     protected $activity;
-    protected $user;
     protected $permissionService;
 
-    /**
-     * ActivityService constructor.
-     */
     public function __construct(Activity $activity, PermissionService $permissionService)
     {
         $this->activity = $activity;
         $this->permissionService = $permissionService;
-        $this->user = user();
     }
 
     /**
-     * Add activity data to database.
+     * Add activity data to database for an entity.
      */
-    public function add(Entity $entity, string $activityKey, ?int $bookId = null)
+    public function addForEntity(Entity $entity, string $type)
     {
-        $activity = $this->newActivityForUser($activityKey, $bookId);
+        $activity = $this->newActivityForUser($type);
         $entity->activity()->save($activity);
-        $this->setNotification($activityKey);
+        $this->setNotification($type);
     }
 
     /**
-     * Adds a activity history with a message, without binding to a entity.
+     * Add a generic activity event to the database.
+     * @param string|Loggable $detail
      */
-    public function addMessage(string $activityKey, string $message, ?int $bookId = null)
+    public function add(string $type, $detail = '')
     {
-        $this->newActivityForUser($activityKey, $bookId)->forceFill([
-            'extra' => $message
-        ])->save();
+        if ($detail instanceof Loggable) {
+            $detail = $detail->logDescriptor();
+        }
 
-        $this->setNotification($activityKey);
+        $activity = $this->newActivityForUser($type);
+        $activity->detail = $detail;
+        $activity->save();
+        $this->setNotification($type);
     }
 
     /**
      * Get a new activity instance for the current user.
      */
-    protected function newActivityForUser(string $key, ?int $bookId = null): Activity
+    protected function newActivityForUser(string $type): Activity
     {
         return $this->activity->newInstance()->forceFill([
-            'key'     => strtolower($key),
-            'user_id' => $this->user->id,
-            'book_id' => $bookId ?? 0,
+            'type'     => strtolower($type),
+            'user_id' => user()->id,
         ]);
     }
 
@@ -61,15 +63,13 @@ class ActivityService
      * and instead uses the 'extra' field with the entities name.
      * Used when an entity is deleted.
      */
-    public function removeEntity(Entity $entity): Collection
+    public function removeEntity(Entity $entity)
     {
-        $activities = $entity->activity()->get();
         $entity->activity()->update([
-            'extra'       => $entity->name,
-            'entity_id'   => 0,
-            'entity_type' => '',
+            'detail'       => $entity->name,
+            'entity_id'   => null,
+            'entity_type' => null,
         ]);
-        return $activities;
     }
 
     /**
@@ -94,17 +94,30 @@ class ActivityService
      */
     public function entityActivity(Entity $entity, int $count = 20, int $page = 1): array
     {
+        /** @var [string => int[]] $queryIds */
+        $queryIds = [$entity->getMorphClass() => [$entity->id]];
+
         if ($entity->isA('book')) {
-            $query = $this->activity->newQuery()->where('book_id', '=', $entity->id);
-        } else {
-            $query = $this->activity->newQuery()->where('entity_type', '=', $entity->getMorphClass())
-                ->where('entity_id', '=', $entity->id);
+            $queryIds[(new Chapter)->getMorphClass()] = $entity->chapters()->visible()->pluck('id');
+        }
+        if ($entity->isA('book') || $entity->isA('chapter')) {
+            $queryIds[(new Page)->getMorphClass()] = $entity->pages()->visible()->pluck('id');
         }
 
-        $activity = $this->permissionService
-            ->filterRestrictedEntityRelations($query, 'activities', 'entity_id', 'entity_type')
-            ->orderBy('created_at', 'desc')
-            ->with(['entity', 'user.avatar'])
+        $query = $this->activity->newQuery();
+        $query->where(function (Builder $query) use ($queryIds) {
+            foreach ($queryIds as $morphClass => $idArr) {
+                $query->orWhere(function (Builder $innerQuery) use ($morphClass, $idArr) {
+                    $innerQuery->where('entity_type', '=', $morphClass)
+                        ->whereIn('entity_id', $idArr);
+                });
+            }
+        });
+
+        $activity = $query->orderBy('created_at', 'desc')
+            ->with(['entity' => function (Relation $query) {
+                $query->withTrashed();
+            }, 'user.avatar'])
             ->skip($count * ($page - 1))
             ->take($count)
             ->get();
@@ -152,9 +165,9 @@ class ActivityService
     /**
      * Flashes a notification message to the session if an appropriate message is available.
      */
-    protected function setNotification(string $activityKey)
+    protected function setNotification(string $type)
     {
-        $notificationTextKey = 'activities.' . $activityKey . '_notification';
+        $notificationTextKey = 'activities.' . $type . '_notification';
         if (trans()->has($notificationTextKey)) {
             $message = trans($notificationTextKey);
             session()->flash('success', $message);
diff --git a/app/Actions/ActivityType.php b/app/Actions/ActivityType.php
new file mode 100644 (file)
index 0000000..216f612
--- /dev/null
@@ -0,0 +1,51 @@
+<?php namespace BookStack\Actions;
+
+class ActivityType
+{
+    const PAGE_CREATE = 'page_create';
+    const PAGE_UPDATE = 'page_update';
+    const PAGE_DELETE = 'page_delete';
+    const PAGE_RESTORE = 'page_restore';
+    const PAGE_MOVE = 'page_move';
+
+    const CHAPTER_CREATE = 'chapter_create';
+    const CHAPTER_UPDATE = 'chapter_update';
+    const CHAPTER_DELETE = 'chapter_delete';
+    const CHAPTER_MOVE = 'chapter_move';
+
+    const BOOK_CREATE = 'book_create';
+    const BOOK_UPDATE = 'book_update';
+    const BOOK_DELETE = 'book_delete';
+    const BOOK_SORT = 'book_sort';
+
+    const BOOKSHELF_CREATE = 'bookshelf_create';
+    const BOOKSHELF_UPDATE = 'bookshelf_update';
+    const BOOKSHELF_DELETE = 'bookshelf_delete';
+
+    const COMMENTED_ON = 'commented_on';
+    const PERMISSIONS_UPDATE = 'permissions_update';
+
+    const SETTINGS_UPDATE = 'settings_update';
+    const MAINTENANCE_ACTION_RUN = 'maintenance_action_run';
+
+    const RECYCLE_BIN_EMPTY = 'recycle_bin_empty';
+    const RECYCLE_BIN_RESTORE = 'recycle_bin_restore';
+    const RECYCLE_BIN_DESTROY = 'recycle_bin_destroy';
+
+    const USER_CREATE = 'user_create';
+    const USER_UPDATE = 'user_update';
+    const USER_DELETE = 'user_delete';
+
+    const API_TOKEN_CREATE = 'api_token_create';
+    const API_TOKEN_UPDATE = 'api_token_update';
+    const API_TOKEN_DELETE = 'api_token_delete';
+
+    const ROLE_CREATE = 'role_create';
+    const ROLE_UPDATE = 'role_update';
+    const ROLE_DELETE = 'role_delete';
+
+    const AUTH_PASSWORD_RESET = 'auth_password_reset_request';
+    const AUTH_PASSWORD_RESET_UPDATE = 'auth_password_reset_update';
+    const AUTH_LOGIN = 'auth_login';
+    const AUTH_REGISTER = 'auth_register';
+}
\ No newline at end of file
index 655d452219b8200da95cab132b605d0fc4ea1543..f5269e2534d7a8ea2cbc77cbfdefa5adac815847 100644 (file)
@@ -1,6 +1,8 @@
 <?php namespace BookStack\Actions;
 
-use BookStack\Ownable;
+use BookStack\Model;
+use BookStack\Traits\HasCreatorAndUpdater;
+use Illuminate\Database\Eloquent\Relations\MorphTo;
 
 /**
  * @property string text
@@ -8,25 +10,25 @@ use BookStack\Ownable;
  * @property int|null parent_id
  * @property int local_id
  */
-class Comment extends Ownable
+class Comment extends Model
 {
+    use HasCreatorAndUpdater;
+
     protected $fillable = ['text', 'parent_id'];
     protected $appends = ['created', 'updated'];
 
     /**
      * Get the entity that this comment belongs to
-     * @return \Illuminate\Database\Eloquent\Relations\MorphTo
      */
-    public function entity()
+    public function entity(): MorphTo
     {
         return $this->morphTo('entity');
     }
 
     /**
      * Check if a comment has been updated since creation.
-     * @return bool
      */
-    public function isUpdated()
+    public function isUpdated(): bool
     {
         return $this->updated_at->timestamp > $this->created_at->timestamp;
     }
index 4dfe3ddb64f86f3252418b3b83d04ef6367d39b6..13a83e7fdd247064983c64928811b49db2ba4ca1 100644 (file)
@@ -1,7 +1,8 @@
 <?php namespace BookStack\Actions;
 
-use BookStack\Entities\Entity;
+use BookStack\Entities\Models\Entity;
 use League\CommonMark\CommonMarkConverter;
+use BookStack\Facades\Activity as ActivityService;
 
 /**
  * Class CommentRepo
@@ -44,6 +45,7 @@ class CommentRepo
         $comment->parent_id = $parent_id;
 
         $entity->comments()->save($comment);
+        ActivityService::addForEntity($entity, ActivityType::COMMENTED_ON);
         return $comment;
     }
 
index 80a91150868e9cd87be62685891237c392606328..5968ffe6d5ea9d875cf4fa574ac63d3d4c8d62a1 100644 (file)
@@ -2,14 +2,10 @@
 
 use BookStack\Model;
 
-/**
- * Class Attribute
- * @package BookStack
- */
 class Tag extends Model
 {
     protected $fillable = ['name', 'value', 'order'];
-    protected $hidden = ['id', 'entity_id', 'entity_type'];
+    protected $hidden = ['id', 'entity_id', 'entity_type', 'created_at', 'updated_at'];
 
     /**
      * Get the entity that this tag belongs to
index 0297d8bc6997b790085a485b1761085cc946ce59..f58589ccd589c4d019d055156f8596974da447c4 100644 (file)
@@ -1,7 +1,7 @@
 <?php namespace BookStack\Actions;
 
 use BookStack\Auth\Permissions\PermissionService;
-use BookStack\Entities\Entity;
+use BookStack\Entities\Models\Entity;
 use DB;
 use Illuminate\Support\Collection;
 
index 324bfaa4ef9fb14c722148529c4bb895692a12e8..ec57cdb764b6e701a109c20a7ad9668048db2f94 100644 (file)
@@ -1,8 +1,8 @@
 <?php namespace BookStack\Actions;
 
 use BookStack\Auth\Permissions\PermissionService;
-use BookStack\Entities\Book;
-use BookStack\Entities\Entity;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Entity;
 use BookStack\Entities\EntityProvider;
 use DB;
 use Illuminate\Support\Collection;
@@ -28,7 +28,7 @@ class ViewService
 
     /**
      * Add a view to the given entity.
-     * @param \BookStack\Entities\Entity $entity
+     * @param \BookStack\Entities\Models\Entity $entity
      * @return int
      */
     public function add(Entity $entity)
@@ -79,29 +79,26 @@ class ViewService
 
     /**
      * Get all recently viewed entities for the current user.
-     * @param int $count
-     * @param int $page
-     * @param Entity|bool $filterModel
-     * @return mixed
      */
-    public function getUserRecentlyViewed($count = 10, $page = 0, $filterModel = false)
+    public function getUserRecentlyViewed(int $count = 10, int $page = 1)
     {
         $user = user();
         if ($user === null || $user->isDefault()) {
             return collect();
         }
 
-        $query = $this->permissionService
-            ->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type');
-
-        if ($filterModel) {
-            $query = $query->where('viewable_type', '=', $filterModel->getMorphClass());
+        $all = collect();
+        /** @var Entity $instance */
+        foreach ($this->entityProvider->all() as $name => $instance) {
+            $items = $instance::visible()->withLastView()
+                ->orderBy('last_viewed_at', 'desc')
+                ->skip($count * ($page - 1))
+                ->take($count)
+                ->get();
+            $all = $all->concat($items);
         }
-        $query = $query->where('user_id', '=', $user->id);
 
-        $viewables = $query->with('viewable')->orderBy('updated_at', 'desc')
-            ->skip($count * $page)->take($count)->get()->pluck('viewable');
-        return $viewables;
+        return $all->sortByDesc('last_viewed_at')->slice(0, $count);
     }
 
     /**
index ddba24bdb65d6ec8dc1474e3d50996c546623228..2953647bb33b21d59f565ce990cffecfc7a8beea 100644 (file)
@@ -1,7 +1,9 @@
 <?php namespace BookStack\Api;
 
 use BookStack\Http\Controllers\Api\ApiController;
+use Illuminate\Contracts\Container\BindingResolutionException;
 use Illuminate\Support\Collection;
+use Illuminate\Support\Facades\Cache;
 use Illuminate\Support\Facades\Route;
 use Illuminate\Support\Str;
 use ReflectionClass;
@@ -14,10 +16,27 @@ class ApiDocsGenerator
     protected $reflectionClasses = [];
     protected $controllerClasses = [];
 
+    /**
+     * Load the docs form the cache if existing
+     * otherwise generate and store in the cache.
+     */
+    public static function generateConsideringCache(): Collection
+    {
+        $appVersion = trim(file_get_contents(base_path('version')));
+        $cacheKey = 'api-docs::' . $appVersion;
+        if (Cache::has($cacheKey) && config('app.env') === 'production') {
+            $docs = Cache::get($cacheKey);
+        } else {
+            $docs = (new static())->generate();
+            Cache::put($cacheKey, $docs, 60 * 24);
+        }
+        return $docs;
+    }
+
     /**
      * Generate API documentation.
      */
-    public function generate(): Collection
+    protected function generate(): Collection
     {
         $apiRoutes = $this->getFlatApiRoutes();
         $apiRoutes = $this->loadDetailsFromControllers($apiRoutes);
@@ -58,7 +77,7 @@ class ApiDocsGenerator
 
     /**
      * Load body params and their rules by inspecting the given class and method name.
-     * @throws \Illuminate\Contracts\Container\BindingResolutionException
+     * @throws BindingResolutionException
      */
     protected function getBodyParamsFromClass(string $className, string $methodName): ?array
     {
index 523c3b8b80ec2a883e590b73cbf701d2d7e67829..defaa7e954af69354fc3f3c637720f392fbc005c 100644 (file)
@@ -1,11 +1,21 @@
 <?php namespace BookStack\Api;
 
 use BookStack\Auth\User;
+use BookStack\Interfaces\Loggable;
 use Illuminate\Database\Eloquent\Model;
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
 use Illuminate\Support\Carbon;
 
-class ApiToken extends Model
+/**
+ * Class ApiToken
+ * @property int $id
+ * @property string $token_id
+ * @property string $secret
+ * @property string $name
+ * @property Carbon $expires_at
+ * @property User $user
+ */
+class ApiToken extends Model implements Loggable
 {
     protected $fillable = ['name', 'expires_at'];
     protected $casts = [
@@ -28,4 +38,12 @@ class ApiToken extends Model
     {
         return Carbon::now()->addYears(100)->format('Y-m-d');
     }
+
+    /**
+     * @inheritdoc
+     */
+    public function logDescriptor(): string
+    {
+        return "({$this->id}) {$this->name}; User: {$this->user->logDescriptor()}";
+    }
 }
index f3d05366d9544ee5dde9c1a17304199b83f834c7..9a0c691c8bbb272301c45bccbf6b7c3739554f84 100644 (file)
@@ -15,8 +15,6 @@ use Illuminate\Contracts\Session\Session;
  * guard with 'remember' functionality removed. Basic auth and event emission
  * has also been removed to keep this simple. Designed to be extended by external
  * Auth Guards.
- *
- * @package Illuminate\Auth
  */
 class ExternalBaseSessionGuard implements StatefulGuard
 {
index 4023913ed77bb47caac93d5dc0d84a6673be4f90..68683bb4368b61ac23769fdce2eeb1ed7e876497 100644 (file)
@@ -9,8 +9,6 @@ namespace BookStack\Auth\Access\Guards;
  * into the default laravel 'Guard' auth flow. Instead most of the logic is done
  * via the Saml2 controller & Saml2Service. This class provides a safer, thin
  * version of SessionGuard.
- *
- * @package BookStack\Auth\Access\Guards
  */
 class Saml2SessionGuard extends ExternalBaseSessionGuard
 {
index 843a2f204920e9bd5154efe477c5212f73efa057..6b7bd9b9bf2a4d4699a74609ab507d33cc72f98d 100644 (file)
@@ -4,7 +4,6 @@
  * Class Ldap
  * An object-orientated thin abstraction wrapper for common PHP LDAP functions.
  * Allows the standard LDAP functions to be mocked for testing.
- * @package BookStack\Services
  */
 class Ldap
 {
index ecc92c117d46ccb84de50a8c2defc2c75322a3a7..2aff6c37d5140c84644f75077cceea2cd25d43d1 100644 (file)
@@ -1,9 +1,11 @@
 <?php namespace BookStack\Auth\Access;
 
+use BookStack\Actions\ActivityType;
 use BookStack\Auth\SocialAccount;
 use BookStack\Auth\User;
 use BookStack\Auth\UserRepo;
 use BookStack\Exceptions\UserRegistrationException;
+use BookStack\Facades\Activity;
 use Exception;
 
 class RegistrationService
@@ -68,6 +70,8 @@ class RegistrationService
             $newUser->socialAccounts()->save($socialAccount);
         }
 
+        Activity::add(ActivityType::AUTH_REGISTER, $socialAccount ?? $newUser);
+
         // Start email confirmation flow if required
         if ($this->emailConfirmationService->confirmationRequired() && !$emailConfirmed) {
             $newUser->save();
index 89ddd0011ecb037c8831b4a79260a18030ee7abe..0316ff976e4623e222ac69cdcf956f8efab55334 100644 (file)
@@ -1,9 +1,11 @@
 <?php namespace BookStack\Auth\Access;
 
+use BookStack\Actions\ActivityType;
 use BookStack\Auth\User;
 use BookStack\Exceptions\JsonDebugException;
 use BookStack\Exceptions\SamlException;
 use BookStack\Exceptions\UserRegistrationException;
+use BookStack\Facades\Activity;
 use Exception;
 use Illuminate\Support\Str;
 use OneLogin\Saml2\Auth;
@@ -372,6 +374,7 @@ class Saml2Service extends ExternalAuthService
         }
 
         auth()->login($user);
+        Activity::add(ActivityType::AUTH_LOGIN, "saml2; {$user->logDescriptor()}");
         return $user;
     }
 }
index 657aae3f327d530557b37c4ff2ce0f6f7126114a..b0383a938522e0ba67cad2213a29895f5d82cba2 100644 (file)
@@ -1,10 +1,12 @@
 <?php namespace BookStack\Auth\Access;
 
+use BookStack\Actions\ActivityType;
 use BookStack\Auth\SocialAccount;
 use BookStack\Auth\UserRepo;
 use BookStack\Exceptions\SocialDriverNotConfigured;
 use BookStack\Exceptions\SocialSignInAccountNotUsed;
 use BookStack\Exceptions\UserRegistrationException;
+use BookStack\Facades\Activity;
 use Illuminate\Support\Str;
 use Laravel\Socialite\Contracts\Factory as Socialite;
 use Laravel\Socialite\Contracts\Provider;
@@ -98,6 +100,7 @@ class SocialAuthService
         // Simply log the user into the application.
         if (!$isLoggedIn && $socialAccount !== null) {
             auth()->login($socialAccount->user);
+            Activity::add(ActivityType::AUTH_LOGIN, $socialAccount);
             return redirect()->intended('/');
         }
 
index 8d1776bd8a45ad59d74d417a82b370ccf20b1fa0..6f7fa582b83a6610b57813bc009fd44582761ab3 100644 (file)
@@ -1,7 +1,7 @@
 <?php namespace BookStack\Auth\Permissions;
 
 use BookStack\Auth\Role;
-use BookStack\Entities\Entity;
+use BookStack\Entities\Models\Entity;
 use BookStack\Model;
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
 use Illuminate\Database\Eloquent\Relations\MorphOne;
index 043227aab9504910f4c9704081b73099456fccb1..d858a7c18eea3a8aa546345189c22d413b499171 100644 (file)
@@ -2,10 +2,12 @@
 
 use BookStack\Auth\Permissions;
 use BookStack\Auth\Role;
-use BookStack\Entities\Book;
-use BookStack\Entities\Entity;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Entity;
 use BookStack\Entities\EntityProvider;
-use BookStack\Ownable;
+use BookStack\Model;
+use BookStack\Traits\HasCreatorAndUpdater;
+use BookStack\Traits\HasOwner;
 use Illuminate\Database\Connection;
 use Illuminate\Database\Eloquent\Builder;
 use Illuminate\Database\Query\Builder as QueryBuilder;
@@ -48,11 +50,6 @@ class PermissionService
 
     /**
      * PermissionService constructor.
-     * @param JointPermission $jointPermission
-     * @param EntityPermission $entityPermission
-     * @param Role $role
-     * @param Connection $db
-     * @param EntityProvider $entityProvider
      */
     public function __construct(
         JointPermission $jointPermission,
@@ -79,7 +76,7 @@ class PermissionService
 
     /**
      * Prepare the local entity cache and ensure it's empty
-     * @param \BookStack\Entities\Entity[] $entities
+     * @param \BookStack\Entities\Models\Entity[] $entities
      */
     protected function readyEntityCache($entities = [])
     {
@@ -116,7 +113,7 @@ class PermissionService
     /**
      * Get a chapter via ID, Checks local cache
      * @param $chapterId
-     * @return \BookStack\Entities\Book
+     * @return \BookStack\Entities\Models\Book
      */
     protected function getChapter($chapterId)
     {
@@ -173,7 +170,7 @@ class PermissionService
         });
 
         // Chunk through all bookshelves
-        $this->entityProvider->bookshelf->newQuery()->select(['id', 'restricted', 'created_by'])
+        $this->entityProvider->bookshelf->newQuery()->withTrashed()->select(['id', 'restricted', 'owned_by'])
             ->chunk(50, function ($shelves) use ($roles) {
                 $this->buildJointPermissionsForShelves($shelves, $roles);
             });
@@ -185,11 +182,11 @@ class PermissionService
      */
     protected function bookFetchQuery()
     {
-        return $this->entityProvider->book->newQuery()
-            ->select(['id', 'restricted', 'created_by'])->with(['chapters' => function ($query) {
-                $query->select(['id', 'restricted', 'created_by', 'book_id']);
+        return $this->entityProvider->book->withTrashed()->newQuery()
+            ->select(['id', 'restricted', 'owned_by'])->with(['chapters' => function ($query) {
+                $query->withTrashed()->select(['id', 'restricted', 'owned_by', 'book_id']);
             }, 'pages'  => function ($query) {
-                $query->select(['id', 'restricted', 'created_by', 'book_id', 'chapter_id']);
+                $query->withTrashed()->select(['id', 'restricted', 'owned_by', 'book_id', 'chapter_id']);
             }]);
     }
 
@@ -235,7 +232,7 @@ class PermissionService
 
     /**
      * Rebuild the entity jointPermissions for a particular entity.
-     * @param \BookStack\Entities\Entity $entity
+     * @param \BookStack\Entities\Models\Entity $entity
      * @throws \Throwable
      */
     public function buildJointPermissionsForEntity(Entity $entity)
@@ -291,7 +288,7 @@ class PermissionService
         });
 
         // Chunk through all bookshelves
-        $this->entityProvider->bookshelf->newQuery()->select(['id', 'restricted', 'created_by'])
+        $this->entityProvider->bookshelf->newQuery()->select(['id', 'restricted', 'owned_by'])
             ->chunk(50, function ($shelves) use ($roles) {
                 $this->buildJointPermissionsForShelves($shelves, $roles);
             });
@@ -330,7 +327,7 @@ class PermissionService
 
     /**
      * Delete all of the entity jointPermissions for a list of entities.
-     * @param \BookStack\Entities\Entity[] $entities
+     * @param \BookStack\Entities\Models\Entity[] $entities
      * @throws \Throwable
      */
     protected function deleteManyJointPermissionsForEntities($entities)
@@ -411,7 +408,7 @@ class PermissionService
 
     /**
      * Get the actions related to an entity.
-     * @param \BookStack\Entities\Entity $entity
+     * @param \BookStack\Entities\Models\Entity $entity
      * @return array
      */
     protected function getActions(Entity $entity)
@@ -497,7 +494,7 @@ class PermissionService
     /**
      * Create an array of data with the information of an entity jointPermissions.
      * Used to build data for bulk insertion.
-     * @param \BookStack\Entities\Entity $entity
+     * @param \BookStack\Entities\Models\Entity $entity
      * @param Role $role
      * @param $action
      * @param $permissionAll
@@ -513,21 +510,19 @@ class PermissionService
             'action'             => $action,
             'has_permission'     => $permissionAll,
             'has_permission_own' => $permissionOwn,
-            'created_by'         => $entity->getRawAttribute('created_by')
+            'owned_by'         => $entity->getRawAttribute('owned_by')
         ];
     }
 
     /**
      * Checks if an entity has a restriction set upon it.
-     * @param Ownable $ownable
-     * @param $permission
-     * @return bool
+     * @param HasCreatorAndUpdater|HasOwner $ownable
      */
-    public function checkOwnableUserAccess(Ownable $ownable, $permission)
+    public function checkOwnableUserAccess(Model $ownable, string $permission): bool
     {
         $explodedPermission = explode('-', $permission);
 
-        $baseQuery = $ownable->where('id', '=', $ownable->id);
+        $baseQuery = $ownable->newQuery()->where('id', '=', $ownable->id);
         $action = end($explodedPermission);
         $this->currentAction = $action;
 
@@ -571,7 +566,7 @@ class PermissionService
                 $query->where('has_permission', '=', 1)
                     ->orWhere(function ($query2) use ($userId) {
                         $query2->where('has_permission_own', '=', 1)
-                            ->where('created_by', '=', $userId);
+                            ->where('owned_by', '=', $userId);
                     });
             });
 
@@ -588,7 +583,7 @@ class PermissionService
     /**
      * Check if an entity has restrictions set on itself or its
      * parent tree.
-     * @param \BookStack\Entities\Entity $entity
+     * @param \BookStack\Entities\Models\Entity $entity
      * @param $action
      * @return bool|mixed
      */
@@ -620,7 +615,7 @@ class PermissionService
                         $query->where('has_permission', '=', true)
                             ->orWhere(function ($query) {
                                 $query->where('has_permission_own', '=', true)
-                                    ->where('created_by', '=', $this->currentUser()->id);
+                                    ->where('owned_by', '=', $this->currentUser()->id);
                             });
                     });
             });
@@ -644,7 +639,7 @@ class PermissionService
                         $query->where('has_permission', '=', true)
                             ->orWhere(function (Builder $query) {
                                 $query->where('has_permission_own', '=', true)
-                                    ->where('created_by', '=', $this->currentUser()->id);
+                                    ->where('owned_by', '=', $this->currentUser()->id);
                             });
                     });
             });
@@ -661,7 +656,7 @@ class PermissionService
             $query->where('draft', '=', false)
                 ->orWhere(function (Builder $query) {
                     $query->where('draft', '=', true)
-                        ->where('created_by', '=', $this->currentUser()->id);
+                        ->where('owned_by', '=', $this->currentUser()->id);
                 });
         });
     }
@@ -669,7 +664,7 @@ class PermissionService
     /**
      * Add restrictions for a generic entity
      * @param string $entityType
-     * @param Builder|\BookStack\Entities\Entity $query
+     * @param Builder|\BookStack\Entities\Models\Entity $query
      * @param string $action
      * @return Builder
      */
@@ -681,7 +676,7 @@ class PermissionService
                 $query->where('draft', '=', false)
                     ->orWhere(function ($query) {
                         $query->where('draft', '=', true)
-                            ->where('created_by', '=', $this->currentUser()->id);
+                            ->where('owned_by', '=', $this->currentUser()->id);
                     });
             });
         }
@@ -715,7 +710,7 @@ class PermissionService
                     ->where(function ($query) {
                         $query->where('has_permission', '=', true)->orWhere(function ($query) {
                             $query->where('has_permission_own', '=', true)
-                                ->where('created_by', '=', $this->currentUser()->id);
+                                ->where('owned_by', '=', $this->currentUser()->id);
                         });
                     });
             });
@@ -751,7 +746,7 @@ class PermissionService
                         ->where(function ($query) {
                             $query->where('has_permission', '=', true)->orWhere(function ($query) {
                                 $query->where('has_permission_own', '=', true)
-                                    ->where('created_by', '=', $this->currentUser()->id);
+                                    ->where('owned_by', '=', $this->currentUser()->id);
                             });
                         });
                 });
index ce61093cc5ed26d4f81ebb01671e4a1e91eac6e1..f54612a4339a3423557a994a3fa876636215799b 100644 (file)
@@ -1,10 +1,11 @@
 <?php namespace BookStack\Auth\Permissions;
 
+use BookStack\Actions\ActivityType;
 use BookStack\Auth\Role;
 use BookStack\Exceptions\PermissionsException;
+use BookStack\Facades\Activity;
 use Exception;
 use Illuminate\Database\Eloquent\Collection;
-use Illuminate\Support\Str;
 
 class PermissionsRepo
 {
@@ -60,6 +61,7 @@ class PermissionsRepo
         $permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
         $this->assignRolePermissions($role, $permissions);
         $this->permissionService->buildJointPermissionForRole($role);
+        Activity::add(ActivityType::ROLE_CREATE, $role);
         return $role;
     }
 
@@ -88,12 +90,13 @@ class PermissionsRepo
         $role->fill($roleData);
         $role->save();
         $this->permissionService->buildJointPermissionForRole($role);
+        Activity::add(ActivityType::ROLE_UPDATE, $role);
     }
 
     /**
      * Assign an list of permission names to an role.
      */
-    public function assignRolePermissions(Role $role, array $permissionNameArray = [])
+    protected function assignRolePermissions(Role $role, array $permissionNameArray = [])
     {
         $permissions = [];
         $permissionNameArray = array_values($permissionNameArray);
@@ -137,6 +140,7 @@ class PermissionsRepo
         }
 
         $this->permissionService->deleteJointPermissionsForRole($role);
+        Activity::add(ActivityType::ROLE_DELETE, $role);
         $role->delete();
     }
 }
index 13ec6df16b8488c23120d9a129e617fcad99182b..629cd6a955d8abf7961b67aa1c598d1d62d30658 100644 (file)
@@ -2,8 +2,10 @@
 
 use BookStack\Auth\Permissions\JointPermission;
 use BookStack\Auth\Permissions\RolePermission;
+use BookStack\Interfaces\Loggable;
 use BookStack\Model;
 use Illuminate\Database\Eloquent\Collection;
+use Illuminate\Database\Eloquent\Relations\BelongsToMany;
 use Illuminate\Database\Eloquent\Relations\HasMany;
 
 /**
@@ -14,7 +16,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
  * @property string $external_auth_id
  * @property string $system_name
  */
-class Role extends Model
+class Role extends Model implements Loggable
 {
 
     protected $fillable = ['display_name', 'description', 'external_auth_id'];
@@ -22,7 +24,7 @@ class Role extends Model
     /**
      * The roles that belong to the role.
      */
-    public function users()
+    public function users(): BelongsToMany
     {
         return $this->belongsToMany(User::class)->orderBy('name', 'asc');
     }
@@ -38,7 +40,7 @@ class Role extends Model
     /**
      * The RolePermissions that belong to the role.
      */
-    public function permissions()
+    public function permissions(): BelongsToMany
     {
         return $this->belongsToMany(RolePermission::class, 'permission_role', 'role_id', 'permission_id');
     }
@@ -104,4 +106,12 @@ class Role extends Model
     {
         return static::query()->where('system_name', '!=', 'admin')->get();
     }
+
+    /**
+     * @inheritdoc
+     */
+    public function logDescriptor(): string
+    {
+        return "({$this->id}) {$this->display_name}";
+    }
 }
index 804dbe6292973c16b7dc068ad00dd353c33fa48d..116cdc8546957a4071ad54bbc52aba5b8b6ede6c 100644 (file)
@@ -1,8 +1,14 @@
 <?php namespace BookStack\Auth;
 
+use BookStack\Interfaces\Loggable;
 use BookStack\Model;
 
-class SocialAccount extends Model
+/**
+ * Class SocialAccount
+ * @property string $driver
+ * @property User $user
+ */
+class SocialAccount extends Model implements Loggable
 {
 
     protected $fillable = ['user_id', 'driver', 'driver_id', 'timestamps'];
@@ -11,4 +17,12 @@ class SocialAccount extends Model
     {
         return $this->belongsTo(User::class);
     }
+
+    /**
+     * @inheritDoc
+     */
+    public function logDescriptor(): string
+    {
+        return "{$this->driver}; {$this->user->logDescriptor()}";
+    }
 }
index f65ef5316f67bfe3c68f40a6e2f7925ac7aa8f27..fdfd9e616567628ae7f04de837350e714f33cea4 100644 (file)
@@ -1,6 +1,8 @@
 <?php namespace BookStack\Auth;
 
+use BookStack\Actions\Activity;
 use BookStack\Api\ApiToken;
+use BookStack\Interfaces\Loggable;
 use BookStack\Model;
 use BookStack\Notifications\ResetPassword;
 use BookStack\Uploads\Image;
@@ -11,11 +13,12 @@ use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
 use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
 use Illuminate\Database\Eloquent\Relations\BelongsToMany;
 use Illuminate\Database\Eloquent\Relations\HasMany;
+use Illuminate\Database\Eloquent\Relations\HasOne;
 use Illuminate\Notifications\Notifiable;
+use Illuminate\Support\Collection;
 
 /**
  * Class User
- * @package BookStack\Auth
  * @property string $id
  * @property string $name
  * @property string $email
@@ -27,7 +30,7 @@ use Illuminate\Notifications\Notifiable;
  * @property string $external_auth_id
  * @property string $system_name
  */
-class User extends Model implements AuthenticatableContract, CanResetPasswordContract
+class User extends Model implements AuthenticatableContract, CanResetPasswordContract, Loggable
 {
     use Authenticatable, CanResetPassword, Notifiable;
 
@@ -54,7 +57,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
 
     /**
      * This holds the user's permissions when loaded.
-     * @var array
+     * @var ?Collection
      */
     protected $permissions;
 
@@ -128,35 +131,44 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
         }
     }
 
+    /**
+     * Check if the user has a particular permission.
+     */
+    public function can(string $permissionName): bool
+    {
+        if ($this->email === 'guest') {
+            return false;
+        }
+
+        return $this->permissions()->contains($permissionName);
+    }
+
     /**
      * Get all permissions belonging to a the current user.
-     * @param bool $cache
-     * @return \Illuminate\Database\Eloquent\Relations\HasManyThrough
      */
-    public function permissions($cache = true)
+    protected function permissions(): Collection
     {
-        if (isset($this->permissions) && $cache) {
+        if (isset($this->permissions)) {
             return $this->permissions;
         }
-        $this->load('roles.permissions');
-        $permissions = $this->roles->map(function ($role) {
-            return $role->permissions;
-        })->flatten()->unique();
-        $this->permissions = $permissions;
-        return $permissions;
+
+        $this->permissions = $this->newQuery()->getConnection()->table('role_user', 'ru')
+            ->select('role_permissions.name as name')->distinct()
+            ->leftJoin('permission_role', 'ru.role_id', '=', 'permission_role.role_id')
+            ->leftJoin('role_permissions', 'permission_role.permission_id', '=', 'role_permissions.id')
+            ->where('ru.user_id', '=', $this->id)
+            ->get()
+            ->pluck('name');
+
+        return $this->permissions;
     }
 
     /**
-     * Check if the user has a particular permission.
-     * @param $permissionName
-     * @return bool
+     * Clear any cached permissions on this instance.
      */
-    public function can($permissionName)
+    public function clearPermissionCache()
     {
-        if ($this->email === 'guest') {
-            return false;
-        }
-        return $this->permissions()->pluck('name')->contains($permissionName);
+        $this->permissions = null;
     }
 
     /**
@@ -229,6 +241,14 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
         return $this->hasMany(ApiToken::class);
     }
 
+    /**
+     * Get the latest activity instance for this user.
+     */
+    public function latestActivity(): HasOne
+    {
+        return $this->hasOne(Activity::class)->latest();
+    }
+
     /**
      * Get the url for editing this user.
      */
@@ -274,4 +294,12 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
     {
         $this->notify(new ResetPassword($token));
     }
+
+    /**
+     * @inheritdoc
+     */
+    public function logDescriptor(): string
+    {
+        return "({$this->id}) {$this->name}";
+    }
 }
index fdb8c0923882cabe08e03f4db68e6b89609e1468..6fb5dfa0fb48af6467a24c19bf360ca15a3d913e 100644 (file)
@@ -1,31 +1,32 @@
 <?php namespace BookStack\Auth;
 
 use Activity;
-use BookStack\Entities\Book;
-use BookStack\Entities\Bookshelf;
-use BookStack\Entities\Chapter;
-use BookStack\Entities\Page;
+use BookStack\Entities\EntityProvider;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Bookshelf;
+use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Models\Page;
 use BookStack\Exceptions\NotFoundException;
 use BookStack\Exceptions\UserUpdateException;
 use BookStack\Uploads\Image;
+use BookStack\Uploads\UserAvatars;
 use Exception;
 use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Database\Eloquent\Collection;
+use Illuminate\Pagination\LengthAwarePaginator;
 use Images;
 use Log;
 
 class UserRepo
 {
-
-    protected $user;
-    protected $role;
+    protected $userAvatar;
 
     /**
      * UserRepo constructor.
      */
-    public function __construct(User $user, Role $role)
+    public function __construct(UserAvatars $userAvatar)
     {
-        $this->user = $user;
-        $this->role = $role;
+        $this->userAvatar = $userAvatar;
     }
 
     /**
@@ -33,36 +34,40 @@ class UserRepo
      */
     public function getByEmail(string $email): ?User
     {
-        return $this->user->where('email', '=', $email)->first();
+        return User::query()->where('email', '=', $email)->first();
     }
 
     /**
-     * @param int $id
-     * @return User
+     * Get a user by their ID.
      */
-    public function getById($id)
+    public function getById(int $id): User
     {
-        return $this->user->newQuery()->findOrFail($id);
+        return User::query()->findOrFail($id);
     }
 
     /**
      * Get all the users with their permissions.
-     * @return Builder|static
      */
-    public function getAllUsers()
+    public function getAllUsers(): Collection
     {
-        return $this->user->with('roles', 'avatar')->orderBy('name', 'asc')->get();
+        return User::query()->with('roles', 'avatar')->orderBy('name', 'asc')->get();
     }
 
     /**
      * Get all the users with their permissions in a paginated format.
-     * @param int $count
-     * @param $sortData
-     * @return Builder|static
      */
-    public function getAllUsersPaginatedAndSorted($count, $sortData)
+    public function getAllUsersPaginatedAndSorted(int $count, array $sortData): LengthAwarePaginator
     {
-        $query = $this->user->with('roles', 'avatar')->orderBy($sortData['sort'], $sortData['order']);
+        $sort = $sortData['sort'];
+        if ($sort === 'latest_activity') {
+            $sort = \BookStack\Actions\Activity::query()->select('created_at')
+                ->whereColumn('activities.user_id', 'users.id')
+                ->latest()
+                ->take(1);
+        }
+
+        $query = User::query()->with(['roles', 'avatar', 'latestActivity'])
+            ->orderBy($sort, $sortData['order']);
 
         if ($sortData['search']) {
             $term = '%' . $sortData['search'] . '%';
@@ -89,14 +94,12 @@ class UserRepo
 
     /**
      * Assign a user to a system-level role.
-     * @param User $user
-     * @param $systemRoleName
      * @throws NotFoundException
      */
-    public function attachSystemRole(User $user, $systemRoleName)
+    public function attachSystemRole(User $user, string $systemRoleName)
     {
-        $role = $this->role->newQuery()->where('system_name', '=', $systemRoleName)->first();
-        if ($role === null) {
+        $role = Role::getSystemRole($systemRoleName);
+        if (is_null($role)) {
             throw new NotFoundException("Role '{$systemRoleName}' not found");
         }
         $user->attachRole($role);
@@ -104,26 +107,23 @@ class UserRepo
 
     /**
      * Checks if the give user is the only admin.
-     * @param User $user
-     * @return bool
      */
-    public function isOnlyAdmin(User $user)
+    public function isOnlyAdmin(User $user): bool
     {
         if (!$user->hasSystemRole('admin')) {
             return false;
         }
 
-        $adminRole = $this->role->getSystemRole('admin');
-        if ($adminRole->users->count() > 1) {
+        $adminRole = Role::getSystemRole('admin');
+        if ($adminRole->users()->count() > 1) {
             return false;
         }
+
         return true;
     }
 
     /**
      * Set the assigned user roles via an array of role IDs.
-     * @param User $user
-     * @param array $roles
      * @throws UserUpdateException
      */
     public function setUserRoles(User $user, array $roles)
@@ -138,14 +138,11 @@ class UserRepo
     /**
      * Check if the given user is the last admin and their new roles no longer
      * contains the admin role.
-     * @param User $user
-     * @param array $newRoles
-     * @return bool
      */
     protected function demotingLastAdmin(User $user, array $newRoles) : bool
     {
         if ($this->isOnlyAdmin($user)) {
-            $adminRole = $this->role->getSystemRole('admin');
+            $adminRole = Role::getSystemRole('admin');
             if (!in_array(strval($adminRole->id), $newRoles)) {
                 return true;
             }
@@ -159,41 +156,59 @@ class UserRepo
      */
     public function create(array $data, bool $emailConfirmed = false): User
     {
-        return $this->user->forceCreate([
+        $details = [
             'name'     => $data['name'],
             'email'    => $data['email'],
             'password' => bcrypt($data['password']),
             'email_confirmed' => $emailConfirmed,
             'external_auth_id' => $data['external_auth_id'] ?? '',
-        ]);
+        ];
+        return User::query()->forceCreate($details);
     }
 
     /**
      * Remove the given user from storage, Delete all related content.
-     * @param User $user
      * @throws Exception
      */
-    public function destroy(User $user)
+    public function destroy(User $user, ?int $newOwnerId = null)
     {
         $user->socialAccounts()->delete();
         $user->apiTokens()->delete();
         $user->delete();
         
         // Delete user profile images
-        $profileImages = Image::where('type', '=', 'user')->where('uploaded_to', '=', $user->id)->get();
+        $profileImages = Image::query()->where('type', '=', 'user')
+            ->where('uploaded_to', '=', $user->id)
+            ->get();
+
         foreach ($profileImages as $image) {
             Images::destroy($image);
         }
+
+        if (!empty($newOwnerId)) {
+            $newOwner = User::query()->find($newOwnerId);
+            if (!is_null($newOwner)) {
+                $this->migrateOwnership($user, $newOwner);
+            }
+        }
+    }
+
+    /**
+     * Migrate ownership of items in the system from one user to another.
+     */
+    protected function migrateOwnership(User $fromUser, User $toUser)
+    {
+        $entities = (new EntityProvider)->all();
+        foreach ($entities as $instance) {
+            $instance->newQuery()->where('owned_by', '=', $fromUser->id)
+                ->update(['owned_by' => $toUser->id]);
+        }
     }
 
     /**
      * Get the latest activity for a user.
-     * @param User $user
-     * @param int $count
-     * @param int $page
-     * @return array
      */
-    public function getActivity(User $user, $count = 20, $page = 0)
+    public function getActivity(User $user, int $count = 20, int $page = 0): array
     {
         return Activity::userActivity($user, $count, $page);
     }
@@ -234,33 +249,22 @@ class UserRepo
 
     /**
      * Get the roles in the system that are assignable to a user.
-     * @return mixed
      */
-    public function getAllRoles()
+    public function getAllRoles(): Collection
     {
-        return $this->role->newQuery()->orderBy('display_name', 'asc')->get();
+        return Role::query()->orderBy('display_name', 'asc')->get();
     }
 
     /**
      * Get an avatar image for a user and set it as their avatar.
      * Returns early if avatars disabled or not set in config.
-     * @param User $user
-     * @return bool
      */
-    public function downloadAndAssignUserAvatar(User $user)
+    public function downloadAndAssignUserAvatar(User $user): void
     {
-        if (!Images::avatarFetchEnabled()) {
-            return false;
-        }
-
         try {
-            $avatar = Images::saveUserAvatar($user);
-            $user->avatar()->associate($avatar);
-            $user->save();
-            return true;
+            $this->userAvatar->fetchAndAssignToUser($user);
         } catch (Exception $e) {
             Log::error('Failed to save user avatar image');
-            return false;
         }
     }
 }
index 8a1d175284851708caa41dff5e44ef6b4cb8a1a1..762845e9f2bb681d47504c66a72cf8d37d49cc85 100755 (executable)
@@ -31,6 +31,13 @@ return [
     // If set to false then a limit will not be enforced.
     'revision_limit' => env('REVISION_LIMIT', 50),
 
+    // The number of days that content will remain in the recycle bin before
+    // being considered for auto-removal. It is not a guarantee that content will
+    // be removed after this time.
+    // Set to 0 for no recycle bin functionality.
+    // Set to -1 for unlimited recycle bin lifetime.
+    'recycle_bin_lifetime' => env('RECYCLE_BIN_LIFETIME', 30),
+
     // Allow <script> tags to entered within page content.
     // <script> tags are escaped by default.
     // Even when overridden the WYSIWYG editor may still escape script content.
@@ -45,6 +52,10 @@ return [
     // and used by BookStack in URL generation.
     'url' => env('APP_URL', '') === 'http://bookstack.dev' ? '' : env('APP_URL', ''),
 
+    // A list of hosts that BookStack can be iframed within.
+    // Space separated if multiple. BookStack host domain is auto-inferred.
+    'iframe_hosts' => env('ALLOWED_IFRAME_HOSTS', null),
+
     // Application timezone for back-end date functions.
     'timezone' => env('APP_TIMEZONE', 'UTC'),
 
@@ -52,7 +63,7 @@ return [
     'locale' => env('APP_LANG', 'en'),
 
     // Locales available
-    'locales' => ['en', 'ar', 'bg', 'cs', 'da', 'de', 'de_informal', 'es', 'es_AR', 'fa', 'fr', 'he', 'hu', 'it', 'ja', 'ko', 'nl', 'pt', 'pt_BR', 'sk', 'sl', 'sv', 'pl',  'ru', 'th', 'tr', 'uk', 'vi', 'zh_CN', 'zh_TW',],
+    'locales' => ['en', 'ar', 'bg', 'cs', 'da', 'de', 'de_informal', 'es', 'es_AR', 'fa', 'fr', 'he', 'hu', 'it', 'ja', 'ko', 'nl', 'nb', 'pt', 'pt_BR', 'sk', 'sl', 'sv', 'pl',  'ru', 'th', 'tr', 'uk', 'vi', 'zh_CN', 'zh_TW',],
 
     //  Application Fallback Locale
     'fallback_locale' => 'en',
@@ -117,6 +128,7 @@ return [
         BookStack\Providers\EventServiceProvider::class,
         BookStack\Providers\RouteServiceProvider::class,
         BookStack\Providers\CustomFacadeProvider::class,
+        BookStack\Providers\CustomValidationServiceProvider::class,
     ],
 
     /*
index bd7d28300abae17112857ead07d3d000c4fd823b..30a5c53691d3a514d852d9d7fbe8f697d7d9321d 100644 (file)
@@ -42,13 +42,6 @@ return [
             'root'   => storage_path(),
         ],
 
-        'ftp' => [
-            'driver'   => 'ftp',
-            'host'     => 'ftp.example.com',
-            'username' => 'your-username',
-            'password' => 'your-password',
-        ],
-
         's3' => [
             'driver' => 's3',
             'key'    => env('STORAGE_S3_KEY', 'your-key'),
@@ -59,16 +52,6 @@ return [
             'use_path_style_endpoint' => env('STORAGE_S3_ENDPOINT', null) !== null,
         ],
 
-        'rackspace' => [
-            'driver'    => 'rackspace',
-            'username'  => 'your-username',
-            'key'       => 'your-key',
-            'container' => 'your-container',
-            'endpoint'  => 'https://identity.api.rackspacecloud.com/v2.0/',
-            'region'    => 'IAD',
-            'url_type'  => 'publicURL',
-        ],
-
     ],
 
 ];
index 37f1627bb5f5c151ae01748cdc06b8f5c7e7eeb2..571836bd2af2276a20732bfbe86459e83343551f 100644 (file)
@@ -1,5 +1,7 @@
 <?php
 
+use \Illuminate\Support\Str;
+
 /**
  * Session configuration options.
  *
@@ -69,7 +71,8 @@ return [
     // By setting this option to true, session cookies will only be sent back
     // to the server if the browser has a HTTPS connection. This will keep
     // the cookie from being sent to you if it can not be done securely.
-    'secure' => env('SESSION_SECURE_COOKIE', false),
+    'secure' => env('SESSION_SECURE_COOKIE', null)
+        ?? Str::startsWith(env('APP_URL'), 'https:'),
 
     // HTTP Access Only
     // Setting this value to true will prevent JavaScript from accessing the
@@ -80,6 +83,6 @@ return [
     // This option determines how your cookies behave when cross-site requests
     // take place, and can be used to mitigate CSRF attacks. By default, we
     // do not enable this as other CSRF protection services are in place.
-    // Options: lax, strict
-    'same_site' => null,
+    // Options: lax, strict, none
+    'same_site' => 'lax',
 ];
index f2e2d9fbd4465a3544e96e54c4649fb54fe81e05..93ca367a2102739b7067ddac3dfd9416749b6514 100644 (file)
@@ -14,8 +14,8 @@ class CleanupImages extends Command
      * @var string
      */
     protected $signature = 'bookstack:cleanup-images
-                            {--a|all : Include images that are used in page revisions}
-                            {--f|force : Actually run the deletions}
+                            {--a|all : Also delete images that are only used in old revisions}
+                            {--f|force : Actually run the deletions, Defaults to a dry-run}
                             ';
 
     /**
index 15f1fcc0a7138f057d3894c727cb62774a0ef37b..681a7564b282e3e6d279c1ffa2b9ddfbce96488e 100644 (file)
@@ -2,7 +2,7 @@
 
 namespace BookStack\Console\Commands;
 
-use BookStack\Entities\PageRevision;
+use BookStack\Entities\Models\PageRevision;
 use Illuminate\Console\Command;
 
 class ClearRevisions extends Command
index 6b5d35a476798a67d2bf16a42d2225b67363d632..d220c59f9ea7cedbf0a77c6e6ccb3bab27d7496e 100644 (file)
@@ -2,7 +2,7 @@
 
 namespace BookStack\Console\Commands;
 
-use BookStack\Entities\Bookshelf;
+use BookStack\Entities\Models\Bookshelf;
 use BookStack\Entities\Repos\BookshelfRepo;
 use Illuminate\Console\Command;
 
index e67da871763f8b9ef379c8687961ec2612d33bc3..3d1a3dca08db4045e528bfcab92c13984cbdeff9 100644 (file)
@@ -28,8 +28,6 @@ class CreateAdmin extends Command
 
     /**
      * Create a new command instance.
-     *
-     * @param UserRepo $userRepo
      */
     public function __construct(UserRepo $userRepo)
     {
index dc57f2cea764b3a8517de9453ae4ee05fd86d9a6..3dc3ec0af0e98b33bd3f1d741540dd13e1d319c5 100644 (file)
@@ -2,7 +2,7 @@
 
 namespace BookStack\Console\Commands;
 
-use BookStack\Entities\SearchService;
+use BookStack\Entities\Tools\SearchIndex;
 use DB;
 use Illuminate\Console\Command;
 
@@ -22,17 +22,15 @@ class RegenerateSearch extends Command
      */
     protected $description = 'Re-index all content for searching';
 
-    protected $searchService;
+    protected $searchIndex;
 
     /**
      * Create a new command instance.
-     *
-     * @param SearchService $searchService
      */
-    public function __construct(SearchService $searchService)
+    public function __construct(SearchIndex $searchIndex)
     {
         parent::__construct();
-        $this->searchService = $searchService;
+        $this->searchIndex = $searchIndex;
     }
 
     /**
@@ -45,10 +43,9 @@ class RegenerateSearch extends Command
         $connection = DB::getDefaultConnection();
         if ($this->option('database') !== null) {
             DB::setDefaultConnection($this->option('database'));
-            $this->searchService->setConnection(DB::connection($this->option('database')));
         }
 
-        $this->searchService->indexAllEntities();
+        $this->searchIndex->indexAllEntities();
         DB::setDefaultConnection($connection);
         $this->comment('Search index regenerated');
     }
index 43d63d026021dd07edcb1165841018ee213b13a3..cf7cf296c94fdbd8cdcb3b7dffd59767cb8000ad 100644 (file)
@@ -1,6 +1,7 @@
 <?php namespace BookStack\Entities;
 
-use BookStack\Entities\Managers\EntityContext;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Tools\ShelfContext;
 use Illuminate\View\View;
 
 class BreadcrumbsViewComposer
@@ -10,9 +11,9 @@ class BreadcrumbsViewComposer
 
     /**
      * BreadcrumbsViewComposer constructor.
-     * @param EntityContext $entityContextManager
+     * @param ShelfContext $entityContextManager
      */
-    public function __construct(EntityContext $entityContextManager)
+    public function __construct(ShelfContext $entityContextManager)
     {
         $this->entityContextManager = $entityContextManager;
     }
diff --git a/app/Entities/Chapter.php b/app/Entities/Chapter.php
deleted file mode 100644 (file)
index 3290afc..0000000
+++ /dev/null
@@ -1,74 +0,0 @@
-<?php namespace BookStack\Entities;
-
-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'];
-    protected $hidden = ['restricted', 'pivot'];
-
-    /**
-     * Get the pages that this chapter contains.
-     * @param string $dir
-     * @return mixed
-     */
-    public function pages($dir = 'ASC')
-    {
-        return $this->hasMany(Page::class)->orderBy('priority', $dir);
-    }
-
-    /**
-     * Get the url of this chapter.
-     * @param string|bool $path
-     * @return string
-     */
-    public function getUrl($path = false)
-    {
-        $bookSlug = $this->getAttribute('bookSlug') ? $this->getAttribute('bookSlug') : $this->book->slug;
-        $fullPath = '/books/' . urlencode($bookSlug) . '/chapter/' . urlencode($this->slug);
-
-        if ($path !== false) {
-            $fullPath .= '/' . trim($path, '/');
-        }
-
-        return url($fullPath);
-    }
-
-    /**
-     * Get an excerpt of this chapter's description to the specified length or less.
-     * @param int $length
-     * @return string
-     */
-    public function getExcerpt(int $length = 100)
-    {
-        $description = $this->text ?? $this->description;
-        return mb_strlen($description) > $length ? mb_substr($description, 0, $length-3) . '...' : $description;
-    }
-
-    /**
-     * Check if this chapter has any child pages.
-     * @return bool
-     */
-    public function hasChildren()
-    {
-        return count($this->pages) > 0;
-    }
-
-    /**
-     * Get the visible pages in this chapter.
-     */
-    public function getVisiblePages(): Collection
-    {
-        return $this->pages()->visible()
-        ->orderBy('draft', 'desc')
-        ->orderBy('priority', 'asc')
-        ->get();
-    }
-}
index 6bf923b3112aa8e7387fd6eedeb601a601511dad..c77a57d61a5710edf652082839492a394c21a341 100644 (file)
@@ -1,13 +1,18 @@
 <?php namespace BookStack\Entities;
 
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Bookshelf;
+use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Models\Entity;
+use BookStack\Entities\Models\Page;
+use BookStack\Entities\Models\PageRevision;
+
 /**
  * Class EntityProvider
  *
  * Provides access to the core entity models.
  * Wrapped up in this provider since they are often used together
  * so this is a neater alternative to injecting all in individually.
- *
- * @package BookStack\Entities
  */
 class EntityProvider
 {
@@ -37,26 +42,20 @@ class EntityProvider
      */
     public $pageRevision;
 
-    /**
-     * EntityProvider constructor.
-     */
-    public function __construct(
-        Bookshelf $bookshelf,
-        Book $book,
-        Chapter $chapter,
-        Page $page,
-        PageRevision $pageRevision
-    ) {
-        $this->bookshelf = $bookshelf;
-        $this->book = $book;
-        $this->chapter = $chapter;
-        $this->page = $page;
-        $this->pageRevision = $pageRevision;
+
+    public function __construct()
+    {
+        $this->bookshelf = new Bookshelf();
+        $this->book = new Book();
+        $this->chapter = new Chapter();
+        $this->page = new Page();
+        $this->pageRevision = new PageRevision();
     }
 
     /**
      * Fetch all core entity types as an associated array
      * with their basic names as the keys.
+     * @return array<Entity>
      */
     public function all(): array
     {
diff --git a/app/Entities/Managers/TrashCan.php b/app/Entities/Managers/TrashCan.php
deleted file mode 100644 (file)
index 1a32294..0000000
+++ /dev/null
@@ -1,109 +0,0 @@
-<?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);
-        }
-    }
-}
similarity index 77%
rename from app/Entities/Book.php
rename to app/Entities/Models/Book.php
index af8344b88f5cb440b9abeb6913e3e55ffeda68f6..6c56767655c894b22c548e7ff552a07f36940ea6 100644 (file)
@@ -1,4 +1,4 @@
-<?php namespace BookStack\Entities;
+<?php namespace BookStack\Entities\Models;
 
 use BookStack\Uploads\Image;
 use Exception;
@@ -12,26 +12,20 @@ use Illuminate\Support\Collection;
  * @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'];
-    protected $hidden = ['restricted', 'pivot', 'image_id'];
+    protected $hidden = ['restricted', 'pivot', 'image_id', 'deleted_at'];
 
     /**
      * Get the url for this book.
-     * @param string|bool $path
-     * @return string
      */
-    public function getUrl($path = false)
+    public function getUrl(string $path = ''): string
     {
-        if ($path !== false) {
-            return url('/books/' . urlencode($this->slug) . '/' . trim($path, '/'));
-        }
-        return url('/books/' . urlencode($this->slug));
+        return url('/books/' . implode('/', [urlencode($this->slug), trim($path, '/')]));
     }
 
     /**
@@ -117,15 +111,4 @@ class Book extends Entity implements HasCoverImage
         $chapters = $this->chapters()->visible()->get();
         return $pages->concat($chapters)->sortBy('priority')->sortByDesc('draft');
     }
-
-    /**
-     * Get an excerpt of this book's description to the specified length or less.
-     * @param int $length
-     * @return string
-     */
-    public function getExcerpt(int $length = 100)
-    {
-        $description = $this->description;
-        return mb_strlen($description) > $length ? mb_substr($description, 0, $length-3) . '...' : $description;
-    }
 }
similarity index 87%
rename from app/Entities/BookChild.php
rename to app/Entities/Models/BookChild.php
index 6eac4375ddce6c271a06669a8eaa108b774d55e2..91f62dc403c64fdde0079b223e4574bd84199251 100644 (file)
@@ -1,5 +1,8 @@
-<?php namespace BookStack\Entities;
+<?php namespace BookStack\Entities\Models;
 
+use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Models\Entity;
+use BookStack\Entities\Models\Book;
 use Illuminate\Database\Eloquent\Builder;
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
 
@@ -10,7 +13,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
  * @property Book $book
  * @method Builder whereSlugs(string $bookSlug, string $childSlug)
  */
-class BookChild extends Entity
+abstract class BookChild extends Entity
 {
 
     /**
@@ -45,9 +48,6 @@ class BookChild extends Entity
         $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) {
similarity index 78%
rename from app/Entities/Bookshelf.php
rename to app/Entities/Models/Bookshelf.php
index 474ba51cd8204bf27dfc95ad421029cdaa8e7375..8ffd06d2e2f1b9d22dee6cabb23318976c85ddac 100644 (file)
@@ -1,4 +1,4 @@
-<?php namespace BookStack\Entities;
+<?php namespace BookStack\Entities\Models;
 
 use BookStack\Uploads\Image;
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
@@ -12,7 +12,7 @@ class Bookshelf extends Entity implements HasCoverImage
 
     protected $fillable = ['name', 'description', 'image_id'];
 
-    protected $hidden = ['restricted', 'image_id'];
+    protected $hidden = ['restricted', 'image_id', 'deleted_at'];
 
     /**
      * Get the books in this shelf.
@@ -36,15 +36,10 @@ class Bookshelf extends Entity implements HasCoverImage
 
     /**
      * Get the url for this bookshelf.
-     * @param string|bool $path
-     * @return string
      */
-    public function getUrl($path = false)
+    public function getUrl(string $path = ''): string
     {
-        if ($path !== false) {
-            return url('/shelves/' . urlencode($this->slug) . '/' . trim($path, '/'));
-        }
-        return url('/shelves/' . urlencode($this->slug));
+        return url('/shelves/' . implode('/', [urlencode($this->slug), trim($path, '/')]));
     }
 
     /**
@@ -85,17 +80,6 @@ class Bookshelf extends Entity implements HasCoverImage
         return 'cover_shelf';
     }
 
-    /**
-     * Get an excerpt of this book's description to the specified length or less.
-     * @param int $length
-     * @return string
-     */
-    public function getExcerpt(int $length = 100)
-    {
-        $description = $this->description;
-        return mb_strlen($description) > $length ? mb_substr($description, 0, $length-3) . '...' : $description;
-    }
-
     /**
      * Check if this shelf contains the given book.
      * @param Book $book
diff --git a/app/Entities/Models/Chapter.php b/app/Entities/Models/Chapter.php
new file mode 100644 (file)
index 0000000..257b19e
--- /dev/null
@@ -0,0 +1,52 @@
+<?php namespace BookStack\Entities\Models;
+
+use Illuminate\Support\Collection;
+
+/**
+ * Class Chapter
+ * @property Collection<Page> $pages
+ */
+class Chapter extends BookChild
+{
+    public $searchFactor = 1.3;
+
+    protected $fillable = ['name', 'description', 'priority', 'book_id'];
+    protected $hidden = ['restricted', 'pivot', 'deleted_at'];
+
+    /**
+     * Get the pages that this chapter contains.
+     * @param string $dir
+     * @return mixed
+     */
+    public function pages($dir = 'ASC')
+    {
+        return $this->hasMany(Page::class)->orderBy('priority', $dir);
+    }
+
+    /**
+     * Get the url of this chapter.
+     */
+    public function getUrl($path = ''): string
+    {
+        $parts = [
+            'books',
+            urlencode($this->getAttribute('bookSlug') ?? $this->book->slug),
+            'chapter',
+            urlencode($this->slug),
+            trim($path, '/'),
+        ];
+
+        return url('/' . implode('/', $parts));
+    }
+
+    /**
+     * Get the visible pages in this chapter.
+     */
+    public function getVisiblePages(): Collection
+    {
+        return $this->pages()->visible()
+        ->orderBy('draft', 'desc')
+        ->orderBy('priority', 'asc')
+        ->get();
+    }
+}
diff --git a/app/Entities/Models/Deletion.php b/app/Entities/Models/Deletion.php
new file mode 100644 (file)
index 0000000..1be0ba4
--- /dev/null
@@ -0,0 +1,48 @@
+<?php namespace BookStack\Entities\Models;
+
+use BookStack\Auth\User;
+use BookStack\Entities\Models\Entity;
+use BookStack\Interfaces\Loggable;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
+use Illuminate\Database\Eloquent\Relations\MorphTo;
+
+class Deletion extends Model implements Loggable
+{
+
+    /**
+     * Get the related deletable record.
+     */
+    public function deletable(): MorphTo
+    {
+        return $this->morphTo('deletable')->withTrashed();
+    }
+
+    /**
+     * The the user that performed the deletion.
+     */
+    public function deleter(): BelongsTo
+    {
+        return $this->belongsTo(User::class, 'deleted_by');
+    }
+
+    /**
+     * Create a new deletion record for the provided entity.
+     */
+    public static function createForEntity(Entity $entity): Deletion
+    {
+        $record = (new self())->forceFill([
+            'deleted_by' => user()->id,
+            'deletable_type' => $entity->getMorphClass(),
+            'deletable_id' => $entity->id,
+        ]);
+        $record->save();
+        return $record;
+    }
+
+    public function logDescriptor(): string
+    {
+        $deletable = $this->deletable()->first();
+        return "Deletion ({$this->id}) for {$deletable->getType()} ({$deletable->id}) {$deletable->name}";
+    }
+}
similarity index 73%
rename from app/Entities/Entity.php
rename to app/Entities/Models/Entity.php
index cc7df46d4f653e201f9ba17c72a09c0b45046577..c6b2468b0814afd124d1c64a4e0f8b116b9ec8d5 100644 (file)
@@ -1,4 +1,4 @@
-<?php namespace BookStack\Entities;
+<?php namespace BookStack\Entities\Models;
 
 use BookStack\Actions\Activity;
 use BookStack\Actions\Comment;
@@ -6,12 +6,17 @@ use BookStack\Actions\Tag;
 use BookStack\Actions\View;
 use BookStack\Auth\Permissions\EntityPermission;
 use BookStack\Auth\Permissions\JointPermission;
+use BookStack\Entities\Tools\SearchIndex;
+use BookStack\Entities\Tools\SlugGenerator;
 use BookStack\Facades\Permissions;
-use BookStack\Ownable;
+use BookStack\Model;
+use BookStack\Traits\HasCreatorAndUpdater;
+use BookStack\Traits\HasOwner;
 use Carbon\Carbon;
 use Illuminate\Database\Eloquent\Builder;
 use Illuminate\Database\Eloquent\Collection;
 use Illuminate\Database\Eloquent\Relations\MorphMany;
+use Illuminate\Database\Eloquent\SoftDeletes;
 
 /**
  * Class Entity
@@ -31,11 +36,12 @@ use Illuminate\Database\Eloquent\Relations\MorphMany;
  * @method static Entity|Builder hasPermission(string $permission)
  * @method static Builder withLastView()
  * @method static Builder withViewCount()
- *
- * @package BookStack\Entities
  */
-class Entity extends Ownable
+abstract class Entity extends Model
 {
+    use SoftDeletes;
+    use HasCreatorAndUpdater;
+    use HasOwner;
 
     /**
      * @var string - Name of property where the main text content is found
@@ -50,7 +56,7 @@ class Entity extends Ownable
     /**
      * Get the entities that are visible to the current user.
      */
-    public function scopeVisible(Builder $query)
+    public function scopeVisible(Builder $query): Builder
     {
         return $this->scopeHasPermission($query, 'view');
     }
@@ -92,24 +98,18 @@ class Entity extends Ownable
     /**
      * Compares this entity to another given entity.
      * Matches by comparing class and id.
-     * @param $entity
-     * @return bool
      */
-    public function matches($entity)
+    public function matches(Entity $entity): bool
     {
         return [get_class($this), $this->id] === [get_class($entity), $entity->id];
     }
 
     /**
-     * Checks if an entity matches or contains another given entity.
-     * @param Entity $entity
-     * @return bool
+     * Checks if the current entity matches or contains the given.
      */
-    public function matchesOrContains(Entity $entity)
+    public function matchesOrContains(Entity $entity): bool
     {
-        $matches = [get_class($this), $this->id] === [get_class($entity), $entity->id];
-
-        if ($matches) {
+        if ($this->matches($entity)) {
             return true;
         }
 
@@ -126,9 +126,8 @@ class Entity extends Ownable
 
     /**
      * Gets the activity objects for this entity.
-     * @return MorphMany
      */
-    public function activity()
+    public function activity(): MorphMany
     {
         return $this->morphMany(Activity::class, 'entity')
             ->orderBy('created_at', 'desc');
@@ -137,26 +136,23 @@ class Entity extends Ownable
     /**
      * Get View objects for this entity.
      */
-    public function views()
+    public function views(): MorphMany
     {
         return $this->morphMany(View::class, 'viewable');
     }
 
     /**
      * Get the Tag models that have been user assigned to this entity.
-     * @return MorphMany
      */
-    public function tags()
+    public function tags(): MorphMany
     {
         return $this->morphMany(Tag::class, 'entity')->orderBy('order', 'asc');
     }
 
     /**
      * Get the comments for an entity
-     * @param bool $orderByCreated
-     * @return MorphMany
      */
-    public function comments($orderByCreated = true)
+    public function comments(bool $orderByCreated = true): MorphMany
     {
         $query = $this->morphMany(Comment::class, 'entity');
         return $orderByCreated ? $query->orderBy('created_at', 'asc') : $query;
@@ -164,9 +160,8 @@ class Entity extends Ownable
 
     /**
      * Get the related search terms.
-     * @return MorphMany
      */
-    public function searchTerms()
+    public function searchTerms(): MorphMany
     {
         return $this->morphMany(SearchTerm::class, 'entity');
     }
@@ -174,18 +169,15 @@ class Entity extends Ownable
     /**
      * Get this entities restrictions.
      */
-    public function permissions()
+    public function permissions(): MorphMany
     {
         return $this->morphMany(EntityPermission::class, 'restrictable');
     }
 
     /**
      * Check if this entity has a specific restriction set against it.
-     * @param $role_id
-     * @param $action
-     * @return bool
      */
-    public function hasRestriction($role_id, $action)
+    public function hasRestriction(int $role_id, string $action): bool
     {
         return $this->permissions()->where('role_id', '=', $role_id)
             ->where('action', '=', $action)->count() > 0;
@@ -193,45 +185,36 @@ class Entity extends Ownable
 
     /**
      * Get the entity jointPermissions this is connected to.
-     * @return MorphMany
      */
-    public function jointPermissions()
+    public function jointPermissions(): MorphMany
     {
         return $this->morphMany(JointPermission::class, 'entity');
     }
 
     /**
-     * Check if this instance or class is a certain type of entity.
-     * Examples of $type are 'page', 'book', 'chapter'
+     * Get the related delete records for this entity.
      */
-    public static function isA(string $type): bool
+    public function deletions(): MorphMany
     {
-        return static::getType() === strtolower($type);
+        return $this->morphMany(Deletion::class, 'deletable');
     }
 
     /**
-     * Get entity type.
-     * @return mixed
+     * Check if this instance or class is a certain type of entity.
+     * Examples of $type are 'page', 'book', 'chapter'
      */
-    public static function getType()
+    public static function isA(string $type): bool
     {
-        return strtolower(static::getClassName());
+        return static::getType() === strtolower($type);
     }
 
     /**
-     * Get an instance of an entity of the given type.
-     * @param $type
-     * @return Entity
+     * Get the entity type as a simple lowercase word.
      */
-    public static function getEntityInstance($type)
+    public static function getType(): string
     {
-        $types = ['Page', 'Book', 'Chapter', 'Bookshelf'];
-        $className = str_replace([' ', '-', '_'], '', ucwords($type));
-        if (!in_array($className, $types)) {
-            return null;
-        }
-
-        return app('BookStack\\Entities\\' . $className);
+        $className = array_slice(explode('\\', static::class), -1, 1)[0];
+        return strtolower($className);
     }
 
     /**
@@ -247,35 +230,45 @@ class Entity extends Ownable
 
     /**
      * Get the body text of this entity.
-     * @return mixed
      */
-    public function getText()
+    public function getText(): string
     {
-        return $this->{$this->textField};
+        return $this->{$this->textField} ?? '';
     }
 
     /**
      * Get an excerpt of this entity's descriptive content to the specified length.
-     * @param int $length
-     * @return mixed
      */
-    public function getExcerpt(int $length = 100)
+    public function getExcerpt(int $length = 100): string
     {
         $text = $this->getText();
+
         if (mb_strlen($text) > $length) {
             $text = mb_substr($text, 0, $length-3) . '...';
         }
+
         return trim($text);
     }
 
     /**
      * Get the url of this entity
-     * @param $path
-     * @return string
      */
-    public function getUrl($path = '/')
+    abstract public function getUrl(string $path = '/'): string;
+
+    /**
+     * Get the parent entity if existing.
+     * This is the "static" parent and does not include dynamic
+     * relations such as shelves to books.
+     */
+    public function getParent(): ?Entity
     {
-        return $path;
+        if ($this->isA('page')) {
+            return $this->chapter_id ? $this->chapter()->withTrashed()->first() : $this->book()->withTrashed()->first();
+        }
+        if ($this->isA('chapter')) {
+            return $this->book()->withTrashed()->first();
+        }
+        return null;
     }
 
     /**
@@ -292,8 +285,7 @@ class Entity extends Ownable
      */
     public function indexForSearch()
     {
-        $searchService = app()->make(SearchService::class);
-        $searchService->indexEntity(clone $this);
+        app(SearchIndex::class)->indexEntity(clone $this);
     }
 
     /**
@@ -301,8 +293,7 @@ class Entity extends Ownable
      */
     public function refreshSlug(): string
     {
-        $generator = new SlugGenerator($this);
-        $this->slug = $generator->generate();
+        $this->slug = (new SlugGenerator)->generate($this);
         return $this->slug;
     }
 }
similarity index 90%
rename from app/Entities/HasCoverImage.php
rename to app/Entities/Models/HasCoverImage.php
index 31277f4b69c59bb659c2842277015ffbccca843c..f3a486d1877f32a2ef3fcf9f2145868d94337005 100644 (file)
@@ -1,7 +1,7 @@
 <?php
 
 
-namespace BookStack\Entities;
+namespace BookStack\Entities\Models;
 
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
 
similarity index 70%
rename from app/Entities/Page.php
rename to app/Entities/Models/Page.php
index 32ba2981d807e7a2b2b7a35c7984e523a82448ca..52c64f0480af78fc64a30bae48c8f05b1d108407 100644 (file)
@@ -1,5 +1,6 @@
-<?php namespace BookStack\Entities;
+<?php namespace BookStack\Entities\Models;
 
+use BookStack\Entities\Tools\PageContent;
 use BookStack\Uploads\Attachment;
 use Illuminate\Database\Eloquent\Builder;
 use Illuminate\Database\Eloquent\Collection;
@@ -27,12 +28,17 @@ class Page extends BookChild
 
     public $textField = 'text';
 
-    protected $hidden = ['html', 'markdown', 'text', 'restricted', 'pivot'];
+    protected $hidden = ['html', 'markdown', 'text', 'restricted', 'pivot', 'deleted_at'];
+
+    protected $casts = [
+        'draft' => 'boolean',
+        'template' => 'boolean',
+    ];
 
     /**
      * Get the entities that are visible to the current user.
      */
-    public function scopeVisible(Builder $query)
+    public function scopeVisible(Builder $query): Builder
     {
         $query = Permissions::enforceDraftVisiblityOnQuery($query);
         return parent::scopeVisible($query);
@@ -49,14 +55,6 @@ class Page extends BookChild
         return $array;
     }
 
-    /**
-     * Get the parent item
-     */
-    public function parent(): Entity
-    {
-        return $this->chapter_id ? $this->chapter : $this->book;
-    }
-
     /**
      * Get the chapter that this page is in, If applicable.
      * @return BelongsTo
@@ -94,22 +92,19 @@ class Page extends BookChild
     }
 
     /**
-     * Get the url for this page.
-     * @param string|bool $path
-     * @return string
+     * Get the url of this page.
      */
-    public function getUrl($path = false)
+    public function getUrl($path = ''): string
     {
-        $bookSlug = $this->getAttribute('bookSlug') ? $this->getAttribute('bookSlug') : $this->book->slug;
-        $midText = $this->draft ? '/draft/' : '/page/';
-        $idComponent = $this->draft ? $this->id : urlencode($this->slug);
-
-        $url = '/books/' . urlencode($bookSlug) . $midText . $idComponent;
-        if ($path !== false) {
-            $url .= '/' . trim($path, '/');
-        }
-
-        return url($url);
+        $parts = [
+            'books',
+            urlencode($this->getAttribute('bookSlug') ?? $this->book->slug),
+            $this->draft ? 'draft' : 'page',
+            $this->draft ? $this->id : urlencode($this->slug),
+            trim($path, '/'),
+        ];
+
+        return url('/' . implode('/', $parts));
     }
 
     /**
@@ -120,4 +115,15 @@ class Page extends BookChild
     {
         return $this->revisions()->first();
     }
+
+    /**
+     * Get this page for JSON display.
+     */
+    public function forJsonDisplay(): Page
+    {
+        $refreshed = $this->refresh()->unsetRelations()->load(['tags', 'createdBy', 'updatedBy']);
+        $refreshed->setHidden(array_diff($refreshed->getHidden(), ['html', 'markdown']));
+        $refreshed->html = (new PageContent($refreshed))->render();
+        return $refreshed;
+    }
 }
similarity index 96%
rename from app/Entities/PageRevision.php
rename to app/Entities/Models/PageRevision.php
index 13dc713ba43be37453ca525f06e91f143793476e..76a3b15ffd44ea64e176eb1020773dc0ca41ec70 100644 (file)
@@ -1,6 +1,7 @@
-<?php namespace BookStack\Entities;
+<?php namespace BookStack\Entities\Models;
 
 use BookStack\Auth\User;
+use BookStack\Entities\Models\Page;
 use BookStack\Model;
 use Carbon\Carbon;
 
similarity index 89%
rename from app/Entities/SearchTerm.php
rename to app/Entities/Models/SearchTerm.php
index 886c4dbc1fe4a4041859357ebe293b8ecb79177d..f55cb8407b34c9ce5b4a1bf672178f320c840db5 100644 (file)
@@ -1,4 +1,4 @@
-<?php namespace BookStack\Entities;
+<?php namespace BookStack\Entities\Models;
 
 use BookStack\Model;
 
index 7c25e49813e18bf7f34b42ce68df72c780f141fa..8b2e70074fe09a3b95c8688b81061f90fef2760c 100644 (file)
@@ -2,11 +2,13 @@
 
 namespace BookStack\Entities\Repos;
 
+use BookStack\Actions\ActivityType;
 use BookStack\Actions\TagRepo;
-use BookStack\Entities\Book;
-use BookStack\Entities\Entity;
-use BookStack\Entities\HasCoverImage;
+use BookStack\Auth\User;
+use BookStack\Entities\Models\Entity;
+use BookStack\Entities\Models\HasCoverImage;
 use BookStack\Exceptions\ImageUploadException;
+use BookStack\Facades\Activity;
 use BookStack\Uploads\ImageRepo;
 use Illuminate\Http\UploadedFile;
 use Illuminate\Support\Collection;
@@ -18,10 +20,6 @@ class BaseRepo
     protected $imageRepo;
 
 
-    /**
-     * BaseRepo constructor.
-     * @param $tagRepo
-     */
     public function __construct(TagRepo $tagRepo, ImageRepo $imageRepo)
     {
         $this->tagRepo = $tagRepo;
@@ -37,6 +35,7 @@ class BaseRepo
         $entity->forceFill([
             'created_by' => user()->id,
             'updated_by' => user()->id,
+            'owned_by' => user()->id,
         ]);
         $entity->refreshSlug();
         $entity->save();
@@ -91,29 +90,4 @@ class BaseRepo
             $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();
-    }
 }
index 70db0fa65750bde4266c97040939d7a0b55c098a..68d62887b72293b1717468cae9b5e6430acc505d 100644 (file)
@@ -1,14 +1,14 @@
 <?php namespace BookStack\Entities\Repos;
 
+use BookStack\Actions\ActivityType;
 use BookStack\Actions\TagRepo;
-use BookStack\Entities\Book;
-use BookStack\Entities\Managers\TrashCan;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Tools\TrashCan;
 use BookStack\Exceptions\ImageUploadException;
 use BookStack\Exceptions\NotFoundException;
-use BookStack\Exceptions\NotifyException;
+use BookStack\Facades\Activity;
 use BookStack\Uploads\ImageRepo;
 use Exception;
-use Illuminate\Contracts\Container\BindingResolutionException;
 use Illuminate\Contracts\Pagination\LengthAwarePaginator;
 use Illuminate\Http\UploadedFile;
 use Illuminate\Support\Collection;
@@ -22,7 +22,6 @@ class BookRepo
 
     /**
      * BookRepo constructor.
-     * @param $tagRepo
      */
     public function __construct(BaseRepo $baseRepo, TagRepo $tagRepo, ImageRepo $imageRepo)
     {
@@ -91,6 +90,7 @@ class BookRepo
     {
         $book = new Book();
         $this->baseRepo->create($book, $input);
+        Activity::addForEntity($book, ActivityType::BOOK_CREATE);
         return $book;
     }
 
@@ -100,6 +100,7 @@ class BookRepo
     public function update(Book $book, array $input): Book
     {
         $this->baseRepo->update($book, $input);
+        Activity::addForEntity($book, ActivityType::BOOK_UPDATE);
         return $book;
     }
 
@@ -113,22 +114,16 @@ class BookRepo
         $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
+     * @throws Exception
      */
     public function destroy(Book $book)
     {
         $trashCan = new TrashCan();
-        $trashCan->destroyBook($book);
+        $trashCan->softDestroyBook($book);
+        Activity::addForEntity($book, ActivityType::BOOK_DELETE);
+
+        $trashCan->autoClearOld();
     }
 }
index ba687c6f6e754f3a49959ad932294620cb3e74c8..b15241fb388735f666ebe3f86ddc2a1f6eaa6911 100644 (file)
@@ -1,10 +1,12 @@
 <?php namespace BookStack\Entities\Repos;
 
-use BookStack\Entities\Book;
-use BookStack\Entities\Bookshelf;
-use BookStack\Entities\Managers\TrashCan;
+use BookStack\Actions\ActivityType;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Bookshelf;
+use BookStack\Entities\Tools\TrashCan;
 use BookStack\Exceptions\ImageUploadException;
 use BookStack\Exceptions\NotFoundException;
+use BookStack\Facades\Activity;
 use Exception;
 use Illuminate\Contracts\Pagination\LengthAwarePaginator;
 use Illuminate\Http\UploadedFile;
@@ -16,7 +18,6 @@ class BookshelfRepo
 
     /**
      * BookshelfRepo constructor.
-     * @param $baseRepo
      */
     public function __construct(BaseRepo $baseRepo)
     {
@@ -87,11 +88,12 @@ class BookshelfRepo
         $shelf = new Bookshelf();
         $this->baseRepo->create($shelf, $input);
         $this->updateBooks($shelf, $bookIds);
+        Activity::addForEntity($shelf, ActivityType::BOOKSHELF_CREATE);
         return $shelf;
     }
 
     /**
-     * Create a new shelf in the system.
+     * Update an existing shelf in the system using the given input.
      */
     public function update(Bookshelf $shelf, array $input, ?array $bookIds): Bookshelf
     {
@@ -101,6 +103,7 @@ class BookshelfRepo
             $this->updateBooks($shelf, $bookIds);
         }
 
+        Activity::addForEntity($shelf, ActivityType::BOOKSHELF_UPDATE);
         return $shelf;
     }
 
@@ -134,14 +137,6 @@ class BookshelfRepo
         $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.
      */
@@ -174,6 +169,8 @@ class BookshelfRepo
     public function destroy(Bookshelf $shelf)
     {
         $trashCan = new TrashCan();
-        $trashCan->destroyShelf($shelf);
+        $trashCan->softDestroyShelf($shelf);
+        Activity::addForEntity($shelf, ActivityType::BOOKSHELF_DELETE);
+        $trashCan->autoClearOld();
     }
 }
index c6f3a2d2f0fc093c37b6e541081c20c977468c75..d56874e0d54a9b647ce59f4a84add65fa793f302 100644 (file)
@@ -1,15 +1,14 @@
 <?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\Actions\ActivityType;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Tools\BookContents;
+use BookStack\Entities\Tools\TrashCan;
 use BookStack\Exceptions\MoveOperationException;
 use BookStack\Exceptions\NotFoundException;
-use BookStack\Exceptions\NotifyException;
+use BookStack\Facades\Activity;
 use Exception;
-use Illuminate\Contracts\Container\BindingResolutionException;
-use Illuminate\Database\Eloquent\Builder;
 use Illuminate\Support\Collection;
 
 class ChapterRepo
@@ -19,7 +18,6 @@ class ChapterRepo
 
     /**
      * ChapterRepo constructor.
-     * @param $baseRepo
      */
     public function __construct(BaseRepo $baseRepo)
     {
@@ -50,6 +48,7 @@ class ChapterRepo
         $chapter->book_id = $parentBook->id;
         $chapter->priority = (new BookContents($parentBook))->getLastPriority() + 1;
         $this->baseRepo->create($chapter, $input);
+        Activity::addForEntity($chapter, ActivityType::CHAPTER_CREATE);
         return $chapter;
     }
 
@@ -59,17 +58,10 @@ class ChapterRepo
     public function update(Chapter $chapter, array $input): Chapter
     {
         $this->baseRepo->update($chapter, $input);
+        Activity::addForEntity($chapter, ActivityType::CHAPTER_UPDATE);
         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
@@ -77,7 +69,9 @@ class ChapterRepo
     public function destroy(Chapter $chapter)
     {
         $trashCan = new TrashCan();
-        $trashCan->destroyChapter($chapter);
+        $trashCan->softDestroyChapter($chapter);
+        Activity::addForEntity($chapter, ActivityType::CHAPTER_DELETE);
+        $trashCan->autoClearOld();
     }
 
     /**
@@ -96,6 +90,7 @@ class ChapterRepo
             throw new MoveOperationException('Chapters can only be moved into books');
         }
 
+        /** @var Book $parent */
         $parent = Book::visible()->where('id', '=', $entityId)->first();
         if ($parent === null) {
             throw new MoveOperationException('Book to move chapter into not found');
@@ -103,6 +98,8 @@ class ChapterRepo
 
         $chapter->changeBook($parent->id);
         $chapter->rebuildPermissions();
+        Activity::addForEntity($chapter, ActivityType::CHAPTER_MOVE);
+
         return $parent;
     }
 }
index 1467feff5bbfa399545579ab9225dad7cb703746..60ae855109a469ab9e3b94508c0a57c85af0b1e7 100644 (file)
@@ -1,17 +1,19 @@
 <?php namespace BookStack\Entities\Repos;
 
-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 BookStack\Actions\ActivityType;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Models\Entity;
+use BookStack\Entities\Tools\BookContents;
+use BookStack\Entities\Tools\PageContent;
+use BookStack\Entities\Tools\TrashCan;
+use BookStack\Entities\Models\Page;
+use BookStack\Entities\Models\PageRevision;
 use BookStack\Exceptions\MoveOperationException;
 use BookStack\Exceptions\NotFoundException;
-use BookStack\Exceptions\NotifyException;
 use BookStack\Exceptions\PermissionsException;
+use BookStack\Facades\Activity;
+use Exception;
 use Illuminate\Database\Eloquent\Builder;
 use Illuminate\Pagination\LengthAwarePaginator;
 use Illuminate\Support\Collection;
@@ -33,9 +35,9 @@ class PageRepo
      * Get a page by ID.
      * @throws NotFoundException
      */
-    public function getById(int $id): Page
+    public function getById(int $id, array $relations = ['book']): Page
     {
-        $page = Page::visible()->with(['book'])->find($id);
+        $page = Page::visible()->with($relations)->find($id);
 
         if (!$page) {
             throw new NotFoundException(trans('errors.page_not_found'));
@@ -128,6 +130,7 @@ class PageRepo
         $page = (new Page())->forceFill([
             'name' => trans('entities.pages_initial_name'),
             'created_by' => user()->id,
+            'owned_by' => user()->id,
             'updated_by' => user()->id,
             'draft' => true,
         ]);
@@ -150,12 +153,8 @@ class PageRepo
     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');
-        }
+        $this->updateTemplateStatusAndContentFromInput($draft, $input);
 
-        $pageContent = new PageContent($draft);
-        $pageContent->setNewHTML($input['html']);
         $draft->draft = false;
         $draft->revision_count = 1;
         $draft->priority = $this->getNewPriority($draft);
@@ -164,7 +163,10 @@ class PageRepo
 
         $this->savePageRevision($draft, trans('entities.pages_initial_revision'));
         $draft->indexForSearch();
-        return $draft->refresh();
+        $draft->refresh();
+
+        Activity::addForEntity($draft, ActivityType::PAGE_CREATE);
+        return $draft;
     }
 
     /**
@@ -176,12 +178,7 @@ class PageRepo
         $oldHtml = $page->html;
         $oldName = $page->name;
 
-        if (isset($input['template']) && userCan('templates-manage')) {
-            $page->template = ($input['template'] === 'true');
-        }
-
-        $pageContent = new PageContent($page);
-        $pageContent->setNewHTML($input['html']);
+        $this->updateTemplateStatusAndContentFromInput($page, $input);
         $this->baseRepo->update($page, $input);
 
         // Update with new details
@@ -202,9 +199,24 @@ class PageRepo
             $this->savePageRevision($page, $summary);
         }
 
+        Activity::addForEntity($page, ActivityType::PAGE_UPDATE);
         return $page;
     }
 
+    protected function updateTemplateStatusAndContentFromInput(Page $page, array $input)
+    {
+        if (isset($input['template']) && userCan('templates-manage')) {
+            $page->template = ($input['template'] === 'true');
+        }
+
+        $pageContent = new PageContent($page);
+        if (isset($input['html'])) {
+            $pageContent->setNewHTML($input['html']);
+        } else {
+            $pageContent->setNewMarkdown($input['markdown']);
+        }
+    }
+
     /**
      * Saves a page revision into the system.
      */
@@ -237,11 +249,10 @@ class PageRepo
     {
         // 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']);
+                (new PageContent($page))->setNewHTML($input['html']);
             }
+            $page->fill($input);
             $page->save();
             return $page;
         }
@@ -259,12 +270,14 @@ class PageRepo
 
     /**
      * Destroy a page from the system.
-     * @throws NotifyException
+     * @throws Exception
      */
     public function destroy(Page $page)
     {
         $trashCan = new TrashCan();
-        $trashCan->destroyPage($page);
+        $trashCan->softDestroyPage($page);
+        Activity::addForEntity($page, ActivityType::PAGE_DELETE);
+        $trashCan->autoClearOld();
     }
 
     /**
@@ -285,6 +298,7 @@ class PageRepo
         $page->save();
 
         $page->indexForSearch();
+        Activity::addForEntity($page, ActivityType::PAGE_RESTORE);
         return $page;
     }
 
@@ -295,7 +309,7 @@ class PageRepo
      * @throws MoveOperationException
      * @throws PermissionsException
      */
-    public function move(Page $page, string $parentIdentifier): Book
+    public function move(Page $page, string $parentIdentifier): Entity
     {
         $parent = $this->findParentByIdentifier($parentIdentifier);
         if ($parent === null) {
@@ -310,7 +324,8 @@ class PageRepo
         $page->changeBook($parent instanceof Book ? $parent->id : $parent->book->id);
         $page->rebuildPermissions();
 
-        return ($parent instanceof Book ? $parent : $parent->book);
+        Activity::addForEntity($page, ActivityType::PAGE_MOVE);
+        return $parent;
     }
 
     /**
@@ -321,7 +336,7 @@ class PageRepo
      */
     public function copy(Page $page, string $parentIdentifier = null, string $newName = null): Page
     {
-        $parent = $parentIdentifier ? $this->findParentByIdentifier($parentIdentifier) : $page->parent();
+        $parent = $parentIdentifier ? $this->findParentByIdentifier($parentIdentifier) : $page->getParent();
         if ($parent === null) {
             throw new MoveOperationException('Book or chapter to move page into not found');
         }
@@ -369,14 +384,6 @@ class PageRepo
         return $parentClass::visible()->where('id', '=', $entityId)->first();
     }
 
-    /**
-     * Update the permissions of a page.
-     */
-    public function updatePermissions(Page $page, bool $restricted, Collection $permissions = null)
-    {
-        $this->baseRepo->updatePermissions($page, $restricted, $permissions);
-    }
-
     /**
      * Change the page's parent to the given entity.
      */
@@ -440,8 +447,9 @@ class PageRepo
      */
     protected function getNewPriority(Page $page): int
     {
-        if ($page->parent() instanceof Chapter) {
-            $lastPage = $page->parent()->pages('desc')->first();
+        $parent = $page->getParent();
+        if ($parent instanceof Chapter) {
+            $lastPage = $parent->pages('desc')->first();
             return $lastPage ? $lastPage->priority + 1 : 0;
         }
 
similarity index 92%
rename from app/Entities/Managers/BookContents.php
rename to app/Entities/Tools/BookContents.php
index 52447e43bf0b397d4c69782fed8453793e3a13bf..71c8f8393a22dff67f95a4ec931ce236d4641b49 100644 (file)
@@ -1,10 +1,10 @@
-<?php namespace BookStack\Entities\Managers;
+<?php namespace BookStack\Entities\Tools;
 
-use BookStack\Entities\Book;
-use BookStack\Entities\BookChild;
-use BookStack\Entities\Chapter;
-use BookStack\Entities\Entity;
-use BookStack\Entities\Page;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\BookChild;
+use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Models\Entity;
+use BookStack\Entities\Models\Page;
 use BookStack\Exceptions\SortOperationException;
 use Illuminate\Support\Collection;
 
@@ -18,7 +18,6 @@ class BookContents
 
     /**
      * BookContents constructor.
-     * @param $book
      */
     public function __construct(Book $book)
     {
@@ -53,12 +52,16 @@ class BookContents
         $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()));
+                $chapter->setAttribute('visible_pages', collect($pages)->sortBy($this->bookChildSortFunc()));
             } else {
                 $lonePages = $lonePages->concat($pages);
             }
         });
 
+        $chapters->whereNull('visible_pages')->each(function (Chapter $chapter) {
+            $chapter->setAttribute('visible_pages', collect([]));
+        });
+
         $all->each(function (Entity $entity) use ($renderPages) {
             $entity->setRelation('book', $this->book);
 
similarity index 94%
rename from app/Entities/ExportService.php
rename to app/Entities/Tools/ExportFormatter.php
index f945dfbe4afe1d17246ca7119c6e776c4acf0455..eb8f6862f23fe76b703c8f0a2514b2f5d61acae9 100644 (file)
@@ -1,14 +1,15 @@
-<?php namespace BookStack\Entities;
+<?php namespace BookStack\Entities\Tools;
 
-use BookStack\Entities\Managers\BookContents;
-use BookStack\Entities\Managers\PageContent;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Models\Page;
 use BookStack\Uploads\ImageService;
 use DomPDF;
 use Exception;
 use SnappyPDF;
 use Throwable;
 
-class ExportService
+class ExportFormatter
 {
 
     protected $imageService;
@@ -142,7 +143,7 @@ class ExportService
     protected function containHtml(string $htmlContent): string
     {
         $imageTagsOutput = [];
-        preg_match_all("/\<img.*src\=(\'|\")(.*?)(\'|\").*?\>/i", $htmlContent, $imageTagsOutput);
+        preg_match_all("/\<img.*?src\=(\'|\")(.*?)(\'|\").*?\>/i", $htmlContent, $imageTagsOutput);
 
         // Replace image src with base64 encoded image strings
         if (isset($imageTagsOutput[0]) && count($imageTagsOutput[0]) > 0) {
@@ -203,7 +204,7 @@ class ExportService
     {
         $text = $chapter->name . "\n\n";
         $text .= $chapter->description . "\n\n";
-        foreach ($chapter->pages as $page) {
+        foreach ($chapter->getVisiblePages() as $page) {
             $text .= $this->pageToPlainText($page);
         }
         return $text;
@@ -214,7 +215,7 @@ class ExportService
      */
     public function bookToPlainText(Book $book): string
     {
-        $bookTree = (new BookContents($book))->getTree(false, true);
+        $bookTree = (new BookContents($book))->getTree(false, false);
         $text = $book->name . "\n\n";
         foreach ($bookTree as $bookChild) {
             if ($bookChild->isA('chapter')) {
similarity index 93%
rename from app/Entities/Managers/PageContent.php
rename to app/Entities/Tools/PageContent.php
index 7338a36b393631289a8f30ce09888d403d52763e..f60971b8bbe02b4539419fe1103426ef1017fda7 100644 (file)
@@ -1,9 +1,10 @@
-<?php namespace BookStack\Entities\Managers;
+<?php namespace BookStack\Entities\Tools;
 
-use BookStack\Entities\Page;
+use BookStack\Entities\Models\Page;
 use DOMDocument;
 use DOMNodeList;
 use DOMXPath;
+use League\CommonMark\CommonMarkConverter;
 
 class PageContent
 {
@@ -25,6 +26,27 @@ class PageContent
     {
         $this->page->html = $this->formatHtml($html);
         $this->page->text = $this->toPlainText();
+        $this->page->markdown = '';
+    }
+
+    /**
+     * Update the content of the page with new provided Markdown content.
+     */
+    public function setNewMarkdown(string $markdown)
+    {
+        $this->page->markdown = $markdown;
+        $html = $this->markdownToHtml($markdown);
+        $this->page->html = $this->formatHtml($html);
+        $this->page->text = $this->toPlainText();
+    }
+
+    /**
+     * Convert the given Markdown content to a HTML string.
+     */
+    protected function markdownToHtml(string $markdown): string
+    {
+        $converter = new CommonMarkConverter();
+        return $converter->convertToHtml($markdown);
     }
 
     /**
similarity index 95%
rename from app/Entities/Managers/PageEditActivity.php
rename to app/Entities/Tools/PageEditActivity.php
index cebbf8720f12a0ac7d66b8cc9e67dae3a5719db7..79de5c827987e8d1f9a4041d70d2598839c2c87c 100644 (file)
@@ -1,7 +1,7 @@
-<?php namespace BookStack\Entities\Managers;
+<?php namespace BookStack\Entities\Tools;
 
-use BookStack\Entities\Page;
-use BookStack\Entities\PageRevision;
+use BookStack\Entities\Models\Page;
+use BookStack\Entities\Models\PageRevision;
 use Carbon\Carbon;
 use Illuminate\Database\Eloquent\Builder;
 
diff --git a/app/Entities/Tools/PermissionsUpdater.php b/app/Entities/Tools/PermissionsUpdater.php
new file mode 100644 (file)
index 0000000..8a27ce7
--- /dev/null
@@ -0,0 +1,68 @@
+<?php namespace BookStack\Entities\Tools;
+
+use BookStack\Actions\ActivityType;
+use BookStack\Auth\User;
+use BookStack\Entities\Models\Entity;
+use BookStack\Facades\Activity;
+use Illuminate\Http\Request;
+use Illuminate\Support\Collection;
+
+class PermissionsUpdater
+{
+
+    /**
+     * Update an entities permissions from a permission form submit request.
+     */
+    public function updateFromPermissionsForm(Entity $entity, Request $request)
+    {
+        $restricted = $request->get('restricted') === 'true';
+        $permissions = $request->get('restrictions', null);
+        $ownerId = $request->get('owned_by', null);
+
+        $entity->restricted = $restricted;
+        $entity->permissions()->delete();
+
+        if (!is_null($permissions)) {
+            $entityPermissionData = $this->formatPermissionsFromRequestToEntityPermissions($permissions);
+            $entity->permissions()->createMany($entityPermissionData);
+        }
+
+        if (!is_null($ownerId)) {
+            $this->updateOwnerFromId($entity, intval($ownerId));
+        }
+
+        $entity->save();
+        $entity->rebuildPermissions();
+
+        Activity::addForEntity($entity, ActivityType::PERMISSIONS_UPDATE);
+    }
+
+    /**
+     * Update the owner of the given entity.
+     * Checks the user exists in the system first.
+     * Does not save the model, just updates it.
+     */
+    protected function updateOwnerFromId(Entity $entity, int $newOwnerId)
+    {
+        $newOwner = User::query()->find($newOwnerId);
+        if (!is_null($newOwner)) {
+            $entity->owned_by = $newOwner->id;
+        }
+    }
+
+    /**
+     * Format permissions provided from a permission form to be
+     * EntityPermission data.
+     */
+    protected function formatPermissionsFromRequestToEntityPermissions(array $permissions): Collection
+    {
+        return collect($permissions)->flatMap(function ($restrictions, $roleId) {
+            return collect($restrictions)->keys()->map(function ($action) use ($roleId) {
+                return [
+                    'role_id' => $roleId,
+                    'action' => strtolower($action),
+                ] ;
+            });
+        });
+    }
+}
diff --git a/app/Entities/Tools/SearchIndex.php b/app/Entities/Tools/SearchIndex.php
new file mode 100644 (file)
index 0000000..81a5022
--- /dev/null
@@ -0,0 +1,120 @@
+<?php namespace BookStack\Entities\Tools;
+
+use BookStack\Entities\EntityProvider;
+use BookStack\Entities\Models\Entity;
+use BookStack\Entities\Models\SearchTerm;
+use Illuminate\Support\Collection;
+
+class SearchIndex
+{
+    /**
+     * @var SearchTerm
+     */
+    protected $searchTerm;
+
+    /**
+     * @var EntityProvider
+     */
+    protected $entityProvider;
+
+
+    public function __construct(SearchTerm $searchTerm, EntityProvider $entityProvider)
+    {
+        $this->searchTerm = $searchTerm;
+        $this->entityProvider = $entityProvider;
+    }
+
+
+    /**
+     * Index the given entity.
+     */
+    public function indexEntity(Entity $entity)
+    {
+        $this->deleteEntityTerms($entity);
+        $nameTerms = $this->generateTermArrayFromText($entity->name, 5 * $entity->searchFactor);
+        $bodyTerms = $this->generateTermArrayFromText($entity->getText(), 1 * $entity->searchFactor);
+        $terms = array_merge($nameTerms, $bodyTerms);
+        foreach ($terms as $index => $term) {
+            $terms[$index]['entity_type'] = $entity->getMorphClass();
+            $terms[$index]['entity_id'] = $entity->id;
+        }
+        $this->searchTerm->newQuery()->insert($terms);
+    }
+
+    /**
+     * Index multiple Entities at once
+     * @param Entity[] $entities
+     */
+    protected function indexEntities(array $entities)
+    {
+        $terms = [];
+        foreach ($entities as $entity) {
+            $nameTerms = $this->generateTermArrayFromText($entity->name, 5 * $entity->searchFactor);
+            $bodyTerms = $this->generateTermArrayFromText($entity->getText(), 1 * $entity->searchFactor);
+            foreach (array_merge($nameTerms, $bodyTerms) as $term) {
+                $term['entity_id'] = $entity->id;
+                $term['entity_type'] = $entity->getMorphClass();
+                $terms[] = $term;
+            }
+        }
+
+        $chunkedTerms = array_chunk($terms, 500);
+        foreach ($chunkedTerms as $termChunk) {
+            $this->searchTerm->newQuery()->insert($termChunk);
+        }
+    }
+
+    /**
+     * Delete and re-index the terms for all entities in the system.
+     */
+    public function indexAllEntities()
+    {
+        $this->searchTerm->newQuery()->truncate();
+
+        foreach ($this->entityProvider->all() as $entityModel) {
+            $selectFields = ['id', 'name', $entityModel->textField];
+            $entityModel->newQuery()
+                ->withTrashed()
+                ->select($selectFields)
+                ->chunk(1000, function (Collection $entities) {
+                    $this->indexEntities($entities->all());
+                });
+        }
+    }
+
+    /**
+     * Delete related Entity search terms.
+     */
+    public function deleteEntityTerms(Entity $entity)
+    {
+        $entity->searchTerms()->delete();
+    }
+
+    /**
+     * Create a scored term array from the given text.
+     */
+    protected function generateTermArrayFromText(string $text, int $scoreAdjustment = 1): array
+    {
+        $tokenMap = []; // {TextToken => OccurrenceCount}
+        $splitChars = " \n\t.,!?:;()[]{}<>`'\"";
+        $token = strtok($text, $splitChars);
+
+        while ($token !== false) {
+            if (!isset($tokenMap[$token])) {
+                $tokenMap[$token] = 0;
+            }
+            $tokenMap[$token]++;
+            $token = strtok($splitChars);
+        }
+
+        $terms = [];
+        foreach ($tokenMap as $token => $count) {
+            $terms[] = [
+                'term' => $token,
+                'score' => $count * $scoreAdjustment
+            ];
+        }
+
+        return $terms;
+    }
+}
similarity index 98%
rename from app/Entities/SearchOptions.php
rename to app/Entities/Tools/SearchOptions.php
index a121bd7939cbc6eb4e6ccd18e96e21dcf1e7f992..60e3a9b7876d971edd29c681643ec58a0fbab09d 100644 (file)
@@ -1,4 +1,4 @@
-<?php namespace BookStack\Entities;
+<?php namespace BookStack\Entities\Tools;
 
 use Illuminate\Http\Request;
 
similarity index 72%
rename from app/Entities/SearchService.php
rename to app/Entities/Tools/SearchRunner.php
index 11b731cd0153591e2cfd7b6b71f88504f088cd92..acfe8d9565fdf1ea2884d337e31e0402270f6cb9 100644 (file)
@@ -1,6 +1,8 @@
-<?php namespace BookStack\Entities;
+<?php namespace BookStack\Entities\Tools;
 
 use BookStack\Auth\Permissions\PermissionService;
+use BookStack\Entities\EntityProvider;
+use BookStack\Entities\Models\Entity;
 use Illuminate\Database\Connection;
 use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
 use Illuminate\Database\Query\Builder;
@@ -8,12 +10,8 @@ use Illuminate\Database\Query\JoinClause;
 use Illuminate\Support\Collection;
 use Illuminate\Support\Str;
 
-class SearchService
+class SearchRunner
 {
-    /**
-     * @var SearchTerm
-     */
-    protected $searchTerm;
 
     /**
      * @var EntityProvider
@@ -37,25 +35,14 @@ class SearchService
      */
     protected $queryOperators = ['<=', '>=', '=', '<', '>', 'like', '!='];
 
-    /**
-     * SearchService constructor.
-     */
-    public function __construct(SearchTerm $searchTerm, EntityProvider $entityProvider, Connection $db, PermissionService $permissionService)
+
+    public function __construct(EntityProvider $entityProvider, Connection $db, PermissionService $permissionService)
     {
-        $this->searchTerm = $searchTerm;
         $this->entityProvider = $entityProvider;
         $this->db = $db;
         $this->permissionService = $permissionService;
     }
 
-    /**
-     * Set the database connection
-     */
-    public function setConnection(Connection $connection)
-    {
-        $this->db = $connection;
-    }
-
     /**
      * Search all entities in the system.
      * The provided count is for each entity to search,
@@ -115,11 +102,12 @@ class SearchService
             $search = $this->buildEntitySearchQuery($opts, $entityType)->where('book_id', '=', $bookId)->take(20)->get();
             $results = $results->merge($search);
         }
+
         return $results->sortByDesc('score')->take(20);
     }
 
     /**
-     * Search a book for entities
+     * Search a chapter for entities
      */
     public function searchChapter(int $chapterId, string $searchString): Collection
     {
@@ -134,7 +122,7 @@ class SearchService
      * matching instead of the items themselves.
      * @return \Illuminate\Database\Eloquent\Collection|int|static[]
      */
-    public function searchEntityTable(SearchOptions $searchOpts, string $entityType = 'page', int $page = 1, int $count = 20, string $action = 'view', bool $getCount = false)
+    protected function searchEntityTable(SearchOptions $searchOpts, string $entityType = 'page', int $page = 1, int $count = 20, string $action = 'view', bool $getCount = false)
     {
         $query = $this->buildEntitySearchQuery($searchOpts, $entityType, $action);
         if ($getCount) {
@@ -155,28 +143,25 @@ class SearchService
 
         // Handle normal search terms
         if (count($searchOpts->searches) > 0) {
-            $subQuery = $this->db->table('search_terms')->select('entity_id', 'entity_type', \DB::raw('SUM(score) as score'));
+            $rawScoreSum = $this->db->raw('SUM(score) as score');
+            $subQuery = $this->db->table('search_terms')->select('entity_id', 'entity_type', $rawScoreSum);
             $subQuery->where('entity_type', '=', $entity->getMorphClass());
             $subQuery->where(function (Builder $query) use ($searchOpts) {
                 foreach ($searchOpts->searches as $inputTerm) {
                     $query->orWhere('term', 'like', $inputTerm .'%');
                 }
             })->groupBy('entity_type', 'entity_id');
-            $entitySelect->join(\DB::raw('(' . $subQuery->toSql() . ') as s'), function (JoinClause $join) {
+            $entitySelect->join($this->db->raw('(' . $subQuery->toSql() . ') as s'), function (JoinClause $join) {
                 $join->on('id', '=', 'entity_id');
             })->selectRaw($entity->getTable().'.*, s.score')->orderBy('score', 'desc');
             $entitySelect->mergeBindings($subQuery);
         }
 
         // Handle exact term matching
-        if (count($searchOpts->exacts) > 0) {
-            $entitySelect->where(function (EloquentBuilder $query) use ($searchOpts, $entity) {
-                foreach ($searchOpts->exacts as $inputTerm) {
-                    $query->where(function (EloquentBuilder $query) use ($inputTerm, $entity) {
-                        $query->where('name', 'like', '%'.$inputTerm .'%')
-                            ->orWhere($entity->textField, 'like', '%'.$inputTerm .'%');
-                    });
-                }
+        foreach ($searchOpts->exacts as $inputTerm) {
+            $entitySelect->where(function (EloquentBuilder $query) use ($inputTerm, $entity) {
+                $query->where('name', 'like', '%'.$inputTerm .'%')
+                    ->orWhere($entity->textField, 'like', '%'.$inputTerm .'%');
             });
         }
 
@@ -239,102 +224,6 @@ class SearchService
         return $query;
     }
 
-    /**
-     * Index the given entity.
-     */
-    public function indexEntity(Entity $entity)
-    {
-        $this->deleteEntityTerms($entity);
-        $nameTerms = $this->generateTermArrayFromText($entity->name, 5 * $entity->searchFactor);
-        $bodyTerms = $this->generateTermArrayFromText($entity->getText(), 1 * $entity->searchFactor);
-        $terms = array_merge($nameTerms, $bodyTerms);
-        foreach ($terms as $index => $term) {
-            $terms[$index]['entity_type'] = $entity->getMorphClass();
-            $terms[$index]['entity_id'] = $entity->id;
-        }
-        $this->searchTerm->newQuery()->insert($terms);
-    }
-
-    /**
-     * Index multiple Entities at once
-     * @param \BookStack\Entities\Entity[] $entities
-     */
-    protected function indexEntities($entities)
-    {
-        $terms = [];
-        foreach ($entities as $entity) {
-            $nameTerms = $this->generateTermArrayFromText($entity->name, 5 * $entity->searchFactor);
-            $bodyTerms = $this->generateTermArrayFromText($entity->getText(), 1 * $entity->searchFactor);
-            foreach (array_merge($nameTerms, $bodyTerms) as $term) {
-                $term['entity_id'] = $entity->id;
-                $term['entity_type'] = $entity->getMorphClass();
-                $terms[] = $term;
-            }
-        }
-
-        $chunkedTerms = array_chunk($terms, 500);
-        foreach ($chunkedTerms as $termChunk) {
-            $this->searchTerm->newQuery()->insert($termChunk);
-        }
-    }
-
-    /**
-     * Delete and re-index the terms for all entities in the system.
-     */
-    public function indexAllEntities()
-    {
-        $this->searchTerm->truncate();
-
-        foreach ($this->entityProvider->all() as $entityModel) {
-            $selectFields = ['id', 'name', $entityModel->textField];
-            $entityModel->newQuery()->select($selectFields)->chunk(1000, function ($entities) {
-                $this->indexEntities($entities);
-            });
-        }
-    }
-
-    /**
-     * Delete related Entity search terms.
-     * @param Entity $entity
-     */
-    public function deleteEntityTerms(Entity $entity)
-    {
-        $entity->searchTerms()->delete();
-    }
-
-    /**
-     * Create a scored term array from the given text.
-     * @param $text
-     * @param float|int $scoreAdjustment
-     * @return array
-     */
-    protected function generateTermArrayFromText($text, $scoreAdjustment = 1)
-    {
-        $tokenMap = []; // {TextToken => OccurrenceCount}
-        $splitChars = " \n\t.,!?:;()[]{}<>`'\"";
-        $token = strtok($text, $splitChars);
-
-        while ($token !== false) {
-            if (!isset($tokenMap[$token])) {
-                $tokenMap[$token] = 0;
-            }
-            $tokenMap[$token]++;
-            $token = strtok($splitChars);
-        }
-
-        $terms = [];
-        foreach ($tokenMap as $token => $count) {
-            $terms[] = [
-                'term' => $token,
-                'score' => $count * $scoreAdjustment
-            ];
-        }
-        return $terms;
-    }
-
-
-
-
     /**
      * Custom entity search filters
      */
similarity index 55%
rename from app/Entities/Managers/EntityContext.php
rename to app/Entities/Tools/ShelfContext.php
index 551cd1a100c142f26cea88d8e86aaa282c768fa0..f3849bbb4741e8c075c122bca3274f242f6d0e28 100644 (file)
@@ -1,29 +1,18 @@
-<?php namespace BookStack\Entities\Managers;
+<?php namespace BookStack\Entities\Tools;
 
-use BookStack\Entities\Book;
-use BookStack\Entities\Bookshelf;
-use Illuminate\Session\Store;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Bookshelf;
 
-class EntityContext
+class ShelfContext
 {
-    protected $session;
-
     protected $KEY_SHELF_CONTEXT_ID = 'context_bookshelf_id';
 
-    /**
-     * EntityContextManager constructor.
-     */
-    public function __construct(Store $session)
-    {
-        $this->session = $session;
-    }
-
     /**
      * Get the current bookshelf context for the given book.
      */
     public function getContextualShelfForBook(Book $book): ?Bookshelf
     {
-        $contextBookshelfId = $this->session->get($this->KEY_SHELF_CONTEXT_ID, null);
+        $contextBookshelfId = session()->get($this->KEY_SHELF_CONTEXT_ID, null);
 
         if (!is_int($contextBookshelfId)) {
             return null;
@@ -37,11 +26,10 @@ class EntityContext
 
     /**
      * Store the current contextual shelf ID.
-     * @param int $shelfId
      */
     public function setShelfContext(int $shelfId)
     {
-        $this->session->put($this->KEY_SHELF_CONTEXT_ID, $shelfId);
+        session()->put($this->KEY_SHELF_CONTEXT_ID, $shelfId);
     }
 
     /**
@@ -49,6 +37,6 @@ class EntityContext
      */
     public function clearShelfContext()
     {
-        $this->session->forget($this->KEY_SHELF_CONTEXT_ID);
+        session()->forget($this->KEY_SHELF_CONTEXT_ID);
     }
 }
diff --git a/app/Entities/Tools/SiblingFetcher.php b/app/Entities/Tools/SiblingFetcher.php
new file mode 100644 (file)
index 0000000..6964fa2
--- /dev/null
@@ -0,0 +1,47 @@
+<?php namespace BookStack\Entities\Tools;
+
+use BookStack\Entities\EntityProvider;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Bookshelf;
+use Illuminate\Support\Collection;
+
+class SiblingFetcher
+{
+
+    /**
+     * Search among the siblings of the entity of given type and id.
+     */
+    public function fetch(string $entityType, int $entityId): Collection
+    {
+        $entity = (new EntityProvider)->get($entityType)->visible()->findOrFail($entityId);
+        $entities = [];
+
+        // Page in chapter
+        if ($entity->isA('page') && $entity->chapter) {
+            $entities = $entity->chapter->getVisiblePages();
+        }
+
+        // Page in book or chapter
+        if (($entity->isA('page') && !$entity->chapter) || $entity->isA('chapter')) {
+            $entities = $entity->book->getDirectChildren();
+        }
+
+        // Book
+        // Gets just the books in a shelf if shelf is in context
+        if ($entity->isA('book')) {
+            $contextShelf = (new ShelfContext)->getContextualShelfForBook($entity);
+            if ($contextShelf) {
+                $entities = $contextShelf->visibleBooks()->get();
+            } else {
+                $entities = Book::visible()->get();
+            }
+        }
+
+        // Shelve
+        if ($entity->isA('bookshelf')) {
+            $entities = Bookshelf::visible()->get();
+        }
+
+        return $entities;
+    }
+}
similarity index 52%
rename from app/Entities/SlugGenerator.php
rename to app/Entities/Tools/SlugGenerator.php
index e8bc556abefa102153b64953a6dc76c89784e206..7075bc72c14ce50e78119592be480f70c6756266 100644 (file)
@@ -1,29 +1,19 @@
-<?php namespace BookStack\Entities;
+<?php namespace BookStack\Entities\Tools;
 
+use BookStack\Entities\Models\Entity;
 use Illuminate\Support\Str;
 
 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
+    public function generate(Entity $entity): string
     {
-        $slug = $this->formatNameAsSlug($this->entity->name);
-        while ($this->slugInUse($slug)) {
+        $slug = $this->formatNameAsSlug($entity->name);
+        while ($this->slugInUse($slug, $entity)) {
             $slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
         }
         return $slug;
@@ -45,16 +35,16 @@ class SlugGenerator
      * Check if a slug is already in-use for this
      * type of model within the same parent.
      */
-    protected function slugInUse(string $slug): bool
+    protected function slugInUse(string $slug, Entity $entity): bool
     {
-        $query = $this->entity->newQuery()->where('slug', '=', $slug);
+        $query = $entity->newQuery()->where('slug', '=', $slug);
 
-        if ($this->entity instanceof BookChild) {
-            $query->where('book_id', '=', $this->entity->book_id);
+        if ($entity instanceof BookChild) {
+            $query->where('book_id', '=', $entity->book_id);
         }
 
-        if ($this->entity->id) {
-            $query->where('id', '!=', $this->entity->id);
+        if ($entity->id) {
+            $query->where('id', '!=', $entity->id);
         }
 
         return $query->count() > 0;
diff --git a/app/Entities/Tools/TrashCan.php b/app/Entities/Tools/TrashCan.php
new file mode 100644 (file)
index 0000000..d2447ec
--- /dev/null
@@ -0,0 +1,325 @@
+<?php namespace BookStack\Entities\Tools;
+
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Bookshelf;
+use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Models\Deletion;
+use BookStack\Entities\Models\Entity;
+use BookStack\Entities\EntityProvider;
+use BookStack\Entities\Models\HasCoverImage;
+use BookStack\Entities\Models\Page;
+use BookStack\Exceptions\NotifyException;
+use BookStack\Facades\Activity;
+use BookStack\Uploads\AttachmentService;
+use BookStack\Uploads\ImageService;
+use Exception;
+use Illuminate\Support\Carbon;
+
+class TrashCan
+{
+
+    /**
+     * Send a shelf to the recycle bin.
+     */
+    public function softDestroyShelf(Bookshelf $shelf)
+    {
+        Deletion::createForEntity($shelf);
+        $shelf->delete();
+    }
+
+    /**
+     * Send a book to the recycle bin.
+     * @throws Exception
+     */
+    public function softDestroyBook(Book $book)
+    {
+        Deletion::createForEntity($book);
+
+        foreach ($book->pages as $page) {
+            $this->softDestroyPage($page, false);
+        }
+
+        foreach ($book->chapters as $chapter) {
+            $this->softDestroyChapter($chapter, false);
+        }
+
+        $book->delete();
+    }
+
+    /**
+     * Send a chapter to the recycle bin.
+     * @throws Exception
+     */
+    public function softDestroyChapter(Chapter $chapter, bool $recordDelete = true)
+    {
+        if ($recordDelete) {
+            Deletion::createForEntity($chapter);
+        }
+
+        if (count($chapter->pages) > 0) {
+            foreach ($chapter->pages as $page) {
+                $this->softDestroyPage($page, false);
+            }
+        }
+
+        $chapter->delete();
+    }
+
+    /**
+     * Send a page to the recycle bin.
+     * @throws Exception
+     */
+    public function softDestroyPage(Page $page, bool $recordDelete = true)
+    {
+        if ($recordDelete) {
+            Deletion::createForEntity($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');
+        }
+
+        $page->delete();
+    }
+
+    /**
+     * Remove a bookshelf from the system.
+     * @throws Exception
+     */
+    protected function destroyShelf(Bookshelf $shelf): int
+    {
+        $this->destroyCommonRelations($shelf);
+        $shelf->forceDelete();
+        return 1;
+    }
+
+    /**
+     * Remove a book from the system.
+     * Destroys any child chapters and pages.
+     * @throws Exception
+     */
+    protected function destroyBook(Book $book): int
+    {
+        $count = 0;
+        $pages = $book->pages()->withTrashed()->get();
+        foreach ($pages as $page) {
+            $this->destroyPage($page);
+            $count++;
+        }
+
+        $chapters = $book->chapters()->withTrashed()->get();
+        foreach ($chapters as $chapter) {
+            $this->destroyChapter($chapter);
+            $count++;
+        }
+
+        $this->destroyCommonRelations($book);
+        $book->forceDelete();
+        return $count + 1;
+    }
+
+    /**
+     * Remove a chapter from the system.
+     * Destroys all pages within.
+     * @throws Exception
+     */
+    protected function destroyChapter(Chapter $chapter): int
+    {
+        $count = 0;
+        $pages = $chapter->pages()->withTrashed()->get();
+        if (count($pages)) {
+            foreach ($pages as $page) {
+                $this->destroyPage($page);
+                $count++;
+            }
+        }
+
+        $this->destroyCommonRelations($chapter);
+        $chapter->forceDelete();
+        return $count + 1;
+    }
+
+    /**
+     * Remove a page from the system.
+     * @throws Exception
+     */
+    protected function destroyPage(Page $page): int
+    {
+        $this->destroyCommonRelations($page);
+
+        // Delete Attached Files
+        $attachmentService = app(AttachmentService::class);
+        foreach ($page->attachments as $attachment) {
+            $attachmentService->deleteFile($attachment);
+        }
+
+        $page->forceDelete();
+        return 1;
+    }
+
+    /**
+     * Get the total counts of those that have been trashed
+     * but not yet fully deleted (In recycle bin).
+     */
+    public function getTrashedCounts(): array
+    {
+        $counts = [];
+
+        /** @var Entity $instance */
+        foreach ((new EntityProvider)->all() as $key => $instance) {
+            $counts[$key] = $instance->newQuery()->onlyTrashed()->count();
+        }
+
+        return $counts;
+    }
+
+    /**
+     * Destroy all items that have pending deletions.
+     * @throws Exception
+     */
+    public function empty(): int
+    {
+        $deletions = Deletion::all();
+        $deleteCount = 0;
+        foreach ($deletions as $deletion) {
+            $deleteCount += $this->destroyFromDeletion($deletion);
+        }
+        return $deleteCount;
+    }
+
+    /**
+     * Destroy an element from the given deletion model.
+     * @throws Exception
+     */
+    public function destroyFromDeletion(Deletion $deletion): int
+    {
+        // We directly load the deletable element here just to ensure it still
+        // exists in the event it has already been destroyed during this request.
+        $entity = $deletion->deletable()->first();
+        $count = 0;
+        if ($entity) {
+            $count = $this->destroyEntity($deletion->deletable);
+        }
+        $deletion->delete();
+        return $count;
+    }
+
+    /**
+     * Restore the content within the given deletion.
+     * @throws Exception
+     */
+    public function restoreFromDeletion(Deletion $deletion): int
+    {
+        $shouldRestore = true;
+        $restoreCount = 0;
+        $parent = $deletion->deletable->getParent();
+
+        if ($parent && $parent->trashed()) {
+            $shouldRestore = false;
+        }
+
+        if ($shouldRestore) {
+            $restoreCount = $this->restoreEntity($deletion->deletable);
+        }
+
+        $deletion->delete();
+        return $restoreCount;
+    }
+
+    /**
+     * Automatically clear old content from the recycle bin
+     * depending on the configured lifetime.
+     * Returns the total number of deleted elements.
+     * @throws Exception
+     */
+    public function autoClearOld(): int
+    {
+        $lifetime = intval(config('app.recycle_bin_lifetime'));
+        if ($lifetime < 0) {
+            return 0;
+        }
+
+        $clearBeforeDate = Carbon::now()->addSeconds(10)->subDays($lifetime);
+        $deleteCount = 0;
+
+        $deletionsToRemove = Deletion::query()->where('created_at', '<', $clearBeforeDate)->get();
+        foreach ($deletionsToRemove as $deletion) {
+            $deleteCount += $this->destroyFromDeletion($deletion);
+        }
+
+        return $deleteCount;
+    }
+
+    /**
+     * Restore an entity so it is essentially un-deleted.
+     * Deletions on restored child elements will be removed during this restoration.
+     */
+    protected function restoreEntity(Entity $entity): int
+    {
+        $count = 1;
+        $entity->restore();
+
+        $restoreAction = function ($entity) use (&$count) {
+            if ($entity->deletions_count > 0) {
+                $entity->deletions()->delete();
+            }
+
+            $entity->restore();
+            $count++;
+        };
+
+        if ($entity->isA('chapter') || $entity->isA('book')) {
+            $entity->pages()->withTrashed()->withCount('deletions')->get()->each($restoreAction);
+        }
+
+        if ($entity->isA('book')) {
+            $entity->chapters()->withTrashed()->withCount('deletions')->get()->each($restoreAction);
+        }
+
+        return $count;
+    }
+
+    /**
+     * Destroy the given entity.
+     */
+    protected function destroyEntity(Entity $entity): int
+    {
+        if ($entity->isA('page')) {
+            return $this->destroyPage($entity);
+        }
+        if ($entity->isA('chapter')) {
+            return $this->destroyChapter($entity);
+        }
+        if ($entity->isA('book')) {
+            return $this->destroyBook($entity);
+        }
+        if ($entity->isA('shelf')) {
+            return $this->destroyShelf($entity);
+        }
+    }
+
+    /**
+     * 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();
+        $entity->deletions()->delete();
+
+        if ($entity instanceof HasCoverImage && $entity->cover) {
+            $imageService = app()->make(ImageService::class);
+            $imageService->destroy($entity->cover);
+        }
+    }
+}
index 65a5bb99f6ca3bfc6f99e4e0137d4e6029d91ff3..0a3d8945356cb9414fc592fb3c4f7668c272e4b7 100644 (file)
@@ -5,7 +5,7 @@ use BookStack\Http\Controllers\Controller;
 use Illuminate\Database\Eloquent\Builder;
 use Illuminate\Http\JsonResponse;
 
-class ApiController extends Controller
+abstract class ApiController extends Controller
 {
 
     protected $rules = [];
index 84ddd521567ca13ebdada94d4722dee2e56ba307..80e86e101038f475da2e9f19a6821f7cab27b462 100644 (file)
@@ -1,8 +1,6 @@
 <?php namespace BookStack\Http\Controllers\Api;
 
 use BookStack\Api\ApiDocsGenerator;
-use Cache;
-use Illuminate\Support\Collection;
 
 class ApiDocsController extends ApiController
 {
@@ -12,7 +10,8 @@ class ApiDocsController extends ApiController
      */
     public function display()
     {
-        $docs = $this->getDocs();
+        $docs = ApiDocsGenerator::generateConsideringCache();
+        $this->setPageTitle(trans('settings.users_api_tokens_docs'));
         return view('api-docs.index', [
             'docs' => $docs,
         ]);
@@ -21,27 +20,10 @@ class ApiDocsController extends ApiController
     /**
      * Show a JSON view of the API docs data.
      */
-    public function json() {
-        $docs = $this->getDocs();
-        return response()->json($docs);
-    }
-
-    /**
-     * Get the base docs data.
-     * Checks and uses the system cache for quick re-fetching.
-     */
-    protected function getDocs(): Collection
+    public function json()
     {
-        $appVersion = trim(file_get_contents(base_path('version')));
-        $cacheKey = 'api-docs::' . $appVersion;
-        if (Cache::has($cacheKey) && config('app.env') === 'production') {
-            $docs = Cache::get($cacheKey);
-        } else {
-            $docs = (new ApiDocsGenerator())->generate();
-            Cache::put($cacheKey, $docs, 60*24);
-        }
-
-        return $docs;
+        $docs = ApiDocsGenerator::generateConsideringCache();
+        return response()->json($docs);
     }
 
 }
index 8333eba3a1d3779431dbbffa39e4e9abd49837d2..1b25b9645288e9ccc3afc80b2a631423d509c98a 100644 (file)
@@ -1,9 +1,8 @@
 <?php namespace BookStack\Http\Controllers\Api;
 
-use BookStack\Entities\Book;
+use BookStack\Entities\Models\Book;
 use BookStack\Entities\Repos\BookRepo;
 use BookStack\Exceptions\NotifyException;
-use BookStack\Facades\Activity;
 use Illuminate\Contracts\Container\BindingResolutionException;
 use Illuminate\Http\Request;
 use Illuminate\Validation\ValidationException;
@@ -26,9 +25,6 @@ class BookApiController extends ApiController
         ],
     ];
 
-    /**
-     * BooksApiController constructor.
-     */
     public function __construct(BookRepo $bookRepo)
     {
         $this->bookRepo = $bookRepo;
@@ -55,8 +51,6 @@ class BookApiController extends ApiController
         $requestData = $this->validate($request, $this->rules['create']);
 
         $book = $this->bookRepo->create($requestData);
-        Activity::add($book, 'book_create', $book->id);
-
         return response()->json($book);
     }
 
@@ -80,15 +74,14 @@ class BookApiController extends ApiController
 
         $requestData = $this->validate($request, $this->rules['update']);
         $book = $this->bookRepo->update($book, $requestData);
-        Activity::add($book, 'book_update', $book->id);
 
         return response()->json($book);
     }
 
     /**
-     * Delete a single book from the system.
-     * @throws NotifyException
-     * @throws BindingResolutionException
+     * Delete a single book.
+     * This will typically send the book to the recycle bin.
+     * @throws \Exception
      */
     public function delete(string $id)
     {
@@ -96,8 +89,6 @@ class BookApiController extends ApiController
         $this->checkOwnablePermission('book-delete', $book);
 
         $this->bookRepo->destroy($book);
-        Activity::addMessage('book_delete', $book->name);
-
         return response('', 204);
     }
 }
\ No newline at end of file
index 31fe5250fd7e3f14d7d60b8ff51a5d4bc4c241fd..3d813c4d4225fabdaa686d9b15881871f85303ad 100644 (file)
@@ -1,23 +1,16 @@
 <?php namespace BookStack\Http\Controllers\Api;
 
-use BookStack\Entities\Book;
-use BookStack\Entities\ExportService;
-use BookStack\Entities\Repos\BookRepo;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Tools\ExportFormatter;
 use Throwable;
 
 class BookExportApiController extends ApiController
 {
-    protected $bookRepo;
-    protected $exportService;
+    protected $exportFormatter;
 
-    /**
-     * BookExportController constructor.
-     */
-    public function __construct(BookRepo $bookRepo, ExportService $exportService)
+    public function __construct(ExportFormatter $exportFormatter)
     {
-        $this->bookRepo = $bookRepo;
-        $this->exportService = $exportService;
-        parent::__construct();
+        $this->exportFormatter = $exportFormatter;
     }
 
     /**
@@ -27,7 +20,7 @@ class BookExportApiController extends ApiController
     public function exportPdf(int $id)
     {
         $book = Book::visible()->findOrFail($id);
-        $pdfContent = $this->exportService->bookToPdf($book);
+        $pdfContent = $this->exportFormatter->bookToPdf($book);
         return $this->downloadResponse($pdfContent, $book->slug . '.pdf');
     }
 
@@ -38,7 +31,7 @@ class BookExportApiController extends ApiController
     public function exportHtml(int $id)
     {
         $book = Book::visible()->findOrFail($id);
-        $htmlContent = $this->exportService->bookToContainedHtml($book);
+        $htmlContent = $this->exportFormatter->bookToContainedHtml($book);
         return $this->downloadResponse($htmlContent, $book->slug . '.html');
     }
 
@@ -48,7 +41,7 @@ class BookExportApiController extends ApiController
     public function exportPlainText(int $id)
     {
         $book = Book::visible()->findOrFail($id);
-        $textContent = $this->exportService->bookToPlainText($book);
+        $textContent = $this->exportFormatter->bookToPlainText($book);
         return $this->downloadResponse($textContent, $book->slug . '.txt');
     }
 }
index 14b5e053b9ec42b8fc9c18dc0c40119be5dcbabf..c4851b003f6cc012d12658362e40f3d1608336f4 100644 (file)
@@ -1,8 +1,7 @@
 <?php namespace BookStack\Http\Controllers\Api;
 
-use BookStack\Facades\Activity;
 use BookStack\Entities\Repos\BookshelfRepo;
-use BookStack\Entities\Bookshelf;
+use BookStack\Entities\Models\Bookshelf;
 use Exception;
 use Illuminate\Database\Eloquent\Relations\BelongsToMany;
 use Illuminate\Http\Request;
@@ -31,7 +30,6 @@ class BookshelfApiController extends ApiController
 
     /**
      * BookshelfApiController constructor.
-     * @param BookshelfRepo $bookshelfRepo
      */
     public function __construct(BookshelfRepo $bookshelfRepo)
     {
@@ -63,7 +61,6 @@ class BookshelfApiController extends ApiController
         $bookIds = $request->get('books', []);
         $shelf = $this->bookshelfRepo->create($requestData, $bookIds);
 
-        Activity::add($shelf, 'bookshelf_create', $shelf->id);
         return response()->json($shelf);
     }
 
@@ -94,19 +91,17 @@ class BookshelfApiController extends ApiController
         $this->checkOwnablePermission('bookshelf-update', $shelf);
 
         $requestData = $this->validate($request, $this->rules['update']);
-
         $bookIds = $request->get('books', null);
 
         $shelf = $this->bookshelfRepo->update($shelf, $requestData, $bookIds);
-        Activity::add($shelf, 'bookshelf_update', $shelf->id);
-
         return response()->json($shelf);
     }
 
 
 
     /**
-     * Delete a single shelf from the system.
+     * Delete a single shelf.
+     * This will typically send the shelf to the recycle bin.
      * @throws Exception
      */
     public function delete(string $id)
@@ -115,8 +110,6 @@ class BookshelfApiController extends ApiController
         $this->checkOwnablePermission('bookshelf-delete', $shelf);
 
         $this->bookshelfRepo->destroy($shelf);
-        Activity::addMessage('bookshelf_delete', $shelf->name);
-
         return response('', 204);
     }
 }
\ No newline at end of file
index 50aa8834ec13ea7cc23bbf158a740f7dc16e8fb0..e69aecc2ddcd9df9141e30f30edd2bcd821bd590 100644 (file)
@@ -1,7 +1,8 @@
 <?php namespace BookStack\Http\Controllers\Api;
 
-use BookStack\Entities\Book;
-use BookStack\Entities\Chapter;
+use BookStack\Actions\ActivityType;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Chapter;
 use BookStack\Entities\Repos\ChapterRepo;
 use BookStack\Facades\Activity;
 use Illuminate\Database\Eloquent\Relations\HasMany;
@@ -58,8 +59,6 @@ class ChapterApiController extends ApiController
         $this->checkOwnablePermission('chapter-create', $book);
 
         $chapter = $this->chapterRepo->create($request->all(), $book);
-        Activity::add($chapter, 'chapter_create', $book->id);
-
         return response()->json($chapter->load(['tags']));
     }
 
@@ -83,13 +82,12 @@ class ChapterApiController extends ApiController
         $this->checkOwnablePermission('chapter-update', $chapter);
 
         $updatedChapter = $this->chapterRepo->update($chapter, $request->all());
-        Activity::add($chapter, 'chapter_update', $chapter->book->id);
-
         return response()->json($updatedChapter->load(['tags']));
     }
 
     /**
-     * Delete a chapter from the system.
+     * Delete a chapter.
+     * This will typically send the chapter to the recycle bin.
      */
     public function delete(string $id)
     {
@@ -97,8 +95,6 @@ class ChapterApiController extends ApiController
         $this->checkOwnablePermission('chapter-delete', $chapter);
 
         $this->chapterRepo->destroy($chapter);
-        Activity::addMessage('chapter_delete', $chapter->name, $chapter->book->id);
-
         return response('', 204);
     }
 }
index f19f29e9d2d752ee23182abbb0def5758e816714..afdfe555dd56f1bee4bc0a40139476b8a2d80c4f 100644 (file)
@@ -1,23 +1,20 @@
 <?php namespace BookStack\Http\Controllers\Api;
 
-use BookStack\Entities\Chapter;
-use BookStack\Entities\ExportService;
+use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Tools\ExportFormatter;
 use BookStack\Entities\Repos\BookRepo;
 use Throwable;
 
 class ChapterExportApiController extends ApiController
 {
-    protected $chapterRepo;
-    protected $exportService;
+    protected $exportFormatter;
 
     /**
      * ChapterExportController constructor.
      */
-    public function __construct(BookRepo $chapterRepo, ExportService $exportService)
+    public function __construct(ExportFormatter $exportFormatter)
     {
-        $this->chapterRepo = $chapterRepo;
-        $this->exportService = $exportService;
-        parent::__construct();
+        $this->exportFormatter = $exportFormatter;
     }
 
     /**
@@ -27,7 +24,7 @@ class ChapterExportApiController extends ApiController
     public function exportPdf(int $id)
     {
         $chapter = Chapter::visible()->findOrFail($id);
-        $pdfContent = $this->exportService->chapterToPdf($chapter);
+        $pdfContent = $this->exportFormatter->chapterToPdf($chapter);
         return $this->downloadResponse($pdfContent, $chapter->slug . '.pdf');
     }
 
@@ -38,7 +35,7 @@ class ChapterExportApiController extends ApiController
     public function exportHtml(int $id)
     {
         $chapter = Chapter::visible()->findOrFail($id);
-        $htmlContent = $this->exportService->chapterToContainedHtml($chapter);
+        $htmlContent = $this->exportFormatter->chapterToContainedHtml($chapter);
         return $this->downloadResponse($htmlContent, $chapter->slug . '.html');
     }
 
@@ -48,7 +45,7 @@ class ChapterExportApiController extends ApiController
     public function exportPlainText(int $id)
     {
         $chapter = Chapter::visible()->findOrFail($id);
-        $textContent = $this->exportService->chapterToPlainText($chapter);
+        $textContent = $this->exportFormatter->chapterToPlainText($chapter);
         return $this->downloadResponse($textContent, $chapter->slug . '.txt');
     }
 }
diff --git a/app/Http/Controllers/Api/PageApiController.php b/app/Http/Controllers/Api/PageApiController.php
new file mode 100644 (file)
index 0000000..0b3323c
--- /dev/null
@@ -0,0 +1,140 @@
+<?php
+
+namespace BookStack\Http\Controllers\Api;
+
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Models\Page;
+use BookStack\Entities\Repos\PageRepo;
+use BookStack\Exceptions\PermissionsException;
+use Exception;
+use Illuminate\Http\Request;
+
+class PageApiController extends ApiController
+{
+    protected $pageRepo;
+
+    protected $rules = [
+        'create' => [
+            'book_id' => 'required_without:chapter_id|integer',
+            'chapter_id' => 'required_without:book_id|integer',
+            'name' => 'required|string|max:255',
+            'html' => 'required_without:markdown|string',
+            'markdown' => 'required_without:html|string',
+            'tags' => 'array',
+        ],
+        'update' => [
+            'book_id' => 'required|integer',
+            'chapter_id' => 'required|integer',
+            'name' => 'string|min:1|max:255',
+            'html' => 'string',
+            'markdown' => 'string',
+            'tags' => 'array',
+        ],
+    ];
+
+    public function __construct(PageRepo $pageRepo)
+    {
+        $this->pageRepo = $pageRepo;
+    }
+
+    /**
+     * Get a listing of pages visible to the user.
+     */
+    public function list()
+    {
+        $pages = Page::visible();
+        return $this->apiListingResponse($pages, [
+            'id', 'book_id', 'chapter_id', 'name', 'slug', 'priority',
+            'draft', 'template',
+            'created_at', 'updated_at', 'created_by', 'updated_by',
+        ]);
+    }
+
+    /**
+     * Create a new page in the system.
+     *
+     * The ID of a parent book or chapter is required to indicate
+     * where this page should be located.
+     *
+     * Any HTML content provided should be kept to a single-block depth of plain HTML
+     * elements to remain compatible with the BookStack front-end and editors.
+     */
+    public function create(Request $request)
+    {
+        $this->validate($request, $this->rules['create']);
+
+        if ($request->has('chapter_id')) {
+            $parent = Chapter::visible()->findOrFail($request->get('chapter_id'));
+        } else {
+            $parent = Book::visible()->findOrFail($request->get('book_id'));
+        }
+        $this->checkOwnablePermission('page-create', $parent);
+
+        $draft = $this->pageRepo->getNewDraftPage($parent);
+        $this->pageRepo->publishDraft($draft, $request->only(array_keys($this->rules['create'])));
+
+        return response()->json($draft->forJsonDisplay());
+    }
+
+    /**
+     * View the details of a single page.
+     *
+     * Pages will always have HTML content. They may have markdown content
+     * if the markdown editor was used to last update the page.
+     */
+    public function read(string $id)
+    {
+        $page = $this->pageRepo->getById($id, []);
+        return response()->json($page->forJsonDisplay());
+    }
+
+    /**
+     * Update the details of a single page.
+     *
+     * See the 'create' action for details on the provided HTML/Markdown.
+     * Providing a 'book_id' or 'chapter_id' property will essentially move
+     * the page into that parent element if you have permissions to do so.
+     */
+    public function update(Request $request, string $id)
+    {
+        $page = $this->pageRepo->getById($id, []);
+        $this->checkOwnablePermission('page-update', $page);
+
+        $parent = null;
+        if ($request->has('chapter_id')) {
+            $parent = Chapter::visible()->findOrFail($request->get('chapter_id'));
+        } else if ($request->has('book_id')) {
+            $parent = Book::visible()->findOrFail($request->get('book_id'));
+        }
+
+        if ($parent && !$parent->matches($page->getParent())) {
+            $this->checkOwnablePermission('page-delete', $page);
+            try {
+                $this->pageRepo->move($page, $parent->getType() . ':' . $parent->id);
+            } catch (Exception $exception) {
+                if ($exception instanceof  PermissionsException) {
+                    $this->showPermissionError();
+                }
+
+                return $this->jsonError(trans('errors.selected_book_chapter_not_found'));
+            }
+        }
+
+        $updatedPage = $this->pageRepo->update($page, $request->all());
+        return response()->json($updatedPage->forJsonDisplay());
+    }
+
+    /**
+     * Delete a page.
+     * This will typically send the page to the recycle bin.
+     */
+    public function delete(string $id)
+    {
+        $page = $this->pageRepo->getById($id, []);
+        $this->checkOwnablePermission('page-delete', $page);
+
+        $this->pageRepo->destroy($page);
+        return response('', 204);
+    }
+}
diff --git a/app/Http/Controllers/Api/PageExportApiController.php b/app/Http/Controllers/Api/PageExportApiController.php
new file mode 100644 (file)
index 0000000..7563092
--- /dev/null
@@ -0,0 +1,47 @@
+<?php namespace BookStack\Http\Controllers\Api;
+
+use BookStack\Entities\Models\Page;
+use BookStack\Entities\Tools\ExportFormatter;
+use Throwable;
+
+class PageExportApiController extends ApiController
+{
+    protected $exportFormatter;
+
+    public function __construct(ExportFormatter $exportFormatter)
+    {
+        $this->exportFormatter = $exportFormatter;
+    }
+
+    /**
+     * Export a page as a PDF file.
+     * @throws Throwable
+     */
+    public function exportPdf(int $id)
+    {
+        $page = Page::visible()->findOrFail($id);
+        $pdfContent = $this->exportFormatter->pageToPdf($page);
+        return $this->downloadResponse($pdfContent, $page->slug . '.pdf');
+    }
+
+    /**
+     * Export a page as a contained HTML file.
+     * @throws Throwable
+     */
+    public function exportHtml(int $id)
+    {
+        $page = Page::visible()->findOrFail($id);
+        $htmlContent = $this->exportFormatter->pageToContainedHtml($page);
+        return $this->downloadResponse($htmlContent, $page->slug . '.html');
+    }
+
+    /**
+     * Export a page as a plain text file.
+     */
+    public function exportPlainText(int $id)
+    {
+        $page = Page::visible()->findOrFail($id);
+        $textContent = $this->exportFormatter->pageToPlainText($page);
+        return $this->downloadResponse($textContent, $page->slug . '.txt');
+    }
+}
index f52143292de060b4b0eaf883c73e9d6e7adb6789..04e89ac5d1a0db18407398aabd3689951822eee8 100644 (file)
@@ -25,7 +25,6 @@ class AttachmentController extends Controller
         $this->attachmentService = $attachmentService;
         $this->attachment = $attachment;
         $this->pageRepo = $pageRepo;
-        parent::__construct();
     }
 
 
index a3ef01baa472ec923de70068abcc84ee2fe09439..eb6eecc944ec0ba29567b2147b5d88af4f39d437 100644 (file)
@@ -23,11 +23,16 @@ class AuditLogController extends Controller
         ];
 
         $query = Activity::query()
-            ->with(['entity', 'user'])
+            ->with([
+                'entity' => function ($query) {
+                    $query->withTrashed();
+                },
+                'user'
+            ])
             ->orderBy($listDetails['sort'], $listDetails['order']);
 
         if ($listDetails['event']) {
-            $query->where('key', '=', $listDetails['event']);
+            $query->where('type', '=', $listDetails['event']);
         }
 
         if ($listDetails['date_from']) {
@@ -40,12 +45,12 @@ class AuditLogController extends Controller
         $activities = $query->paginate(100);
         $activities->appends($listDetails);
 
-        $keys = DB::table('activities')->select('key')->distinct()->pluck('key');
+        $types = DB::table('activities')->select('type')->distinct()->pluck('type');
         $this->setPageTitle(trans('settings.audit'));
         return view('settings.audit', [
             'activities' => $activities,
             'listDetails' => $listDetails,
-            'activityKeys' => $keys,
+            'activityTypes' => $types,
         ]);
     }
 }
index 099558eb77fdce133b307aaac327277bc02510f8..bffeb5f61b2f2f7528b87e6ad506f4b6dded53d2 100644 (file)
@@ -21,15 +21,11 @@ class ConfirmEmailController extends Controller
 
     /**
      * Create a new controller instance.
-     *
-     * @param EmailConfirmationService $emailConfirmationService
-     * @param UserRepo $userRepo
      */
     public function __construct(EmailConfirmationService $emailConfirmationService, UserRepo $userRepo)
     {
         $this->emailConfirmationService = $emailConfirmationService;
         $this->userRepo = $userRepo;
-        parent::__construct();
     }
 
 
index fadac641ecdb810b916560611029a1b517d3d6fe..5a033c6aad57d89fe7067b1c65016c200e327439 100644 (file)
@@ -2,6 +2,7 @@
 
 namespace BookStack\Http\Controllers\Auth;
 
+use BookStack\Actions\ActivityType;
 use BookStack\Http\Controllers\Controller;
 use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
 use Illuminate\Http\Request;
@@ -31,7 +32,6 @@ class ForgotPasswordController extends Controller
     {
         $this->middleware('guest');
         $this->middleware('guard:standard');
-        parent::__construct();
     }
 
 
@@ -52,6 +52,10 @@ class ForgotPasswordController extends Controller
             $request->only('email')
         );
 
+        if ($response === Password::RESET_LINK_SENT) {
+            $this->logActivity(ActivityType::AUTH_PASSWORD_RESET, $request->get('email'));
+        }
+
         if ($response === Password::RESET_LINK_SENT || $response === Password::INVALID_USER) {
             $message = trans('auth.reset_password_sent', ['email' => $request->get('email')]);
             $this->showSuccessNotification($message);
index 8084ce1a5dcfa220af09c73b21f711bdcc363dce..1252e6217a8b66f1bda3b405af28085a177308de 100644 (file)
@@ -3,10 +3,10 @@
 namespace BookStack\Http\Controllers\Auth;
 
 use Activity;
+use BookStack\Actions\ActivityType;
 use BookStack\Auth\Access\SocialAuthService;
 use BookStack\Exceptions\LoginAttemptEmailNeededException;
 use BookStack\Exceptions\LoginAttemptException;
-use BookStack\Exceptions\UserRegistrationException;
 use BookStack\Http\Controllers\Controller;
 use Illuminate\Foundation\Auth\AuthenticatesUsers;
 use Illuminate\Http\Request;
@@ -46,7 +46,6 @@ class LoginController extends Controller
         $this->socialAuthService = $socialAuthService;
         $this->redirectPath = url('/');
         $this->redirectAfterLogout = url('/login');
-        parent::__construct();
     }
 
     public function username()
@@ -151,6 +150,7 @@ class LoginController extends Controller
             }
         }
 
+        $this->logActivity(ActivityType::AUTH_LOGIN, $user);
         return redirect()->intended($this->redirectPath());
     }
 
index 0bdeef9e6855c1337c34ff934bece9ab5d42d45d..e3d22264d5301a73c2d2bee41daa1c71209512a9 100644 (file)
@@ -51,7 +51,6 @@ class RegisterController extends Controller
 
         $this->redirectTo = url('/');
         $this->redirectPath = url('/');
-        parent::__construct();
     }
 
     /**
index efdf0015924f6d831a0233a737e7209ff246b7e0..59e9ab79baa7cb146ed7582e1fc50cd88a7d9e31 100644 (file)
@@ -2,6 +2,7 @@
 
 namespace BookStack\Http\Controllers\Auth;
 
+use BookStack\Actions\ActivityType;
 use BookStack\Http\Controllers\Controller;
 use Illuminate\Foundation\Auth\ResetsPasswords;
 use Illuminate\Http\Request;
@@ -33,7 +34,6 @@ class ResetPasswordController extends Controller
     {
         $this->middleware('guest');
         $this->middleware('guard:standard');
-        parent::__construct();
     }
 
     /**
@@ -47,6 +47,7 @@ class ResetPasswordController extends Controller
     {
         $message = trans('auth.reset_password_success');
         $this->showSuccessNotification($message);
+        $this->logActivity(ActivityType::AUTH_PASSWORD_RESET_UPDATE, user());
         return redirect($this->redirectPath())
             ->with('status', trans($response));
     }
index 7ffcc572bcd06dc43f003df6edb9f8c05d84720e..8a3bf065ed566b55062a184c76c7144797417060 100644 (file)
@@ -15,7 +15,6 @@ class Saml2Controller extends Controller
      */
     public function __construct(Saml2Service $samlService)
     {
-        parent::__construct();
         $this->samlService = $samlService;
         $this->middleware('guard:saml2');
     }
index c61b1c42b688e58b8c6defd8c007f8db7a099009..926458fa613ddc1073305ea5405750c7ce01e085 100644 (file)
@@ -27,8 +27,6 @@ class UserInviteController extends Controller
 
         $this->inviteService = $inviteService;
         $this->userRepo = $userRepo;
-
-        parent::__construct();
     }
 
     /**
index 1643c62f980cd151dabd3fedf0031084bd336c78..3d695ba85dbdae692f35d24e429e4fc553effbd5 100644 (file)
@@ -1,12 +1,13 @@
 <?php namespace BookStack\Http\Controllers;
 
 use Activity;
-use BookStack\Entities\Managers\BookContents;
-use BookStack\Entities\Bookshelf;
-use BookStack\Entities\Managers\EntityContext;
+use BookStack\Actions\ActivityType;
+use BookStack\Entities\Tools\BookContents;
+use BookStack\Entities\Models\Bookshelf;
+use BookStack\Entities\Tools\PermissionsUpdater;
+use BookStack\Entities\Tools\ShelfContext;
 use BookStack\Entities\Repos\BookRepo;
 use BookStack\Exceptions\ImageUploadException;
-use BookStack\Exceptions\NotifyException;
 use Illuminate\Http\Request;
 use Illuminate\Validation\ValidationException;
 use Throwable;
@@ -18,14 +19,10 @@ class BookController extends Controller
     protected $bookRepo;
     protected $entityContextManager;
 
-    /**
-     * BookController constructor.
-     */
-    public function __construct(EntityContext $entityContextManager, BookRepo $bookRepo)
+    public function __construct(ShelfContext $entityContextManager, BookRepo $bookRepo)
     {
         $this->bookRepo = $bookRepo;
         $this->entityContextManager = $entityContextManager;
-        parent::__construct();
     }
 
     /**
@@ -97,11 +94,10 @@ class BookController extends Controller
 
         $book = $this->bookRepo->create($request->all());
         $this->bookRepo->updateCoverImage($book, $request->file('image', null));
-        Activity::add($book, 'book_create', $book->id);
 
         if ($bookshelf) {
             $bookshelf->appendBook($book);
-            Activity::add($bookshelf, 'bookshelf_update');
+            Activity::addForEntity($bookshelf, ActivityType::BOOKSHELF_UPDATE);
         }
 
         return redirect($book->getUrl());
@@ -162,8 +158,6 @@ class BookController extends Controller
         $resetCover = $request->has('image_reset');
         $this->bookRepo->updateCoverImage($book, $request->file('image', null), $resetCover);
 
-        Activity::add($book, 'book_update', $book->id);
-
         return redirect($book->getUrl());
     }
 
@@ -181,14 +175,12 @@ class BookController extends Controller
     /**
      * Remove the specified book from the system.
      * @throws Throwable
-     * @throws NotifyException
      */
     public function destroy(string $bookSlug)
     {
         $book = $this->bookRepo->getBySlug($bookSlug);
         $this->checkOwnablePermission('book-delete', $book);
 
-        Activity::addMessage('book_delete', $book->name);
         $this->bookRepo->destroy($book);
 
         return redirect('/books');
@@ -211,14 +203,12 @@ class BookController extends Controller
      * Set the restrictions for this book.
      * @throws Throwable
      */
-    public function permissions(Request $request, string $bookSlug)
+    public function permissions(Request $request, PermissionsUpdater $permissionsUpdater, string $bookSlug)
     {
         $book = $this->bookRepo->getBySlug($bookSlug);
         $this->checkOwnablePermission('restrictions-manage', $book);
 
-        $restricted = $request->get('restricted') === 'true';
-        $permissions = $request->filled('restrictions') ? collect($request->get('restrictions')) : null;
-        $this->bookRepo->updatePermissions($book, $restricted, $permissions);
+        $permissionsUpdater->updateFromPermissionsForm($book, $request);
 
         $this->showSuccessNotification(trans('entities.books_permissions_updated'));
         return redirect($book->getUrl());
index cfa3d6a3a3d162e9eb5d3bf19afa5eb4a4f6b7b8..1c1f124422f962020d31e4f35595b2e65d0f80cf 100644 (file)
@@ -2,7 +2,7 @@
 
 namespace BookStack\Http\Controllers;
 
-use BookStack\Entities\ExportService;
+use BookStack\Entities\Tools\ExportFormatter;
 use BookStack\Entities\Repos\BookRepo;
 use Throwable;
 
@@ -10,16 +10,15 @@ class BookExportController extends Controller
 {
 
     protected $bookRepo;
-    protected $exportService;
+    protected $exportFormatter;
 
     /**
      * BookExportController constructor.
      */
-    public function __construct(BookRepo $bookRepo, ExportService $exportService)
+    public function __construct(BookRepo $bookRepo, ExportFormatter $exportFormatter)
     {
         $this->bookRepo = $bookRepo;
-        $this->exportService = $exportService;
-        parent::__construct();
+        $this->exportFormatter = $exportFormatter;
     }
 
     /**
@@ -29,7 +28,7 @@ class BookExportController extends Controller
     public function pdf(string $bookSlug)
     {
         $book = $this->bookRepo->getBySlug($bookSlug);
-        $pdfContent = $this->exportService->bookToPdf($book);
+        $pdfContent = $this->exportFormatter->bookToPdf($book);
         return $this->downloadResponse($pdfContent, $bookSlug . '.pdf');
     }
 
@@ -40,7 +39,7 @@ class BookExportController extends Controller
     public function html(string $bookSlug)
     {
         $book = $this->bookRepo->getBySlug($bookSlug);
-        $htmlContent = $this->exportService->bookToContainedHtml($book);
+        $htmlContent = $this->exportFormatter->bookToContainedHtml($book);
         return $this->downloadResponse($htmlContent, $bookSlug . '.html');
     }
 
@@ -50,7 +49,7 @@ class BookExportController extends Controller
     public function plainText(string $bookSlug)
     {
         $book = $this->bookRepo->getBySlug($bookSlug);
-        $textContent = $this->exportService->bookToPlainText($book);
+        $textContent = $this->exportFormatter->bookToPlainText($book);
         return $this->downloadResponse($textContent, $bookSlug . '.txt');
     }
 }
index f5fb6f255537c2d16017f7365974863cc402260f..6d3199cbee990fe90b484c2b3a3bc6d4c73a54b8 100644 (file)
@@ -2,8 +2,9 @@
 
 namespace BookStack\Http\Controllers;
 
-use BookStack\Entities\Book;
-use BookStack\Entities\Managers\BookContents;
+use BookStack\Actions\ActivityType;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Tools\BookContents;
 use BookStack\Entities\Repos\BookRepo;
 use BookStack\Exceptions\SortOperationException;
 use BookStack\Facades\Activity;
@@ -14,14 +15,9 @@ class BookSortController extends Controller
 
     protected $bookRepo;
 
-    /**
-     * BookSortController constructor.
-     * @param $bookRepo
-     */
     public function __construct(BookRepo $bookRepo)
     {
         $this->bookRepo = $bookRepo;
-        parent::__construct();
     }
 
     /**
@@ -74,7 +70,7 @@ class BookSortController extends Controller
 
         // Rebuild permissions and add activity for involved books.
         $booksInvolved->each(function (Book $book) {
-            Activity::add($book, 'book_sort', $book->id);
+            Activity::addForEntity($book, ActivityType::BOOK_SORT);
         });
 
         return redirect($book->getUrl());
index f2cc11c7ba16126eb6f7fef610c8662525d190bf..32c22e185fa20116cabc0d3221d621c0e3f70d12 100644 (file)
@@ -1,8 +1,9 @@
 <?php namespace BookStack\Http\Controllers;
 
 use Activity;
-use BookStack\Entities\Book;
-use BookStack\Entities\Managers\EntityContext;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Tools\PermissionsUpdater;
+use BookStack\Entities\Tools\ShelfContext;
 use BookStack\Entities\Repos\BookshelfRepo;
 use BookStack\Exceptions\ImageUploadException;
 use BookStack\Exceptions\NotFoundException;
@@ -19,15 +20,11 @@ class BookshelfController extends Controller
     protected $entityContextManager;
     protected $imageRepo;
 
-    /**
-     * BookController constructor.
-     */
-    public function __construct(BookshelfRepo $bookshelfRepo, EntityContext $entityContextManager, ImageRepo $imageRepo)
+    public function __construct(BookshelfRepo $bookshelfRepo, ShelfContext $entityContextManager, ImageRepo $imageRepo)
     {
         $this->bookshelfRepo = $bookshelfRepo;
         $this->entityContextManager = $entityContextManager;
         $this->imageRepo = $imageRepo;
-        parent::__construct();
     }
 
     /**
@@ -92,7 +89,6 @@ class BookshelfController extends Controller
         $shelf = $this->bookshelfRepo->create($request->all(), $bookIds);
         $this->bookshelfRepo->updateCoverImage($shelf, $request->file('image', null));
 
-        Activity::add($shelf, 'bookshelf_create');
         return redirect($shelf->getUrl());
     }
 
@@ -156,7 +152,6 @@ class BookshelfController extends Controller
         $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());
     }
@@ -182,7 +177,6 @@ class BookshelfController extends Controller
         $shelf = $this->bookshelfRepo->getBySlug($slug);
         $this->checkOwnablePermission('bookshelf-delete', $shelf);
 
-        Activity::addMessage('bookshelf_delete', $shelf->name);
         $this->bookshelfRepo->destroy($shelf);
 
         return redirect('/shelves');
@@ -204,14 +198,12 @@ class BookshelfController extends Controller
     /**
      * Set the permissions for this bookshelf.
      */
-    public function permissions(Request $request, string $slug)
+    public function permissions(Request $request, PermissionsUpdater $permissionsUpdater, string $slug)
     {
         $shelf = $this->bookshelfRepo->getBySlug($slug);
         $this->checkOwnablePermission('restrictions-manage', $shelf);
 
-        $restricted = $request->get('restricted') === 'true';
-        $permissions = $request->filled('restrictions') ? collect($request->get('restrictions')) : null;
-        $this->bookshelfRepo->updatePermissions($shelf, $restricted, $permissions);
+        $permissionsUpdater->updateFromPermissionsForm($shelf, $request);
 
         $this->showSuccessNotification(trans('entities.shelves_permissions_updated'));
         return redirect($shelf->getUrl());
index 1355979107eb0181d272e3610511688d5772b7b7..1d69df2a2f6029e148ff59a4f1bb59f04678d8b0 100644 (file)
@@ -1,9 +1,9 @@
 <?php namespace BookStack\Http\Controllers;
 
-use Activity;
-use BookStack\Entities\Book;
-use BookStack\Entities\Managers\BookContents;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Tools\BookContents;
 use BookStack\Entities\Repos\ChapterRepo;
+use BookStack\Entities\Tools\PermissionsUpdater;
 use BookStack\Exceptions\MoveOperationException;
 use BookStack\Exceptions\NotFoundException;
 use Illuminate\Http\Request;
@@ -22,7 +22,6 @@ class ChapterController extends Controller
     public function __construct(ChapterRepo $chapterRepo)
     {
         $this->chapterRepo = $chapterRepo;
-        parent::__construct();
     }
 
     /**
@@ -51,7 +50,6 @@ class ChapterController extends Controller
         $this->checkOwnablePermission('chapter-create', $book);
 
         $chapter = $this->chapterRepo->create($request->all(), $book);
-        Activity::add($chapter, 'chapter_create', $book->id);
 
         return redirect($chapter->getUrl());
     }
@@ -100,7 +98,6 @@ class ChapterController extends Controller
         $this->checkOwnablePermission('chapter-update', $chapter);
 
         $this->chapterRepo->update($chapter, $request->all());
-        Activity::add($chapter, 'chapter_update', $chapter->book->id);
 
         return redirect($chapter->getUrl());
     }
@@ -128,7 +125,6 @@ class ChapterController extends Controller
         $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
         $this->checkOwnablePermission('chapter-delete', $chapter);
 
-        Activity::addMessage('chapter_delete', $chapter->name, $chapter->book->id);
         $this->chapterRepo->destroy($chapter);
 
         return redirect($chapter->book->getUrl());
@@ -173,8 +169,6 @@ class ChapterController extends Controller
             return redirect()->back();
         }
 
-        Activity::add($chapter, 'chapter_move', $newBook->id);
-
         $this->showSuccessNotification(trans('entities.chapter_move_success', ['bookName' => $newBook->name]));
         return redirect($chapter->getUrl());
     }
@@ -197,14 +191,12 @@ class ChapterController extends Controller
      * Set the restrictions for this chapter.
      * @throws NotFoundException
      */
-    public function permissions(Request $request, string $bookSlug, string $chapterSlug)
+    public function permissions(Request $request, PermissionsUpdater $permissionsUpdater, string $bookSlug, string $chapterSlug)
     {
         $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
         $this->checkOwnablePermission('restrictions-manage', $chapter);
 
-        $restricted = $request->get('restricted') === 'true';
-        $permissions = $request->filled('restrictions') ? collect($request->get('restrictions')) : null;
-        $this->chapterRepo->updatePermissions($chapter, $restricted, $permissions);
+        $permissionsUpdater->updateFromPermissionsForm($chapter, $request);
 
         $this->showSuccessNotification(trans('entities.chapters_permissions_success'));
         return redirect($chapter->getUrl());
index 0c86f854828b70dad5418a9b475c7262aef16612..52d087442ab287eb2d533365ed6cfd8cce5da642 100644 (file)
@@ -1,6 +1,6 @@
 <?php namespace BookStack\Http\Controllers;
 
-use BookStack\Entities\ExportService;
+use BookStack\Entities\Tools\ExportFormatter;
 use BookStack\Entities\Repos\ChapterRepo;
 use BookStack\Exceptions\NotFoundException;
 use Throwable;
@@ -9,16 +9,15 @@ class ChapterExportController extends Controller
 {
 
     protected $chapterRepo;
-    protected $exportService;
+    protected $exportFormatter;
 
     /**
      * ChapterExportController constructor.
      */
-    public function __construct(ChapterRepo $chapterRepo, ExportService $exportService)
+    public function __construct(ChapterRepo $chapterRepo, ExportFormatter $exportFormatter)
     {
         $this->chapterRepo = $chapterRepo;
-        $this->exportService = $exportService;
-        parent::__construct();
+        $this->exportFormatter = $exportFormatter;
     }
 
     /**
@@ -29,7 +28,7 @@ class ChapterExportController extends Controller
     public function pdf(string $bookSlug, string $chapterSlug)
     {
         $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
-        $pdfContent = $this->exportService->chapterToPdf($chapter);
+        $pdfContent = $this->exportFormatter->chapterToPdf($chapter);
         return $this->downloadResponse($pdfContent, $chapterSlug . '.pdf');
     }
 
@@ -41,7 +40,7 @@ class ChapterExportController extends Controller
     public function html(string $bookSlug, string $chapterSlug)
     {
         $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
-        $containedHtml = $this->exportService->chapterToContainedHtml($chapter);
+        $containedHtml = $this->exportFormatter->chapterToContainedHtml($chapter);
         return $this->downloadResponse($containedHtml, $chapterSlug . '.html');
     }
 
@@ -52,7 +51,7 @@ class ChapterExportController extends Controller
     public function plainText(string $bookSlug, string $chapterSlug)
     {
         $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
-        $chapterText = $this->exportService->chapterToPlainText($chapter);
+        $chapterText = $this->exportFormatter->chapterToPlainText($chapter);
         return $this->downloadResponse($chapterText, $chapterSlug . '.txt');
     }
 }
index 4eb56a4b0cd0720cfcc110e5c22662b958231f4b..bf1a76f518f3ce70f2792bd1138bc309ca961d4d 100644 (file)
@@ -1,8 +1,9 @@
 <?php namespace BookStack\Http\Controllers;
 
 use Activity;
+use BookStack\Actions\ActivityType;
 use BookStack\Actions\CommentRepo;
-use BookStack\Entities\Page;
+use BookStack\Entities\Models\Page;
 use Illuminate\Http\Request;
 use Illuminate\Validation\ValidationException;
 
@@ -13,7 +14,6 @@ class CommentController extends Controller
     public function __construct(CommentRepo $commentRepo)
     {
         $this->commentRepo = $commentRepo;
-        parent::__construct();
     }
 
     /**
@@ -40,7 +40,6 @@ class CommentController extends Controller
         // Create a new comment.
         $this->checkPermission('comment-create-all');
         $comment = $this->commentRepo->create($page, $request->get('text'), $request->get('parent_id'));
-        Activity::add($page, 'commented_on', $page->book->id);
         return view('comments.comment', ['comment' => $comment]);
     }
 
index 6a1dfcb0140062d0fcabcffe9174f226f1cacc48..479d5ac15852be57d44370f433185c15839af226 100644 (file)
@@ -2,26 +2,21 @@
 
 namespace BookStack\Http\Controllers;
 
-use BookStack\Ownable;
+use BookStack\Facades\Activity;
+use BookStack\Interfaces\Loggable;
+use BookStack\HasCreatorAndUpdater;
+use BookStack\Model;
 use Illuminate\Foundation\Bus\DispatchesJobs;
 use Illuminate\Foundation\Validation\ValidatesRequests;
 use Illuminate\Http\Exceptions\HttpResponseException;
-use Illuminate\Http\Request;
+use Illuminate\Http\JsonResponse;
+use Illuminate\Http\Response;
 use Illuminate\Routing\Controller as BaseController;
-use Illuminate\Validation\ValidationException;
 
 abstract class Controller extends BaseController
 {
     use DispatchesJobs, ValidatesRequests;
 
-    /**
-     * Controller constructor.
-     */
-    public function __construct()
-    {
-        //
-    }
-
     /**
      * Check if the current user is signed in.
      */
@@ -43,9 +38,8 @@ abstract class Controller extends BaseController
 
     /**
      * Adds the page title into the view.
-     * @param $title
      */
-    public function setPageTitle($title)
+    public function setPageTitle(string $title)
     {
         view()->share('pageTitle', $title);
     }
@@ -67,79 +61,59 @@ abstract class Controller extends BaseController
     }
 
     /**
-     * Checks for a permission.
-     * @param string $permissionName
-     * @return bool|\Illuminate\Http\RedirectResponse
+     * Checks that the current user has the given permission otherwise throw an exception.
      */
-    protected function checkPermission($permissionName)
+    protected function checkPermission(string $permission): void
     {
-        if (!user() || !user()->can($permissionName)) {
+        if (!user() || !user()->can($permission)) {
             $this->showPermissionError();
         }
-        return true;
     }
 
     /**
-     * Check the current user's permissions against an ownable item.
-     * @param $permission
-     * @param Ownable $ownable
-     * @return bool
+     * Check the current user's permissions against an ownable item otherwise throw an exception.
      */
-    protected function checkOwnablePermission($permission, Ownable $ownable)
+    protected function checkOwnablePermission(string $permission, Model $ownable): void
     {
-        if (userCan($permission, $ownable)) {
-            return true;
+        if (!userCan($permission, $ownable)) {
+            $this->showPermissionError();
         }
-        return $this->showPermissionError();
     }
 
     /**
-     * Check if a user has a permission or bypass if the callback is true.
-     * @param $permissionName
-     * @param $callback
-     * @return bool
+     * Check if a user has a permission or bypass the permission
+     * check if the given callback resolves true.
      */
-    protected function checkPermissionOr($permissionName, $callback)
+    protected function checkPermissionOr(string $permission, callable $callback): void
     {
-        $callbackResult = $callback();
-        if ($callbackResult === false) {
-            $this->checkPermission($permissionName);
+        if ($callback() !== true) {
+            $this->checkPermission($permission);
         }
-        return true;
     }
 
     /**
      * Check if the current user has a permission or bypass if the provided user
      * id matches the current user.
-     * @param string $permissionName
-     * @param int $userId
-     * @return bool
      */
-    protected function checkPermissionOrCurrentUser(string $permissionName, int $userId)
+    protected function checkPermissionOrCurrentUser(string $permission, int $userId): void
     {
-        return $this->checkPermissionOr($permissionName, function () use ($userId) {
+        $this->checkPermissionOr($permission, function () use ($userId) {
             return $userId === user()->id;
         });
     }
 
     /**
      * Send back a json error message.
-     * @param string $messageText
-     * @param int $statusCode
-     * @return mixed
      */
-    protected function jsonError($messageText = "", $statusCode = 500)
+    protected function jsonError(string $messageText = "", int $statusCode = 500): JsonResponse
     {
         return response()->json(['message' => $messageText, 'status' => 'error'], $statusCode);
     }
 
     /**
      * Create a response that forces a download in the browser.
-     * @param string $content
-     * @param string $fileName
-     * @return \Illuminate\Http\Response
      */
-    protected function downloadResponse(string $content, string $fileName)
+    protected function downloadResponse(string $content, string $fileName): Response
     {
         return response()->make($content, 200, [
             'Content-Type'        => 'application/octet-stream',
@@ -149,31 +123,37 @@ abstract class Controller extends BaseController
 
     /**
      * Show a positive, successful notification to the user on next view load.
-     * @param string $message
      */
-    protected function showSuccessNotification(string $message)
+    protected function showSuccessNotification(string $message): void
     {
         session()->flash('success', $message);
     }
 
     /**
      * Show a warning notification to the user on next view load.
-     * @param string $message
      */
-    protected function showWarningNotification(string $message)
+    protected function showWarningNotification(string $message): void
     {
         session()->flash('warning', $message);
     }
 
     /**
      * Show an error notification to the user on next view load.
-     * @param string $message
      */
-    protected function showErrorNotification(string $message)
+    protected function showErrorNotification(string $message): void
     {
         session()->flash('error', $message);
     }
 
+    /**
+     * Log an activity in the system.
+     * @param string|Loggable
+     */
+    protected function logActivity(string $type, $detail = ''): void
+    {
+        Activity::add($type, $detail);
+    }
+
     /**
      * Get the validation rules for image files.
      */
index 60d2664d03a81107b9427f1258a8a82664551c90..d97740d2725f01d8bedda10e687fd915d5e08deb 100644 (file)
@@ -1,9 +1,9 @@
 <?php namespace BookStack\Http\Controllers;
 
 use Activity;
-use BookStack\Entities\Book;
-use BookStack\Entities\Managers\PageContent;
-use BookStack\Entities\Page;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Tools\PageContent;
+use BookStack\Entities\Models\Page;
 use BookStack\Entities\Repos\BookRepo;
 use BookStack\Entities\Repos\BookshelfRepo;
 use Illuminate\Http\Response;
@@ -14,7 +14,6 @@ class HomeController extends Controller
 
     /**
      * Display the homepage.
-     * @return Response
      */
     public function index()
     {
@@ -22,17 +21,24 @@ class HomeController extends Controller
         $draftPages = [];
 
         if ($this->isSignedIn()) {
-            $draftPages = Page::visible()->where('draft', '=', true)
+            $draftPages = Page::visible()
+                ->where('draft', '=', true)
                 ->where('created_by', '=', user()->id)
-                ->orderBy('updated_at', 'desc')->take(6)->get();
+                ->orderBy('updated_at', 'desc')
+                ->with('book')
+                ->take(6)
+                ->get();
         }
 
         $recentFactor = count($draftPages) > 0 ? 0.5 : 1;
         $recents = $this->isSignedIn() ?
-              Views::getUserRecentlyViewed(12*$recentFactor, 0)
+              Views::getUserRecentlyViewed(12*$recentFactor, 1)
             : Book::visible()->orderBy('created_at', 'desc')->take(12 * $recentFactor)->get();
-        $recentlyUpdatedPages = Page::visible()->where('draft', false)
-            ->orderBy('updated_at', 'desc')->take(12)->get();
+        $recentlyUpdatedPages = Page::visible()->with('book')
+            ->where('draft', false)
+            ->orderBy('updated_at', 'desc')
+            ->take(12)
+            ->get();
 
         $homepageOptions = ['default', 'books', 'bookshelves', 'page'];
         $homepageOption = setting('app-homepage-type', 'default');
index 29b1e9027ea128ff9189cc086e97a336c5381441..462ab68f6f32f66ee44ef4439d2448ff62e2c12e 100644 (file)
@@ -15,7 +15,6 @@ class DrawioImageController extends Controller
     public function __construct(ImageRepo $imageRepo)
     {
         $this->imageRepo = $imageRepo;
-        parent::__construct();
     }
 
     /**
index 61907c0039bc7f72d8ec7cdd34804cf5f33536b1..c3ad0b7b261fe6d5946de46eacf5f263715e7470 100644 (file)
@@ -18,7 +18,6 @@ class GalleryImageController extends Controller
     public function __construct(ImageRepo $imageRepo)
     {
         $this->imageRepo = $imageRepo;
-        parent::__construct();
     }
 
     /**
index 52cc463c8543753803b556057af1db7caab280da..ecc36bf67e24ad531f83326ed32d22bf4f97f63d 100644 (file)
@@ -1,14 +1,11 @@
 <?php namespace BookStack\Http\Controllers\Images;
 
-use BookStack\Entities\Page;
 use BookStack\Exceptions\ImageUploadException;
 use BookStack\Http\Controllers\Controller;
-use BookStack\Entities\Repos\PageRepo;
 use BookStack\Uploads\Image;
 use BookStack\Uploads\ImageRepo;
 use Exception;
 use Illuminate\Filesystem\Filesystem as File;
-use Illuminate\Http\JsonResponse;
 use Illuminate\Http\Request;
 use Illuminate\Validation\ValidationException;
 
@@ -26,7 +23,6 @@ class ImageController extends Controller
         $this->image = $image;
         $this->file = $file;
         $this->imageRepo = $imageRepo;
-        parent::__construct();
     }
 
     /**
index 664a896b25e714ca36ab80c45ee724d0374dbf82..3354a148cfd1f08a628b0f30c0f15d7a4f15b21b 100644 (file)
@@ -2,6 +2,8 @@
 
 namespace BookStack\Http\Controllers;
 
+use BookStack\Actions\ActivityType;
+use BookStack\Entities\Tools\TrashCan;
 use BookStack\Notifications\TestEmail;
 use BookStack\Uploads\ImageService;
 use Illuminate\Http\Request;
@@ -19,7 +21,13 @@ class MaintenanceController extends Controller
         // Get application version
         $version = trim(file_get_contents(base_path('version')));
 
-        return view('settings.maintenance', ['version' => $version]);
+        // Recycle bin details
+        $recycleStats = (new TrashCan())->getTrashedCounts();
+
+        return view('settings.maintenance', [
+            'version' => $version,
+            'recycleStats' => $recycleStats,
+        ]);
     }
 
     /**
@@ -28,6 +36,7 @@ class MaintenanceController extends Controller
     public function cleanupImages(Request $request, ImageService $imageService)
     {
         $this->checkPermission('settings-manage');
+        $this->logActivity(ActivityType::MAINTENANCE_ACTION_RUN, 'cleanup-images');
 
         $checkRevisions = !($request->get('ignore_revisions', 'false') === 'true');
         $dryRun = !($request->has('confirm'));
@@ -54,6 +63,7 @@ class MaintenanceController extends Controller
     public function sendTestEmail()
     {
         $this->checkPermission('settings-manage');
+        $this->logActivity(ActivityType::MAINTENANCE_ACTION_RUN, 'send-test-email');
 
         try {
             user()->notify(new TestEmail());
index 57d70fb3247f8177b4879e9f25eecf54dbbfd76d..7d8e54382961006db647b5f1b1b4fe9982337a2d 100644 (file)
@@ -1,11 +1,11 @@
 <?php namespace BookStack\Http\Controllers;
 
-use Activity;
-use BookStack\Entities\Managers\BookContents;
-use BookStack\Entities\Managers\PageContent;
-use BookStack\Entities\Managers\PageEditActivity;
-use BookStack\Entities\Page;
+use BookStack\Entities\Tools\BookContents;
+use BookStack\Entities\Tools\PageContent;
+use BookStack\Entities\Tools\PageEditActivity;
+use BookStack\Entities\Models\Page;
 use BookStack\Entities\Repos\PageRepo;
+use BookStack\Entities\Tools\PermissionsUpdater;
 use BookStack\Exceptions\NotFoundException;
 use BookStack\Exceptions\NotifyException;
 use BookStack\Exceptions\PermissionsException;
@@ -26,7 +26,6 @@ class PageController extends Controller
     public function __construct(PageRepo $pageRepo)
     {
         $this->pageRepo = $pageRepo;
-        parent::__construct();
     }
 
     /**
@@ -78,7 +77,7 @@ class PageController extends Controller
     public function editDraft(string $bookSlug, int $pageId)
     {
         $draft = $this->pageRepo->getById($pageId);
-        $this->checkOwnablePermission('page-create', $draft->parent());
+        $this->checkOwnablePermission('page-create', $draft->getParent());
         $this->setPageTitle(trans('entities.pages_edit_draft'));
 
         $draftsEnabled = $this->isSignedIn();
@@ -104,10 +103,9 @@ class PageController extends Controller
             'name' => 'required|string|max:255'
         ]);
         $draftPage = $this->pageRepo->getById($pageId);
-        $this->checkOwnablePermission('page-create', $draftPage->parent());
+        $this->checkOwnablePermission('page-create', $draftPage->getParent());
 
         $page = $this->pageRepo->publishDraft($draftPage, $request->all());
-        Activity::add($page, 'page_create', $draftPage->book->id);
 
         return redirect($page->getUrl());
     }
@@ -224,7 +222,6 @@ class PageController extends Controller
         $this->checkOwnablePermission('page-update', $page);
 
         $this->pageRepo->update($page, $request->all());
-        Activity::add($page, 'page_update', $page->book->id);
 
         return redirect($page->getUrl());
     }
@@ -304,13 +301,10 @@ class PageController extends Controller
     {
         $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
         $this->checkOwnablePermission('page-delete', $page);
+        $parent = $page->getParent();
 
-        $book = $page->book;
-        $parent = $page->chapter ?? $book;
         $this->pageRepo->destroy($page);
-        Activity::addMessage('page_delete', $page->name, $book->id);
 
-        $this->showSuccessNotification(trans('entities.pages_delete_success'));
         return redirect($parent->getUrl());
     }
 
@@ -394,7 +388,6 @@ class PageController extends Controller
             return redirect()->back();
         }
 
-        Activity::add($page, 'page_move', $page->book->id);
         $this->showSuccessNotification(trans('entities.pages_move_success', ['parentName' => $parent->name]));
         return redirect($page->getUrl());
     }
@@ -439,8 +432,6 @@ class PageController extends Controller
             return redirect()->back();
         }
 
-        Activity::add($pageCopy, 'page_create', $pageCopy->book->id);
-
         $this->showSuccessNotification(trans('entities.pages_copy_success'));
         return redirect($pageCopy->getUrl());
     }
@@ -463,14 +454,12 @@ class PageController extends Controller
      * @throws NotFoundException
      * @throws Throwable
      */
-    public function permissions(Request $request, string $bookSlug, string $pageSlug)
+    public function permissions(Request $request, PermissionsUpdater $permissionsUpdater, string $bookSlug, string $pageSlug)
     {
         $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
         $this->checkOwnablePermission('restrictions-manage', $page);
 
-        $restricted = $request->get('restricted') === 'true';
-        $permissions = $request->filled('restrictions') ? collect($request->get('restrictions')) : null;
-        $this->pageRepo->updatePermissions($page, $restricted, $permissions);
+        $permissionsUpdater->updateFromPermissionsForm($page, $request);
 
         $this->showSuccessNotification(trans('entities.pages_permissions_success'));
         return redirect($page->getUrl());
index 3b02ea224716c4f01bc1ceffdd043d5e88702ba4..e5e027fe72cd2f5cec19418d9ea81901238e2eb7 100644 (file)
@@ -2,8 +2,8 @@
 
 namespace BookStack\Http\Controllers;
 
-use BookStack\Entities\ExportService;
-use BookStack\Entities\Managers\PageContent;
+use BookStack\Entities\Tools\ExportFormatter;
+use BookStack\Entities\Tools\PageContent;
 use BookStack\Entities\Repos\PageRepo;
 use BookStack\Exceptions\NotFoundException;
 use Throwable;
@@ -12,18 +12,15 @@ class PageExportController extends Controller
 {
 
     protected $pageRepo;
-    protected $exportService;
+    protected $exportFormatter;
 
     /**
      * PageExportController constructor.
-     * @param PageRepo $pageRepo
-     * @param ExportService $exportService
      */
-    public function __construct(PageRepo $pageRepo, ExportService $exportService)
+    public function __construct(PageRepo $pageRepo, ExportFormatter $exportFormatter)
     {
         $this->pageRepo = $pageRepo;
-        $this->exportService = $exportService;
-        parent::__construct();
+        $this->exportFormatter = $exportFormatter;
     }
 
     /**
@@ -36,7 +33,7 @@ class PageExportController extends Controller
     {
         $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
         $page->html = (new PageContent($page))->render();
-        $pdfContent = $this->exportService->pageToPdf($page);
+        $pdfContent = $this->exportFormatter->pageToPdf($page);
         return $this->downloadResponse($pdfContent, $pageSlug . '.pdf');
     }
 
@@ -49,7 +46,7 @@ class PageExportController extends Controller
     {
         $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
         $page->html = (new PageContent($page))->render();
-        $containedHtml = $this->exportService->pageToContainedHtml($page);
+        $containedHtml = $this->exportFormatter->pageToContainedHtml($page);
         return $this->downloadResponse($containedHtml, $pageSlug . '.html');
     }
 
@@ -60,7 +57,7 @@ class PageExportController extends Controller
     public function plainText(string $bookSlug, string $pageSlug)
     {
         $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
-        $pageText = $this->exportService->pageToPlainText($page);
+        $pageText = $this->exportFormatter->pageToPlainText($page);
         return $this->downloadResponse($pageText, $pageSlug . '.txt');
     }
 }
index 797f5db8f43ff1ca9f8f561e029b888cec7a662d..4c43330164b743133490dbdf8e764cdfc2836383 100644 (file)
@@ -1,10 +1,9 @@
 <?php namespace BookStack\Http\Controllers;
 
-use BookStack\Entities\Managers\PageContent;
+use BookStack\Entities\Tools\PageContent;
 use BookStack\Entities\Repos\PageRepo;
 use BookStack\Exceptions\NotFoundException;
-use BookStack\Facades\Activity;
-use GatherContent\Htmldiff\Htmldiff;
+use Ssddanbrown\HtmlDiff\Diff;
 
 class PageRevisionController extends Controller
 {
@@ -17,7 +16,6 @@ class PageRevisionController extends Controller
     public function __construct(PageRepo $pageRepo)
     {
         $this->pageRepo = $pageRepo;
-        parent::__construct();
     }
 
     /**
@@ -74,7 +72,7 @@ class PageRevisionController extends Controller
 
         $prev = $revision->getPrevious();
         $prevContent = $prev->html ?? '';
-        $diff = (new Htmldiff)->diff($prevContent, $revision->html);
+        $diff = Diff::excecute($prevContent, $revision->html);
 
         $page->fill($revision->toArray());
         // TODO - Refactor PageContent so we don't need to juggle this
@@ -101,7 +99,6 @@ class PageRevisionController extends Controller
 
         $page = $this->pageRepo->restoreRevision($page, $revisionId);
 
-        Activity::add($page, 'page_restore', $page->book->id);
         return redirect($page->getUrl());
     }
 
index eaa1a8ae26ae18f28473c6796062dfb5065dba97..2307bc0d52c09b5bd6ee1411f795ac83c11f8e0a 100644 (file)
@@ -16,7 +16,6 @@ class PageTemplateController extends Controller
     public function __construct(PageRepo $pageRepo)
     {
         $this->pageRepo = $pageRepo;
-        parent::__construct();
     }
 
     /**
diff --git a/app/Http/Controllers/RecycleBinController.php b/app/Http/Controllers/RecycleBinController.php
new file mode 100644 (file)
index 0000000..a644a28
--- /dev/null
@@ -0,0 +1,107 @@
+<?php namespace BookStack\Http\Controllers;
+
+use BookStack\Actions\ActivityType;
+use BookStack\Entities\Models\Deletion;
+use BookStack\Entities\Tools\TrashCan;
+
+class RecycleBinController extends Controller
+{
+
+    protected $recycleBinBaseUrl = '/settings/recycle-bin';
+
+    /**
+     * On each request to a method of this controller check permissions
+     * using a middleware closure.
+     */
+    public function __construct()
+    {
+        $this->middleware(function ($request, $next) {
+            $this->checkPermission('settings-manage');
+            $this->checkPermission('restrictions-manage-all');
+            return $next($request);
+        });
+    }
+
+
+    /**
+     * Show the top-level listing for the recycle bin.
+     */
+    public function index()
+    {
+        $deletions = Deletion::query()->with(['deletable', 'deleter'])->paginate(10);
+
+        $this->setPageTitle(trans('settings.recycle_bin'));
+        return view('settings.recycle-bin.index', [
+            'deletions' => $deletions,
+        ]);
+    }
+
+    /**
+     * Show the page to confirm a restore of the deletion of the given id.
+     */
+    public function showRestore(string $id)
+    {
+        /** @var Deletion $deletion */
+        $deletion = Deletion::query()->findOrFail($id);
+
+        return view('settings.recycle-bin.restore', [
+            'deletion' => $deletion,
+        ]);
+    }
+
+    /**
+     * Restore the element attached to the given deletion.
+     * @throws \Exception
+     */
+    public function restore(string $id)
+    {
+        /** @var Deletion $deletion */
+        $deletion = Deletion::query()->findOrFail($id);
+        $this->logActivity(ActivityType::RECYCLE_BIN_RESTORE, $deletion);
+        $restoreCount = (new TrashCan())->restoreFromDeletion($deletion);
+
+        $this->showSuccessNotification(trans('settings.recycle_bin_restore_notification', ['count' => $restoreCount]));
+        return redirect($this->recycleBinBaseUrl);
+    }
+
+    /**
+     * Show the page to confirm a Permanent deletion of the element attached to the deletion of the given id.
+     */
+    public function showDestroy(string $id)
+    {
+        /** @var Deletion $deletion */
+        $deletion = Deletion::query()->findOrFail($id);
+
+        return view('settings.recycle-bin.destroy', [
+            'deletion' => $deletion,
+        ]);
+    }
+
+    /**
+     * Permanently delete the content associated with the given deletion.
+     * @throws \Exception
+     */
+    public function destroy(string $id)
+    {
+        /** @var Deletion $deletion */
+        $deletion = Deletion::query()->findOrFail($id);
+        $this->logActivity(ActivityType::RECYCLE_BIN_DESTROY, $deletion);
+        $deleteCount = (new TrashCan())->destroyFromDeletion($deletion);
+
+        $this->showSuccessNotification(trans('settings.recycle_bin_destroy_notification', ['count' => $deleteCount]));
+        return redirect($this->recycleBinBaseUrl);
+    }
+
+    /**
+     * Empty out the recycle bin.
+     * @throws \Exception
+     */
+    public function empty()
+    {
+        $deleteCount = (new TrashCan())->empty();
+
+        $this->logActivity(ActivityType::RECYCLE_BIN_EMPTY);
+        $this->showSuccessNotification(trans('settings.recycle_bin_destroy_notification', ['count' => $deleteCount]));
+        return redirect($this->recycleBinBaseUrl);
+    }
+}
similarity index 89%
rename from app/Http/Controllers/PermissionController.php
rename to app/Http/Controllers/RoleController.php
index 1200d44ab69092ddcd3e498a2abf9ac961386f7a..e16a724a48c49fa419506a997ace27310af607b1 100644 (file)
@@ -6,7 +6,7 @@ use Exception;
 use Illuminate\Http\Request;
 use Illuminate\Validation\ValidationException;
 
-class PermissionController extends Controller
+class RoleController extends Controller
 {
 
     protected $permissionsRepo;
@@ -17,13 +17,12 @@ class PermissionController extends Controller
     public function __construct(PermissionsRepo $permissionsRepo)
     {
         $this->permissionsRepo = $permissionsRepo;
-        parent::__construct();
     }
 
     /**
      * Show a listing of the roles in the system.
      */
-    public function listRoles()
+    public function list()
     {
         $this->checkPermission('user-roles-manage');
         $roles = $this->permissionsRepo->getAllRoles();
@@ -33,7 +32,7 @@ class PermissionController extends Controller
     /**
      * Show the form to create a new role
      */
-    public function createRole()
+    public function create()
     {
         $this->checkPermission('user-roles-manage');
         return view('settings.roles.create');
@@ -42,7 +41,7 @@ class PermissionController extends Controller
     /**
      * Store a new role in the system.
      */
-    public function storeRole(Request $request)
+    public function store(Request $request)
     {
         $this->checkPermission('user-roles-manage');
         $this->validate($request, [
@@ -59,7 +58,7 @@ class PermissionController extends Controller
      * Show the form for editing a user role.
      * @throws PermissionsException
      */
-    public function editRole(string $id)
+    public function edit(string $id)
     {
         $this->checkPermission('user-roles-manage');
         $role = $this->permissionsRepo->getRoleById($id);
@@ -73,7 +72,7 @@ class PermissionController extends Controller
      * Updates a user role.
      * @throws ValidationException
      */
-    public function updateRole(Request $request, string $id)
+    public function update(Request $request, string $id)
     {
         $this->checkPermission('user-roles-manage');
         $this->validate($request, [
@@ -90,7 +89,7 @@ class PermissionController extends Controller
      * Show the view to delete a role.
      * Offers the chance to migrate users.
      */
-    public function showDeleteRole(string $id)
+    public function showDelete(string $id)
     {
         $this->checkPermission('user-roles-manage');
         $role = $this->permissionsRepo->getRoleById($id);
@@ -105,7 +104,7 @@ class PermissionController extends Controller
      * Migrate from a previous role if set.
      * @throws Exception
      */
-    public function deleteRole(Request $request, string $id)
+    public function delete(Request $request, string $id)
     {
         $this->checkPermission('user-roles-manage');
 
index 8105843b576acb9072651c878190a5489968f7b4..21ebea378c06c11f897046575a3fe76a3d0185f9 100644 (file)
@@ -1,32 +1,29 @@
 <?php namespace BookStack\Http\Controllers;
 
 use BookStack\Actions\ViewService;
-use BookStack\Entities\Book;
-use BookStack\Entities\Bookshelf;
-use BookStack\Entities\Entity;
-use BookStack\Entities\Managers\EntityContext;
-use BookStack\Entities\SearchService;
-use BookStack\Entities\SearchOptions;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Bookshelf;
+use BookStack\Entities\Models\Entity;
+use BookStack\Entities\Tools\SearchRunner;
+use BookStack\Entities\Tools\ShelfContext;
+use BookStack\Entities\Tools\SearchOptions;
+use BookStack\Entities\Tools\SiblingFetcher;
 use Illuminate\Http\Request;
 
 class SearchController extends Controller
 {
     protected $viewService;
-    protected $searchService;
+    protected $searchRunner;
     protected $entityContextManager;
 
-    /**
-     * SearchController constructor.
-     */
     public function __construct(
         ViewService $viewService,
-        SearchService $searchService,
-        EntityContext $entityContextManager
+        SearchRunner $searchRunner,
+        ShelfContext $entityContextManager
     ) {
         $this->viewService = $viewService;
-        $this->searchService = $searchService;
+        $this->searchRunner = $searchRunner;
         $this->entityContextManager = $entityContextManager;
-        parent::__construct();
     }
 
     /**
@@ -41,7 +38,7 @@ class SearchController extends Controller
         $page = intval($request->get('page', '0')) ?: 1;
         $nextPageLink = url('/search?term=' . urlencode($fullSearchString) . '&page=' . ($page+1));
 
-        $results = $this->searchService->searchEntities($searchOpts, 'all', $page, 20);
+        $results = $this->searchRunner->searchEntities($searchOpts, 'all', $page, 20);
 
         return view('search.all', [
             'entities'   => $results['results'],
@@ -53,14 +50,13 @@ class SearchController extends Controller
         ]);
     }
 
-
     /**
      * Searches all entities within a book.
      */
     public function searchBook(Request $request, int $bookId)
     {
         $term = $request->get('term', '');
-        $results = $this->searchService->searchBook($bookId, $term);
+        $results = $this->searchRunner->searchBook($bookId, $term);
         return view('partials.entity-list', ['entities' => $results]);
     }
 
@@ -70,7 +66,7 @@ class SearchController extends Controller
     public function searchChapter(Request $request, int $chapterId)
     {
         $term = $request->get('term', '');
-        $results = $this->searchService->searchChapter($chapterId, $term);
+        $results = $this->searchRunner->searchChapter($chapterId, $term);
         return view('partials.entity-list', ['entities' => $results]);
     }
 
@@ -87,7 +83,7 @@ class SearchController extends Controller
         // Search for entities otherwise show most popular
         if ($searchTerm !== false) {
             $searchTerm .= ' {type:'. implode('|', $entityTypes) .'}';
-            $entities = $this->searchService->searchEntities(SearchOptions::fromString($searchTerm), 'all', 1, 20, $permission)['results'];
+            $entities = $this->searchRunner->searchEntities(SearchOptions::fromString($searchTerm), 'all', 1, 20, $permission)['results'];
         } else {
             $entities = $this->viewService->getPopular(20, 0, $entityTypes, $permission);
         }
@@ -103,39 +99,7 @@ class SearchController extends Controller
         $type = $request->get('entity_type', null);
         $id = $request->get('entity_id', null);
 
-        $entity = Entity::getEntityInstance($type)->newQuery()->visible()->find($id);
-        if (!$entity) {
-            return $this->jsonError(trans('errors.entity_not_found'), 404);
-        }
-
-        $entities = [];
-
-        // Page in chapter
-        if ($entity->isA('page') && $entity->chapter) {
-            $entities = $entity->chapter->getVisiblePages();
-        }
-
-        // Page in book or chapter
-        if (($entity->isA('page') && !$entity->chapter) || $entity->isA('chapter')) {
-            $entities = $entity->book->getDirectChildren();
-        }
-
-        // Book
-        // Gets just the books in a shelf if shelf is in context
-        if ($entity->isA('book')) {
-            $contextShelf = $this->entityContextManager->getContextualShelfForBook($entity);
-            if ($contextShelf) {
-                $entities = $contextShelf->visibleBooks()->get();
-            } else {
-                $entities = Book::visible()->get();
-            }
-        }
-
-        // Shelve
-        if ($entity->isA('bookshelf')) {
-            $entities = Bookshelf::visible()->get();
-        }
-
+        $entities = (new SiblingFetcher)->fetch($type, $id);
         return view('partials.entity-list-basic', ['entities' => $entities, 'style' => 'compact']);
     }
 }
index 50d91d3881e39362908bfe30aaa3a8168042355b..f02f541bc9ba5b1a9ad14270d900ae053b23900d 100644 (file)
@@ -1,5 +1,6 @@
 <?php namespace BookStack\Http\Controllers;
 
+use BookStack\Actions\ActivityType;
 use BookStack\Auth\User;
 use BookStack\Uploads\ImageRepo;
 use Illuminate\Http\Request;
@@ -14,7 +15,6 @@ class SettingController extends Controller
     public function __construct(ImageRepo $imageRepo)
     {
         $this->imageRepo = $imageRepo;
-        parent::__construct();
     }
 
     /**
@@ -47,10 +47,10 @@ class SettingController extends Controller
 
         // Cycles through posted settings and update them
         foreach ($request->all() as $name => $value) {
+            $key = str_replace('setting-', '', trim($name));
             if (strpos($name, 'setting-') !== 0) {
                 continue;
             }
-            $key = str_replace('setting-', '', trim($name));
             setting()->put($key, $value);
         }
 
@@ -68,8 +68,10 @@ class SettingController extends Controller
             setting()->remove('app-logo');
         }
 
+        $section = $request->get('section', '');
+        $this->logActivity(ActivityType::SETTINGS_UPDATE, $section);
         $this->showSuccessNotification(trans('settings.settings_save_success'));
-        $redirectLocation = '/settings#' . $request->get('section', '');
+        $redirectLocation = '/settings#' . $section;
         return redirect(rtrim($redirectLocation, '#'));
     }
 }
index 8c6d6748fa5b79d41090e56f8fd1b1c73dab57c4..ce84bf4101e4c23f6437915a35c3d014a7c306a5 100644 (file)
@@ -14,7 +14,6 @@ class TagController extends Controller
     public function __construct(TagRepo $tagRepo)
     {
         $this->tagRepo = $tagRepo;
-        parent::__construct();
     }
 
     /**
index 55675233c38af9552175d69b0eccb561e0034080..ab0e9069e7c2db27fd310308d39c2be6bc14d537 100644 (file)
@@ -1,9 +1,9 @@
 <?php namespace BookStack\Http\Controllers;
 
+use BookStack\Actions\ActivityType;
 use BookStack\Api\ApiToken;
 use BookStack\Auth\User;
 use Illuminate\Http\Request;
-use Illuminate\Support\Carbon;
 use Illuminate\Support\Facades\Hash;
 use Illuminate\Support\Str;
 
@@ -57,6 +57,8 @@ class UserApiTokenController extends Controller
 
         session()->flash('api-token-secret:' . $token->id, $secret);
         $this->showSuccessNotification(trans('settings.user_api_token_create_success'));
+        $this->logActivity(ActivityType::API_TOKEN_CREATE, $token);
+
         return redirect($user->getEditUrl('/api-tokens/' . $token->id));
     }
 
@@ -93,6 +95,7 @@ class UserApiTokenController extends Controller
         ])->save();
 
         $this->showSuccessNotification(trans('settings.user_api_token_update_success'));
+        $this->logActivity(ActivityType::API_TOKEN_UPDATE, $token);
         return redirect($user->getEditUrl('/api-tokens/' . $token->id));
     }
 
@@ -117,6 +120,8 @@ class UserApiTokenController extends Controller
         $token->delete();
 
         $this->showSuccessNotification(trans('settings.user_api_token_delete_success'));
+        $this->logActivity(ActivityType::API_TOKEN_DELETE, $token);
+
         return redirect($user->getEditUrl('#api_tokens'));
     }
 
index 651dedc0d855d5f54ecc2aba6b2def4e2feced1c..852d507c1dcea5ed16b53ea7a9d306107a74c0ff 100644 (file)
@@ -1,5 +1,6 @@
 <?php namespace BookStack\Http\Controllers;
 
+use BookStack\Actions\ActivityType;
 use BookStack\Auth\Access\SocialAuthService;
 use BookStack\Auth\Access\UserInviteService;
 use BookStack\Auth\User;
@@ -26,7 +27,6 @@ class UserController extends Controller
         $this->userRepo = $userRepo;
         $this->inviteService = $inviteService;
         $this->imageRepo = $imageRepo;
-        parent::__construct();
     }
 
     /**
@@ -102,6 +102,7 @@ class UserController extends Controller
 
         $this->userRepo->downloadAndAssignUserAvatar($user);
 
+        $this->logActivity(ActivityType::USER_CREATE, $user);
         return redirect('/settings/users');
     }
 
@@ -187,13 +188,14 @@ class UserController extends Controller
             $user->image_id = $image->id;
         }
 
-        // Delete the profile image if set to
+        // Delete the profile image if reset option is in request
         if ($request->has('profile_image_reset')) {
             $this->imageRepo->destroyImage($user->avatar);
         }
 
         $user->save();
         $this->showSuccessNotification(trans('settings.users_edit_success'));
+        $this->logActivity(ActivityType::USER_UPDATE, $user);
 
         $redirectUrl = userCan('users-manage') ? '/settings/users' : ('/settings/users/' . $user->id);
         return redirect($redirectUrl);
@@ -215,12 +217,13 @@ class UserController extends Controller
      * Remove the specified user from storage.
      * @throws \Exception
      */
-    public function destroy(int $id)
+    public function destroy(Request $request, int $id)
     {
         $this->preventAccessInDemoMode();
         $this->checkPermissionOrCurrentUser('users-manage', $id);
 
         $user = $this->userRepo->getById($id);
+        $newOwnerId = $request->get('new_owner_id', null);
 
         if ($this->userRepo->isOnlyAdmin($user)) {
             $this->showErrorNotification(trans('errors.users_cannot_delete_only_admin'));
@@ -232,8 +235,9 @@ class UserController extends Controller
             return redirect($user->getEditUrl());
         }
 
-        $this->userRepo->destroy($user);
+        $this->userRepo->destroy($user, $newOwnerId);
         $this->showSuccessNotification(trans('settings.users_delete_success'));
+        $this->logActivity(ActivityType::USER_DELETE, $user);
 
         return redirect('/settings/users');
     }
diff --git a/app/Http/Controllers/UserSearchController.php b/app/Http/Controllers/UserSearchController.php
new file mode 100644 (file)
index 0000000..a0dfbd8
--- /dev/null
@@ -0,0 +1,31 @@
+<?php
+
+namespace BookStack\Http\Controllers;
+
+use BookStack\Auth\User;
+use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Http\Request;
+
+class UserSearchController extends Controller
+{
+    /**
+     * Search users in the system, with the response formatted
+     * for use in a select-style list.
+     */
+    public function forSelect(Request $request)
+    {
+        $search = $request->get('search', '');
+        $query = User::query()->orderBy('name', 'desc')
+            ->take(20);
+
+        if (!empty($search)) {
+            $query->where(function (Builder $query) use ($search) {
+                $query->where('email', 'like', '%' . $search . '%')
+                    ->orWhere('name', 'like', '%' . $search . '%');
+            });
+        }
+
+        $users = $query->get();
+        return view('components.user-select-list', compact('users'));
+    }
+}
index a0c45ea896acdb18aed235ec7b51b14133c5a32f..532942f23e5b8f4b396fdd6601c002ddcf4390fd 100644 (file)
@@ -22,6 +22,7 @@ class Kernel extends HttpKernel
      */
     protected $middlewareGroups = [
         'web' => [
+            \BookStack\Http\Middleware\ControlIframeSecurity::class,
             \BookStack\Http\Middleware\EncryptCookies::class,
             \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
             \Illuminate\Session\Middleware\StartSession::class,
diff --git a/app/Http/Middleware/ControlIframeSecurity.php b/app/Http/Middleware/ControlIframeSecurity.php
new file mode 100644 (file)
index 0000000..cc80344
--- /dev/null
@@ -0,0 +1,36 @@
+<?php
+
+namespace BookStack\Http\Middleware;
+
+use Closure;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Sets CSP headers to restrict the hosts that BookStack can be
+ * iframed within. Also adjusts the cookie samesite options
+ * so that cookies will operate in the third-party context.
+ */
+class ControlIframeSecurity
+{
+    /**
+     * Handle an incoming request.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  \Closure  $next
+     * @return mixed
+     */
+    public function handle($request, Closure $next)
+    {
+        $iframeHosts = collect(explode(' ', config('app.iframe_hosts', '')))->filter();
+        if ($iframeHosts->count() > 0) {
+            config()->set('session.same_site', 'none');
+        }
+
+        $iframeHosts->prepend("'self'");
+
+        $response = $next($request);
+        $cspValue = 'frame-ancestors ' . $iframeHosts->join(' ');
+        $response->headers->set('Content-Security-Policy', $cspValue);
+        return $response;
+    }
+}
index c0ac7a7c4ec781ccfc1939493756c695e4f7ad6d..6a8ec237dd0b47b5a3be5cb7272dc456fecd33f7 100644 (file)
@@ -9,13 +9,11 @@ class Localization
 
     /**
      * Array of right-to-left locales
-     * @var array
      */
     protected $rtlLocales = ['ar', 'he'];
 
     /**
      * Map of BookStack locale names to best-estimate system locale names.
-     * @var array
      */
     protected $localeMap = [
         'ar' => 'ar',
@@ -32,6 +30,7 @@ class Localization
         'ja' => 'ja',
         'ko' => 'ko_KR',
         'nl' => 'nl_NL',
+        'nb' => 'nb_NO',
         'pl' => 'pl_PL',
         'pt' => 'pl_PT',
         'pt_BR' => 'pt_BR',
diff --git a/app/Interfaces/Loggable.php b/app/Interfaces/Loggable.php
new file mode 100644 (file)
index 0000000..33e1d7c
--- /dev/null
@@ -0,0 +1,11 @@
+<?php
+
+namespace BookStack\Interfaces;
+
+interface Loggable
+{
+    /**
+     * Get the string descriptor for this item.
+     */
+    public function logDescriptor(): string;
+}
\ No newline at end of file
diff --git a/app/Ownable.php b/app/Ownable.php
deleted file mode 100644 (file)
index bf24fad..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-<?php namespace BookStack;
-
-use BookStack\Auth\User;
-
-/**
- * @property int created_by
- * @property int updated_by
- */
-abstract class Ownable extends Model
-{
-    /**
-     * Relation for the user that created this entity.
-     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
-     */
-    public function createdBy()
-    {
-        return $this->belongsTo(User::class, 'created_by');
-    }
-
-    /**
-     * Relation for the user that updated this entity.
-     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
-     */
-    public function updatedBy()
-    {
-        return $this->belongsTo(User::class, 'updated_by');
-    }
-
-    /**
-     * Gets the class name.
-     * @return string
-     */
-    public static function getClassName()
-    {
-        return strtolower(array_slice(explode('\\', static::class), -1, 1)[0]);
-    }
-}
index f418153997286e5636e754279be147e753171ec7..1c6180a1f4b3c3329bbb28632b4bca076cd19b8e 100644 (file)
@@ -1,11 +1,11 @@
 <?php namespace BookStack\Providers;
 
 use Blade;
-use BookStack\Entities\Book;
-use BookStack\Entities\Bookshelf;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Bookshelf;
 use BookStack\Entities\BreadcrumbsViewComposer;
-use BookStack\Entities\Chapter;
-use BookStack\Entities\Page;
+use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Models\Page;
 use BookStack\Settings\Setting;
 use BookStack\Settings\SettingService;
 use Illuminate\Database\Eloquent\Relations\Relation;
@@ -13,7 +13,6 @@ use Illuminate\Support\Facades\View;
 use Illuminate\Support\ServiceProvider;
 use Schema;
 use URL;
-use Validator;
 
 class AppServiceProvider extends ServiceProvider
 {
@@ -32,37 +31,11 @@ class AppServiceProvider extends ServiceProvider
             URL::forceScheme($isHttps ? 'https' : 'http');
         }
 
-        // Custom validation methods
-        Validator::extend('image_extension', function ($attribute, $value, $parameters, $validator) {
-            $validImageExtensions = ['png', 'jpg', 'jpeg', 'gif', 'webp'];
-            return in_array(strtolower($value->getClientOriginalExtension()), $validImageExtensions);
-        });
-
-        Validator::extend('no_double_extension', function ($attribute, $value, $parameters, $validator) {
-            $uploadName = $value->getClientOriginalName();
-            return substr_count($uploadName, '.') < 2;
-        });
-
-        Validator::extend('safe_url', function ($attribute, $value, $parameters, $validator) {
-            $cleanLinkName = strtolower(trim($value));
-            $isJs = strpos($cleanLinkName, 'javascript:') === 0;
-            $isData = strpos($cleanLinkName, 'data:') === 0;
-            return !$isJs && !$isData;
-        });
-
         // Custom blade view directives
         Blade::directive('icon', function ($expression) {
             return "<?php echo icon($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" .
-                "<?php endforeach; ?>" .
-                '<?php $__env->stopPush(); ?>';
-        });
-
         // Allow longer string lengths after upgrade to utf8mb4
         Schema::defaultStringLength(191);
 
diff --git a/app/Providers/CustomValidationServiceProvider.php b/app/Providers/CustomValidationServiceProvider.php
new file mode 100644 (file)
index 0000000..4a5272b
--- /dev/null
@@ -0,0 +1,33 @@
+<?php
+
+namespace BookStack\Providers;
+
+use Illuminate\Support\Facades\Validator;
+use Illuminate\Support\ServiceProvider;
+
+class CustomValidationServiceProvider extends ServiceProvider
+{
+
+    /**
+     * Register our custom validation rules when the application boots.
+     */
+    public function boot(): void
+    {
+        Validator::extend('image_extension', function ($attribute, $value, $parameters, $validator) {
+            $validImageExtensions = ['png', 'jpg', 'jpeg', 'gif', 'webp'];
+            return in_array(strtolower($value->getClientOriginalExtension()), $validImageExtensions);
+        });
+
+        Validator::extend('no_double_extension', function ($attribute, $value, $parameters, $validator) {
+            $uploadName = $value->getClientOriginalName();
+            return substr_count($uploadName, '.') < 2;
+        });
+
+        Validator::extend('safe_url', function ($attribute, $value, $parameters, $validator) {
+            $cleanLinkName = strtolower(trim($value));
+            $isJs = strpos($cleanLinkName, 'javascript:') === 0;
+            $isData = strpos($cleanLinkName, 'data:') === 0;
+            return !$isJs && !$isData;
+        });
+    }
+}
diff --git a/app/Traits/HasCreatorAndUpdater.php b/app/Traits/HasCreatorAndUpdater.php
new file mode 100644 (file)
index 0000000..ad6c303
--- /dev/null
@@ -0,0 +1,28 @@
+<?php namespace BookStack\Traits;
+
+use BookStack\Auth\User;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
+
+/**
+ * @property int created_by
+ * @property int updated_by
+ */
+trait HasCreatorAndUpdater
+{
+    /**
+     * Relation for the user that created this entity.
+     */
+    public function createdBy(): BelongsTo
+    {
+        return $this->belongsTo(User::class, 'created_by');
+    }
+
+    /**
+     * Relation for the user that updated this entity.
+     */
+    public function updatedBy(): BelongsTo
+    {
+        return $this->belongsTo(User::class, 'updated_by');
+    }
+
+}
diff --git a/app/Traits/HasOwner.php b/app/Traits/HasOwner.php
new file mode 100644 (file)
index 0000000..9d1eb3d
--- /dev/null
@@ -0,0 +1,19 @@
+<?php namespace BookStack\Traits;
+
+use BookStack\Auth\User;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
+
+/**
+ * @property int owned_by
+ */
+trait HasOwner
+{
+    /**
+     * Relation for the user that owns this entity.
+     */
+    public function ownedBy(): BelongsTo
+    {
+        return $this->belongsTo(User::class, 'owned_by');
+    }
+
+}
index 66c032be587dca8c0a41c606df510c6e74d14479..d1060477d085d3cda5c23b7363c5d54067de7b3d 100644 (file)
@@ -1,7 +1,8 @@
 <?php namespace BookStack\Uploads;
 
-use BookStack\Entities\Page;
-use BookStack\Ownable;
+use BookStack\Entities\Models\Page;
+use BookStack\Model;
+use BookStack\Traits\HasCreatorAndUpdater;
 
 /**
  * @property int id
@@ -10,8 +11,10 @@ use BookStack\Ownable;
  * @property string extension
  * @property bool external
  */
-class Attachment extends Ownable
+class Attachment extends Model
 {
+    use HasCreatorAndUpdater;
+
     protected $fillable = ['name', 'order'];
 
     /**
index e85901e17c7d34fae1e9967b27c119f824a708dc..b14f49473709647e23fd03afeea406d39c752361 100644 (file)
@@ -2,17 +2,29 @@
 
 use BookStack\Exceptions\FileUploadException;
 use Exception;
+use Illuminate\Contracts\Filesystem\Factory as FileSystem;
+use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
 use Illuminate\Support\Str;
 use Symfony\Component\HttpFoundation\File\UploadedFile;
 
-class AttachmentService extends UploadService
+class AttachmentService
 {
 
+    protected $fileSystem;
+
+    /**
+     * AttachmentService constructor.
+     */
+    public function __construct(FileSystem $fileSystem)
+    {
+        $this->fileSystem = $fileSystem;
+    }
+
+
     /**
      * Get the storage that will be used for storing files.
-     * @return \Illuminate\Contracts\Filesystem\Filesystem
      */
-    protected function getStorage()
+    protected function getStorage(): FileSystemInstance
     {
         $storageType = config('filesystems.attachments');
 
index c76979d7cab0c5bee668b3e6a993d781842aa77c..dc26af002ab5e29de70d3679d9a56282b39bb458 100644 (file)
@@ -1,11 +1,13 @@
 <?php namespace BookStack\Uploads;
 
-use BookStack\Entities\Page;
-use BookStack\Ownable;
+use BookStack\Entities\Models\Page;
+use BookStack\Model;
+use BookStack\Traits\HasCreatorAndUpdater;
 use Images;
 
-class Image extends Ownable
+class Image extends Model
 {
+    use HasCreatorAndUpdater;
 
     protected $fillable = ['name'];
     protected $hidden = [];
index a0855508594b7c336252bc4d9856c5eb01db20ec..b4d743b73447a2cc99dade9b367f78e489020279 100644 (file)
@@ -1,7 +1,7 @@
 <?php namespace BookStack\Uploads;
 
 use BookStack\Auth\Permissions\PermissionService;
-use BookStack\Entities\Page;
+use BookStack\Entities\Models\Page;
 use BookStack\Exceptions\ImageUploadException;
 use Exception;
 use Illuminate\Database\Eloquent\Builder;
@@ -112,7 +112,7 @@ class ImageRepo
                 if ($filterType === 'page') {
                     $query->where('uploaded_to', '=', $contextPage->id);
                 } elseif ($filterType === 'book') {
-                    $validPageIds = $contextPage->book->pages()->get(['id'])->pluck('id')->toArray();
+                    $validPageIds = $contextPage->book->pages()->visible()->get(['id'])->pluck('id')->toArray();
                     $query->whereIn('uploaded_to', $validPageIds);
                 }
             };
index 89744386d60208d9a85bf0ed1562c79af8a963d0..92c3994a71e0386a792951dac75b6ad5542b9ec7 100644 (file)
@@ -1,50 +1,41 @@
 <?php namespace BookStack\Uploads;
 
-use BookStack\Auth\User;
-use BookStack\Exceptions\HttpFetchException;
 use BookStack\Exceptions\ImageUploadException;
 use DB;
+use ErrorException;
 use Exception;
 use Illuminate\Contracts\Cache\Repository as Cache;
 use Illuminate\Contracts\Filesystem\Factory as FileSystem;
+use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
+use Illuminate\Contracts\Filesystem\FileNotFoundException;
 use Illuminate\Support\Str;
 use Intervention\Image\Exception\NotSupportedException;
 use Intervention\Image\ImageManager;
-use phpDocumentor\Reflection\Types\Integer;
 use Symfony\Component\HttpFoundation\File\UploadedFile;
 
-class ImageService extends UploadService
+class ImageService
 {
-
     protected $imageTool;
     protected $cache;
     protected $storageUrl;
     protected $image;
-    protected $http;
+    protected $fileSystem;
 
     /**
      * ImageService constructor.
-     * @param Image $image
-     * @param ImageManager $imageTool
-     * @param FileSystem $fileSystem
-     * @param Cache $cache
-     * @param HttpFetcher $http
      */
-    public function __construct(Image $image, ImageManager $imageTool, FileSystem $fileSystem, Cache $cache, HttpFetcher $http)
+    public function __construct(Image $image, ImageManager $imageTool, FileSystem $fileSystem, Cache $cache)
     {
         $this->image = $image;
         $this->imageTool = $imageTool;
+        $this->fileSystem = $fileSystem;
         $this->cache = $cache;
-        $this->http = $http;
-        parent::__construct($fileSystem);
     }
 
     /**
      * Get the storage that will be used for storing images.
-     * @param string $type
-     * @return \Illuminate\Contracts\Filesystem\Filesystem
      */
-    protected function getStorage($type = '')
+    protected function getStorage(string $type = ''): FileSystemInstance
     {
         $storageType = config('filesystems.images');
 
@@ -58,12 +49,6 @@ class ImageService extends UploadService
 
     /**
      * Saves a new image from an upload.
-     * @param UploadedFile $uploadedFile
-     * @param string $type
-     * @param int $uploadedTo
-     * @param int|null $resizeWidth
-     * @param int|null $resizeHeight
-     * @param bool $keepRatio
      * @return mixed
      * @throws ImageUploadException
      */
@@ -87,14 +72,9 @@ class ImageService extends UploadService
 
     /**
      * Save a new image from a uri-encoded base64 string of data.
-     * @param string $base64Uri
-     * @param string $name
-     * @param string $type
-     * @param int $uploadedTo
-     * @return Image
      * @throws ImageUploadException
      */
-    public function saveNewFromBase64Uri(string $base64Uri, string $name, string $type, $uploadedTo = 0)
+    public function saveNewFromBase64Uri(string $base64Uri, string $name, string $type, int $uploadedTo = 0): Image
     {
         $splitData = explode(';base64,', $base64Uri);
         if (count($splitData) < 2) {
@@ -104,30 +84,11 @@ class ImageService extends UploadService
         return $this->saveNew($name, $data, $type, $uploadedTo);
     }
 
-    /**
-     * Gets an image from url and saves it to the database.
-     * @param             $url
-     * @param string      $type
-     * @param bool|string $imageName
-     * @return mixed
-     * @throws \Exception
-     */
-    private function saveNewFromUrl($url, $type, $imageName = false)
-    {
-        $imageName = $imageName ? $imageName : basename($url);
-        try {
-            $imageData = $this->http->fetch($url);
-        } catch (HttpFetchException $exception) {
-            throw new \Exception(trans('errors.cannot_get_image_from_url', ['url' => $url]));
-        }
-        return $this->saveNew($imageName, $imageData, $type);
-    }
-
     /**
      * Save a new image into storage.
      * @throws ImageUploadException
      */
-    private function saveNew(string $imageName, string $imageData, string $type, int $uploadedTo = 0): Image
+    public function saveNew(string $imageName, string $imageData, string $type, int $uploadedTo = 0): Image
     {
         $storage = $this->getStorage($type);
         $secureUploads = setting('app-secure-images');
@@ -152,10 +113,10 @@ class ImageService extends UploadService
         }
 
         $imageDetails = [
-            'name'       => $imageName,
-            'path'       => $fullPath,
-            'url'        => $this->getPublicUrl($fullPath),
-            'type'       => $type,
+            'name' => $imageName,
+            'path' => $fullPath,
+            'url' => $this->getPublicUrl($fullPath),
+            'type' => $type,
             'uploaded_to' => $uploadedTo
         ];
 
@@ -185,15 +146,13 @@ class ImageService extends UploadService
             $name = Str::random(10);
         }
 
-        return  $name . '.' . $extension;
+        return $name . '.' . $extension;
     }
 
     /**
      * Checks if the image is a gif. Returns true if it is, else false.
-     * @param Image $image
-     * @return boolean
      */
-    protected function isGif(Image $image)
+    protected function isGif(Image $image): bool
     {
         return strtolower(pathinfo($image->path, PATHINFO_EXTENSION)) === 'gif';
     }
@@ -253,7 +212,7 @@ class ImageService extends UploadService
         try {
             $thumb = $this->imageTool->make($imageData);
         } catch (Exception $e) {
-            if ($e instanceof \ErrorException || $e instanceof NotSupportedException) {
+            if ($e instanceof ErrorException || $e instanceof NotSupportedException) {
                 throw new ImageUploadException(trans('errors.cannot_create_thumbs'));
             }
             throw $e;
@@ -281,11 +240,9 @@ class ImageService extends UploadService
 
     /**
      * Get the raw data content from an image.
-     * @param Image $image
-     * @return string
-     * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
+     * @throws FileNotFoundException
      */
-    public function getImageData(Image $image)
+    public function getImageData(Image $image): string
     {
         $imagePath = $image->path;
         $storage = $this->getStorage();
@@ -294,7 +251,6 @@ class ImageService extends UploadService
 
     /**
      * Destroy an image along with its revisions, thumbnails and remaining folders.
-     * @param Image $image
      * @throws Exception
      */
     public function destroy(Image $image)
@@ -324,7 +280,7 @@ class ImageService extends UploadService
         // Cleanup of empty folders
         $foldersInvolved = array_merge([$imageFolder], $storage->directories($imageFolder));
         foreach ($foldersInvolved as $directory) {
-            if ($this->isFolderEmpty($directory)) {
+            if ($this->isFolderEmpty($storage, $directory)) {
                 $storage->deleteDirectory($directory);
             }
         }
@@ -333,57 +289,13 @@ class ImageService extends UploadService
     }
 
     /**
-     * Save an avatar image from an external service.
-     * @param \BookStack\Auth\User $user
-     * @param int $size
-     * @return Image
-     * @throws Exception
-     */
-    public function saveUserAvatar(User $user, $size = 500)
-    {
-        $avatarUrl = $this->getAvatarUrl();
-        $email = strtolower(trim($user->email));
-
-        $replacements = [
-            '${hash}' => md5($email),
-            '${size}' => $size,
-            '${email}' => urlencode($email),
-        ];
-
-        $userAvatarUrl = strtr($avatarUrl, $replacements);
-        $imageName = str_replace(' ', '-', $user->name . '-avatar.png');
-        $image = $this->saveNewFromUrl($userAvatarUrl, 'user', $imageName);
-        $image->created_by = $user->id;
-        $image->updated_by = $user->id;
-        $image->uploaded_to = $user->id;
-        $image->save();
-
-        return $image;
-    }
-
-    /**
-     * Check if fetching external avatars is enabled.
-     * @return bool
-     */
-    public function avatarFetchEnabled()
-    {
-        $fetchUrl = $this->getAvatarUrl();
-        return is_string($fetchUrl) && strpos($fetchUrl, 'http') === 0;
-    }
-
-    /**
-     * Get the URL to fetch avatars from.
-     * @return string|mixed
+     * Check whether or not a folder is empty.
      */
-    protected function getAvatarUrl()
+    protected function isFolderEmpty(FileSystemInstance $storage, string $path): bool
     {
-        $url = trim(config('services.avatar_url'));
-
-        if (empty($url) && !config('services.disable_services')) {
-            $url = 'https://www.gravatar.com/avatar/${hash}?s=${size}&d=identicon';
-        }
-
-        return $url;
+        $files = $storage->files($path);
+        $folders = $storage->directories($path);
+        return (count($files) === 0 && count($folders) === 0);
     }
 
     /**
@@ -392,26 +304,23 @@ class ImageService extends UploadService
      * Could be much improved to be more specific but kept it generic for now to be safe.
      *
      * Returns the path of the images that would be/have been deleted.
-     * @param bool $checkRevisions
-     * @param bool $dryRun
-     * @param array $types
-     * @return array
      */
-    public function deleteUnusedImages($checkRevisions = true, $dryRun = true, $types = ['gallery', 'drawio'])
+    public function deleteUnusedImages(bool $checkRevisions = true, bool $dryRun = true)
     {
-        $types = array_intersect($types, ['gallery', 'drawio']);
+        $types = ['gallery', 'drawio'];
         $deletedPaths = [];
 
         $this->image->newQuery()->whereIn('type', $types)
-            ->chunk(1000, function ($images) use ($types, $checkRevisions, &$deletedPaths, $dryRun) {
+            ->chunk(1000, function ($images) use ($checkRevisions, &$deletedPaths, $dryRun) {
                 foreach ($images as $image) {
                     $searchQuery = '%' . basename($image->path) . '%';
                     $inPage = DB::table('pages')
-                         ->where('html', 'like', $searchQuery)->count() > 0;
+                            ->where('html', 'like', $searchQuery)->count() > 0;
+
                     $inRevision = false;
                     if ($checkRevisions) {
-                        $inRevision =  DB::table('page_revisions')
-                             ->where('html', 'like', $searchQuery)->count() > 0;
+                        $inRevision = DB::table('page_revisions')
+                                ->where('html', 'like', $searchQuery)->count() > 0;
                     }
 
                     if (!$inPage && !$inRevision) {
@@ -427,38 +336,25 @@ class ImageService extends UploadService
 
     /**
      * Convert a image URI to a Base64 encoded string.
-     * Attempts to find locally via set storage method first.
-     * @param string $uri
-     * @return null|string
-     * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
+     * Attempts to convert the URL to a system storage url then
+     * fetch the data from the disk or storage location.
+     * Returns null if the image data cannot be fetched from storage.
+     * @throws FileNotFoundException
      */
-    public function imageUriToBase64(string $uri)
+    public function imageUriToBase64(string $uri): ?string
     {
-        $isLocal = strpos(trim($uri), 'http') !== 0;
-
-        // Attempt to find local files even if url not absolute
-        $base = url('/');
-        if (!$isLocal && strpos($uri, $base) === 0) {
-            $isLocal = true;
-            $uri = str_replace($base, '', $uri);
+        $storagePath = $this->imageUrlToStoragePath($uri);
+        if (empty($uri) || is_null($storagePath)) {
+            return null;
         }
 
+        $storage = $this->getStorage();
         $imageData = null;
-
-        if ($isLocal) {
-            $uri = trim($uri, '/');
-            $storage = $this->getStorage();
-            if ($storage->exists($uri)) {
-                $imageData = $storage->get($uri);
-            }
-        } else {
-            try {
-                $imageData = $this->http->fetch($uri);
-            } catch (\Exception $e) {
-            }
+        if ($storage->exists($storagePath)) {
+            $imageData = $storage->get($storagePath);
         }
 
-        if ($imageData === null) {
+        if (is_null($imageData)) {
             return null;
         }
 
@@ -470,12 +366,45 @@ class ImageService extends UploadService
         return 'data:image/' . $extension . ';base64,' . base64_encode($imageData);
     }
 
+    /**
+     * Get a storage path for the given image URL.
+     * Ensures the path will start with "uploads/images".
+     * Returns null if the url cannot be resolved to a local URL.
+     */
+    private function imageUrlToStoragePath(string $url): ?string
+    {
+        $url = ltrim(trim($url), '/');
+
+        // Handle potential relative paths
+        $isRelative = strpos($url, 'http') !== 0;
+        if ($isRelative) {
+            if (strpos(strtolower($url), 'uploads/images') === 0) {
+                return trim($url, '/');
+            }
+            return null;
+        }
+
+        // Handle local images based on paths on the same domain
+        $potentialHostPaths = [
+            url('uploads/images/'),
+            $this->getPublicUrl('/uploads/images/'),
+        ];
+
+        foreach ($potentialHostPaths as $potentialBasePath) {
+            $potentialBasePath = strtolower($potentialBasePath);
+            if (strpos(strtolower($url), $potentialBasePath) === 0) {
+                return 'uploads/images/' . trim(substr($url, strlen($potentialBasePath)), '/');
+            }
+        }
+
+        return null;
+    }
+
     /**
      * Gets a public facing url for an image by checking relevant environment variables.
-     * @param string $filePath
-     * @return string
+     * If s3-style store is in use it will default to guessing a public bucket URL.
      */
-    private function getPublicUrl($filePath)
+    private function getPublicUrl(string $filePath): string
     {
         if ($this->storageUrl === null) {
             $storageUrl = config('filesystems.url');
diff --git a/app/Uploads/UploadService.php b/app/Uploads/UploadService.php
deleted file mode 100644 (file)
index 292e61e..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-<?php namespace BookStack\Uploads;
-
-use Illuminate\Contracts\Filesystem\Factory as FileSystem;
-use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
-
-abstract class UploadService
-{
-
-    /**
-     * @var FileSystem
-     */
-    protected $fileSystem;
-
-
-    /**
-     * FileService constructor.
-     * @param $fileSystem
-     */
-    public function __construct(FileSystem $fileSystem)
-    {
-        $this->fileSystem = $fileSystem;
-    }
-
-    /**
-     * Get the storage that will be used for storing images.
-     * @return FileSystemInstance
-     */
-    protected function getStorage()
-    {
-        $storageType = config('filesystems.default');
-        return $this->fileSystem->disk($storageType);
-    }
-
-    /**
-     * Check whether or not a folder is empty.
-     * @param $path
-     * @return bool
-     */
-    protected function isFolderEmpty($path)
-    {
-        $files = $this->getStorage()->files($path);
-        $folders = $this->getStorage()->directories($path);
-        return (count($files) === 0 && count($folders) === 0);
-    }
-}
diff --git a/app/Uploads/UserAvatars.php b/app/Uploads/UserAvatars.php
new file mode 100644 (file)
index 0000000..92b06bc
--- /dev/null
@@ -0,0 +1,100 @@
+<?php namespace BookStack\Uploads;
+
+use BookStack\Auth\User;
+use BookStack\Exceptions\HttpFetchException;
+use Exception;
+
+class UserAvatars
+{
+    protected $imageService;
+    protected $http;
+
+    public function __construct(ImageService $imageService, HttpFetcher $http)
+    {
+        $this->imageService = $imageService;
+        $this->http = $http;
+    }
+
+    /**
+     * Fetch and assign an avatar image to the given user.
+     */
+    public function fetchAndAssignToUser(User $user): void
+    {
+        if (!$this->avatarFetchEnabled()) {
+            return;
+        }
+
+        try {
+            $avatar = $this->saveAvatarImage($user);
+            $user->avatar()->associate($avatar);
+            $user->save();
+        } catch (Exception $e) {
+            Log::error('Failed to save user avatar image');
+        }
+    }
+
+    /**
+     * Save an avatar image from an external service.
+     * @throws Exception
+     */
+    protected function saveAvatarImage(User $user, int $size = 500): Image
+    {
+        $avatarUrl = $this->getAvatarUrl();
+        $email = strtolower(trim($user->email));
+
+        $replacements = [
+            '${hash}' => md5($email),
+            '${size}' => $size,
+            '${email}' => urlencode($email),
+        ];
+
+        $userAvatarUrl = strtr($avatarUrl, $replacements);
+        $imageName = str_replace(' ', '-', $user->id . '-avatar.png');
+        $imageData = $this->getAvatarImageData($userAvatarUrl);
+
+        $image = $this->imageService->saveNew($imageName, $imageData, 'user', $user->id);
+        $image->created_by = $user->id;
+        $image->updated_by = $user->id;
+        $image->save();
+
+        return $image;
+    }
+
+    /**
+     * Gets an image from url and returns it as a string of image data.
+     * @throws Exception
+     */
+    protected function getAvatarImageData(string $url): string
+    {
+        try {
+            $imageData = $this->http->fetch($url);
+        } catch (HttpFetchException $exception) {
+            throw new Exception(trans('errors.cannot_get_image_from_url', ['url' => $url]));
+        }
+        return $imageData;
+    }
+
+    /**
+     * Check if fetching external avatars is enabled.
+     */
+    protected function avatarFetchEnabled(): bool
+    {
+        $fetchUrl = $this->getAvatarUrl();
+        return is_string($fetchUrl) && strpos($fetchUrl, 'http') === 0;
+    }
+
+    /**
+     * Get the URL to fetch avatars from.
+     */
+    protected function getAvatarUrl(): string
+    {
+        $url = trim(config('services.avatar_url'));
+
+        if (empty($url) && !config('services.disable_services')) {
+            $url = 'https://www.gravatar.com/avatar/${hash}?s=${size}&d=identicon';
+        }
+
+        return $url;
+    }
+
+}
\ No newline at end of file
index 935d4d8daee4a2d4600dc0394e85c28c7184c48e..c090bfd055acc400e1ad4c46e7059b6c19820f60 100644 (file)
@@ -2,7 +2,7 @@
 
 use BookStack\Auth\Permissions\PermissionService;
 use BookStack\Auth\User;
-use BookStack\Ownable;
+use BookStack\Model;
 use BookStack\Settings\SettingService;
 
 /**
@@ -56,7 +56,7 @@ function hasAppAccess(): bool
  * Check if the current user has a permission. If an ownable element
  * is passed in the jointPermissions are checked against that particular item.
  */
-function userCan(string $permission, Ownable $ownable = null): bool
+function userCan(string $permission, Model $ownable = null): bool
 {
     if ($ownable === null) {
         return user() && user()->can($permission);
index 8a2b7d656552e4c05aaad75242f61aba45156594..a58905095355e985d6b98ad81c65d468a3a91217 100644 (file)
@@ -5,44 +5,42 @@
     "license": "MIT",
     "type": "project",
     "require": {
-        "php": "^7.2",
+        "php": "^7.2.5",
         "ext-curl": "*",
         "ext-dom": "*",
         "ext-gd": "*",
         "ext-json": "*",
         "ext-mbstring": "*",
-        "ext-tidy": "*",
         "ext-xml": "*",
-        "barryvdh/laravel-dompdf": "^0.8.6",
-        "barryvdh/laravel-snappy": "^0.4.7",
+        "barryvdh/laravel-dompdf": "^0.8.7",
+        "barryvdh/laravel-snappy": "^0.4.8",
         "doctrine/dbal": "^2.9",
-        "facade/ignition": "^1.4",
-        "fideloper/proxy": "^4.0",
-        "gathercontent/htmldiff": "^0.2.1",
-        "intervention/image": "^2.5",
-        "laravel/framework": "^6.18",
-        "laravel/socialite": "^4.3.2",
-        "league/commonmark": "^1.4",
-        "league/flysystem-aws-s3-v3": "^1.0",
-        "nunomaduro/collision": "^3.0",
+        "facade/ignition": "^1.16.4",
+        "fideloper/proxy": "^4.4.1",
+        "intervention/image": "^2.5.1",
+        "laravel/framework": "^6.20",
+        "laravel/socialite": "^5.1",
+        "league/commonmark": "^1.5",
+        "league/flysystem-aws-s3-v3": "^1.0.29",
+        "nunomaduro/collision": "^3.1",
         "onelogin/php-saml": "^3.3",
-        "predis/predis": "^1.1",
-        "socialiteproviders/discord": "^2.0",
-        "socialiteproviders/gitlab": "^3.0",
-        "socialiteproviders/microsoft-azure": "^3.0",
-        "socialiteproviders/okta": "^1.0",
-        "socialiteproviders/slack": "^3.0",
-        "socialiteproviders/twitch": "^5.0"
+        "predis/predis": "^1.1.6",
+        "socialiteproviders/discord": "^4.1",
+        "socialiteproviders/gitlab": "^4.1",
+        "socialiteproviders/microsoft-azure": "^4.1",
+        "socialiteproviders/okta": "^4.1",
+        "socialiteproviders/slack": "^4.1",
+        "socialiteproviders/twitch": "^5.3",
+        "ssddanbrown/htmldiff": "^1.0"
     },
     "require-dev": {
-        "barryvdh/laravel-debugbar": "^3.2.8",
-        "barryvdh/laravel-ide-helper": "^2.6.4",
-        "fzaninotto/faker": "^1.4",
-        "laravel/browser-kit-testing": "^5.1",
-        "mockery/mockery": "^1.0",
+        "barryvdh/laravel-debugbar": "^3.5.1",
+        "barryvdh/laravel-ide-helper": "^2.8.2",
+        "fakerphp/faker": "^1.9.1",
+        "laravel/browser-kit-testing": "^5.2",
+        "mockery/mockery": "^1.3.3",
         "phpunit/phpunit": "^8.0",
-        "squizlabs/php_codesniffer": "^3.4",
-        "wnx/laravel-stats": "^2.0"
+        "squizlabs/php_codesniffer": "^3.5.8"
     },
     "autoload": {
         "classmap": [
         "post-create-project-cmd": [
             "@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');\""
-        ],
         "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');\""
         ],
         "post-install-cmd": [
             "@php artisan cache:clear",
@@ -94,7 +87,7 @@
         "preferred-install": "dist",
         "sort-packages": true,
         "platform": {
-            "php": "7.2.0"
+            "php": "7.2.5"
         }
     },
     "extra": {
index b22874455b95bc9db5a0fd453ae38938dcaa6d8b..360bd57a65024e65cf0c31d4b28647b1dcb53049 100644 (file)
@@ -4,20 +4,20 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "34390536dd685e0bc49b179babaa06ec",
+    "content-hash": "e89dcb5443300c86da774d0abd956d71",
     "packages": [
         {
             "name": "aws/aws-sdk-php",
-            "version": "3.154.6",
+            "version": "3.171.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/aws/aws-sdk-php.git",
-                "reference": "83a1382930359e4d4f4c9187239f059d5b282520"
+                "reference": "742663a85ec84647f74dea454d2dc45bba180f9d"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/83a1382930359e4d4f4c9187239f059d5b282520",
-                "reference": "83a1382930359e4d4f4c9187239f059d5b282520",
+                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/742663a85ec84647f74dea454d2dc45bba180f9d",
+                "reference": "742663a85ec84647f74dea454d2dc45bba180f9d",
                 "shasum": ""
             },
             "require": {
                 "s3",
                 "sdk"
             ],
-            "time": "2020-09-18T18:16:42+00:00"
+            "support": {
+                "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
+                "issues": "https://github.com/aws/aws-sdk-php/issues",
+                "source": "https://github.com/aws/aws-sdk-php/tree/3.171.2"
+            },
+            "time": "2020-12-18T19:12:13+00:00"
         },
         {
             "name": "barryvdh/laravel-dompdf",
                 "laravel",
                 "pdf"
             ],
+            "support": {
+                "issues": "https://github.com/barryvdh/laravel-dompdf/issues",
+                "source": "https://github.com/barryvdh/laravel-dompdf/tree/master"
+            },
             "funding": [
                 {
                     "url": "https://github.com/barryvdh",
                 "wkhtmltoimage",
                 "wkhtmltopdf"
             ],
-            "time": "2020-09-07T12:33:10+00:00"
-        },
-        {
-            "name": "cogpowered/finediff",
-            "version": "0.3.1",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/cogpowered/FineDiff.git",
-                "reference": "339ddc8c3afb656efed4f2f0a80e5c3d026f8ea8"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/cogpowered/FineDiff/zipball/339ddc8c3afb656efed4f2f0a80e5c3d026f8ea8",
-                "reference": "339ddc8c3afb656efed4f2f0a80e5c3d026f8ea8",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=5.3.0"
-            },
-            "require-dev": {
-                "mockery/mockery": "*",
-                "phpunit/phpunit": "*"
+            "support": {
+                "issues": "https://github.com/barryvdh/laravel-snappy/issues",
+                "source": "https://github.com/barryvdh/laravel-snappy/tree/master"
             },
-            "type": "library",
-            "autoload": {
-                "psr-0": {
-                    "cogpowered\\FineDiff": "src/"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Rob Crowe",
-                    "email": "[email protected]"
-                },
-                {
-                    "name": "Raymond Hill"
-                }
-            ],
-            "description": "PHP implementation of a Fine granularity Diff engine",
-            "homepage": "https://github.com/cogpowered/FineDiff",
-            "keywords": [
-                "diff",
-                "finediff",
-                "opcode",
-                "string",
-                "text"
-            ],
-            "time": "2014-05-19T10:25:02+00:00"
+            "time": "2020-09-07T12:33:10+00:00"
         },
         {
             "name": "doctrine/cache",
                 "redis",
                 "xcache"
             ],
+            "support": {
+                "issues": "https://github.com/doctrine/cache/issues",
+                "source": "https://github.com/doctrine/cache/tree/1.10.x"
+            },
             "funding": [
                 {
                     "url": "https://www.doctrine-project.org/sponsorship.html",
                 "sqlserver",
                 "sqlsrv"
             ],
+            "support": {
+                "issues": "https://github.com/doctrine/dbal/issues",
+                "source": "https://github.com/doctrine/dbal/tree/2.10.4"
+            },
             "funding": [
                 {
                     "url": "https://www.doctrine-project.org/sponsorship.html",
                 "event system",
                 "events"
             ],
+            "support": {
+                "issues": "https://github.com/doctrine/event-manager/issues",
+                "source": "https://github.com/doctrine/event-manager/tree/1.1.x"
+            },
             "funding": [
                 {
                     "url": "https://www.doctrine-project.org/sponsorship.html",
                 "uppercase",
                 "words"
             ],
+            "support": {
+                "issues": "https://github.com/doctrine/inflector/issues",
+                "source": "https://github.com/doctrine/inflector/tree/2.0.x"
+            },
             "funding": [
                 {
                     "url": "https://www.doctrine-project.org/sponsorship.html",
                 "parser",
                 "php"
             ],
+            "support": {
+                "issues": "https://github.com/doctrine/lexer/issues",
+                "source": "https://github.com/doctrine/lexer/tree/1.2.1"
+            },
             "funding": [
                 {
                     "url": "https://www.doctrine-project.org/sponsorship.html",
             ],
             "description": "DOMPDF is a CSS 2.1 compliant HTML to PDF converter",
             "homepage": "https://github.com/dompdf/dompdf",
+            "support": {
+                "issues": "https://github.com/dompdf/dompdf/issues",
+                "source": "https://github.com/dompdf/dompdf/tree/master"
+            },
             "time": "2020-08-30T22:54:22+00:00"
         },
         {
             "name": "dragonmantank/cron-expression",
-            "version": "v2.3.0",
+            "version": "v2.3.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/dragonmantank/cron-expression.git",
-                "reference": "72b6fbf76adb3cf5bc0db68559b33d41219aba27"
+                "reference": "65b2d8ee1f10915efb3b55597da3404f096acba2"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/72b6fbf76adb3cf5bc0db68559b33d41219aba27",
-                "reference": "72b6fbf76adb3cf5bc0db68559b33d41219aba27",
+                "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/65b2d8ee1f10915efb3b55597da3404f096acba2",
+                "reference": "65b2d8ee1f10915efb3b55597da3404f096acba2",
                 "shasum": ""
             },
             "require": {
-                "php": "^7.0"
+                "php": "^7.0|^8.0"
             },
             "require-dev": {
-                "phpunit/phpunit": "^6.4|^7.0"
+                "phpunit/phpunit": "^6.4|^7.0|^8.0|^9.0"
             },
             "type": "library",
             "extra": {
                 "cron",
                 "schedule"
             ],
-            "time": "2019-03-31T00:38:28+00:00"
+            "support": {
+                "issues": "https://github.com/dragonmantank/cron-expression/issues",
+                "source": "https://github.com/dragonmantank/cron-expression/tree/v2.3.1"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/dragonmantank",
+                    "type": "github"
+                }
+            ],
+            "time": "2020-10-13T00:52:37+00:00"
         },
         {
             "name": "egulias/email-validator",
-            "version": "2.1.20",
+            "version": "2.1.24",
             "source": {
                 "type": "git",
                 "url": "https://github.com/egulias/EmailValidator.git",
-                "reference": "f46887bc48db66c7f38f668eb7d6ae54583617ff"
+                "reference": "ca90a3291eee1538cd48ff25163240695bd95448"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/f46887bc48db66c7f38f668eb7d6ae54583617ff",
-                "reference": "f46887bc48db66c7f38f668eb7d6ae54583617ff",
+                "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/ca90a3291eee1538cd48ff25163240695bd95448",
+                "reference": "ca90a3291eee1538cd48ff25163240695bd95448",
                 "shasum": ""
             },
             "require": {
                 "validation",
                 "validator"
             ],
-            "time": "2020-09-06T13:44:32+00:00"
+            "support": {
+                "issues": "https://github.com/egulias/EmailValidator/issues",
+                "source": "https://github.com/egulias/EmailValidator/tree/2.1.24"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/egulias",
+                    "type": "github"
+                }
+            ],
+            "time": "2020-11-14T15:56:27+00:00"
         },
         {
             "name": "facade/flare-client-php",
-            "version": "1.3.6",
+            "version": "1.3.7",
             "source": {
                 "type": "git",
                 "url": "https://github.com/facade/flare-client-php.git",
-                "reference": "451fadf38e9f635e7f8e1f5b3cf5c9eb82f11799"
+                "reference": "fd688d3c06658f2b3b5f7bb19f051ee4ddf02492"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/facade/flare-client-php/zipball/451fadf38e9f635e7f8e1f5b3cf5c9eb82f11799",
-                "reference": "451fadf38e9f635e7f8e1f5b3cf5c9eb82f11799",
+                "url": "https://api.github.com/repos/facade/flare-client-php/zipball/fd688d3c06658f2b3b5f7bb19f051ee4ddf02492",
+                "reference": "fd688d3c06658f2b3b5f7bb19f051ee4ddf02492",
                 "shasum": ""
             },
             "require": {
                 "facade/ignition-contracts": "~1.0",
                 "illuminate/pipeline": "^5.5|^6.0|^7.0|^8.0",
-                "php": "^7.1",
+                "php": "^7.1|^8.0",
                 "symfony/http-foundation": "^3.3|^4.1|^5.0",
                 "symfony/mime": "^3.4|^4.0|^5.1",
                 "symfony/var-dumper": "^3.4|^4.0|^5.0"
                 "flare",
                 "reporting"
             ],
+            "support": {
+                "issues": "https://github.com/facade/flare-client-php/issues",
+                "source": "https://github.com/facade/flare-client-php/tree/1.3.7"
+            },
             "funding": [
                 {
                     "url": "https://github.com/spatie",
                     "type": "github"
                 }
             ],
-            "time": "2020-09-18T06:35:11+00:00"
+            "time": "2020-10-21T16:02:39+00:00"
         },
         {
             "name": "facade/ignition",
-            "version": "1.16.3",
+            "version": "1.16.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/facade/ignition.git",
-                "reference": "19674150bb46a4de0ba138c747f538fe7be11dbc"
+                "reference": "1da1705e7f6b24ed45af05461463228da424e14f"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/facade/ignition/zipball/19674150bb46a4de0ba138c747f538fe7be11dbc",
-                "reference": "19674150bb46a4de0ba138c747f538fe7be11dbc",
+                "url": "https://api.github.com/repos/facade/ignition/zipball/1da1705e7f6b24ed45af05461463228da424e14f",
+                "reference": "1da1705e7f6b24ed45af05461463228da424e14f",
                 "shasum": ""
             },
             "require": {
                 "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",
+                "php": "^7.1|^8.0",
                 "scrivo/highlight.php": "^9.15",
                 "symfony/console": "^3.4 || ^4.0",
                 "symfony/var-dumper": "^3.4 || ^4.0"
             },
             "require-dev": {
-                "friendsofphp/php-cs-fixer": "^2.14",
-                "mockery/mockery": "^1.2",
+                "mockery/mockery": "~1.3.3|^1.4.2",
                 "orchestra/testbench": "^3.5 || ^3.6 || ^3.7 || ^3.8 || ^4.0"
             },
             "suggest": {
                 "laravel",
                 "page"
             ],
-            "time": "2020-07-13T15:54:05+00:00"
+            "support": {
+                "docs": "https://flareapp.io/docs/ignition-for-laravel/introduction",
+                "forum": "https://twitter.com/flareappio",
+                "issues": "https://github.com/facade/ignition/issues",
+                "source": "https://github.com/facade/ignition"
+            },
+            "time": "2020-10-30T13:40:01+00:00"
         },
         {
             "name": "facade/ignition-contracts",
                 "flare",
                 "ignition"
             ],
+            "support": {
+                "issues": "https://github.com/facade/ignition-contracts/issues",
+                "source": "https://github.com/facade/ignition-contracts/tree/1.0.1"
+            },
             "time": "2020-07-14T10:10:28+00:00"
         },
         {
             "name": "fideloper/proxy",
-            "version": "4.4.0",
+            "version": "4.4.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/fideloper/TrustedProxy.git",
-                "reference": "9beebf48a1c344ed67c1d36bb1b8709db7c3c1a8"
+                "reference": "c073b2bd04d1c90e04dc1b787662b558dd65ade0"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/fideloper/TrustedProxy/zipball/9beebf48a1c344ed67c1d36bb1b8709db7c3c1a8",
-                "reference": "9beebf48a1c344ed67c1d36bb1b8709db7c3c1a8",
+                "url": "https://api.github.com/repos/fideloper/TrustedProxy/zipball/c073b2bd04d1c90e04dc1b787662b558dd65ade0",
+                "reference": "c073b2bd04d1c90e04dc1b787662b558dd65ade0",
                 "shasum": ""
             },
             "require": {
-                "illuminate/contracts": "^5.0|^6.0|^7.0|^8.0",
+                "illuminate/contracts": "^5.0|^6.0|^7.0|^8.0|^9.0",
                 "php": ">=5.4.0"
             },
             "require-dev": {
-                "illuminate/http": "^5.0|^6.0|^7.0|^8.0",
+                "illuminate/http": "^5.0|^6.0|^7.0|^8.0|^9.0",
                 "mockery/mockery": "^1.0",
                 "phpunit/phpunit": "^6.0"
             },
                 "proxy",
                 "trusted proxy"
             ],
-            "time": "2020-06-23T01:36:47+00:00"
+            "support": {
+                "issues": "https://github.com/fideloper/TrustedProxy/issues",
+                "source": "https://github.com/fideloper/TrustedProxy/tree/4.4.1"
+            },
+            "time": "2020-10-22T13:48:01+00:00"
         },
         {
             "name": "filp/whoops",
-            "version": "2.7.3",
+            "version": "2.9.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/filp/whoops.git",
-                "reference": "5d5fe9bb3d656b514d455645b3addc5f7ba7714d"
+                "reference": "307fb34a5ab697461ec4c9db865b20ff2fd40771"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/filp/whoops/zipball/5d5fe9bb3d656b514d455645b3addc5f7ba7714d",
-                "reference": "5d5fe9bb3d656b514d455645b3addc5f7ba7714d",
+                "url": "https://api.github.com/repos/filp/whoops/zipball/307fb34a5ab697461ec4c9db865b20ff2fd40771",
+                "reference": "307fb34a5ab697461ec4c9db865b20ff2fd40771",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.5.9 || ^7.0",
+                "php": "^5.5.9 || ^7.0 || ^8.0",
                 "psr/log": "^1.0.1"
             },
             "require-dev": {
                 "mockery/mockery": "^0.9 || ^1.0",
-                "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0",
+                "phpunit/phpunit": "^4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.3",
                 "symfony/var-dumper": "^2.6 || ^3.0 || ^4.0 || ^5.0"
             },
             "suggest": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "2.6-dev"
+                    "dev-master": "2.7-dev"
                 }
             },
             "autoload": {
                 "throwable",
                 "whoops"
             ],
-            "time": "2020-06-14T09:00:00+00:00"
-        },
-        {
-            "name": "gathercontent/htmldiff",
-            "version": "0.2.1",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/gathercontent/htmldiff.git",
-                "reference": "24674a62315f64330134b4a4c5b01a7b59193c93"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/gathercontent/htmldiff/zipball/24674a62315f64330134b4a4c5b01a7b59193c93",
-                "reference": "24674a62315f64330134b4a4c5b01a7b59193c93",
-                "shasum": ""
-            },
-            "require": {
-                "cogpowered/finediff": "0.3.1",
-                "ext-tidy": "*"
-            },
-            "require-dev": {
-                "phpunit/phpunit": "4.*",
-                "squizlabs/php_codesniffer": "1.*"
-            },
-            "type": "library",
-            "autoload": {
-                "psr-0": {
-                    "GatherContent\\Htmldiff": "src/"
-                }
+            "support": {
+                "issues": "https://github.com/filp/whoops/issues",
+                "source": "https://github.com/filp/whoops/tree/2.9.1"
             },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Andrew Cairns",
-                    "email": "[email protected]"
-                },
-                {
-                    "name": "Mathew Chapman",
-                    "email": "[email protected]"
-                },
-                {
-                    "name": "Peter Legierski",
-                    "email": "[email protected]"
-                }
-            ],
-            "description": "Compare two HTML strings",
-            "time": "2015-04-15T15:39:46+00:00"
+            "time": "2020-11-01T12:00:00+00:00"
         },
         {
             "name": "guzzlehttp/guzzle",
-            "version": "6.5.5",
+            "version": "7.2.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/guzzle/guzzle.git",
-                "reference": "9d4290de1cfd701f38099ef7e183b64b4b7b0c5e"
+                "reference": "0aa74dfb41ae110835923ef10a9d803a22d50e79"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/guzzle/guzzle/zipball/9d4290de1cfd701f38099ef7e183b64b4b7b0c5e",
-                "reference": "9d4290de1cfd701f38099ef7e183b64b4b7b0c5e",
+                "url": "https://api.github.com/repos/guzzle/guzzle/zipball/0aa74dfb41ae110835923ef10a9d803a22d50e79",
+                "reference": "0aa74dfb41ae110835923ef10a9d803a22d50e79",
                 "shasum": ""
             },
             "require": {
                 "ext-json": "*",
-                "guzzlehttp/promises": "^1.0",
-                "guzzlehttp/psr7": "^1.6.1",
-                "php": ">=5.5",
-                "symfony/polyfill-intl-idn": "^1.17.0"
+                "guzzlehttp/promises": "^1.4",
+                "guzzlehttp/psr7": "^1.7",
+                "php": "^7.2.5 || ^8.0",
+                "psr/http-client": "^1.0"
+            },
+            "provide": {
+                "psr/http-client-implementation": "1.0"
             },
             "require-dev": {
                 "ext-curl": "*",
-                "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0",
+                "php-http/client-integration-tests": "^3.0",
+                "phpunit/phpunit": "^8.5.5 || ^9.3.5",
                 "psr/log": "^1.1"
             },
             "suggest": {
+                "ext-curl": "Required for CURL handler support",
+                "ext-intl": "Required for Internationalized Domain Name (IDN) support",
                 "psr/log": "Required for using the Log middleware"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "6.5-dev"
+                    "dev-master": "7.1-dev"
                 }
             },
             "autoload": {
                     "name": "Michael Dowling",
                     "email": "[email protected]",
                     "homepage": "https://github.com/mtdowling"
+                },
+                {
+                    "name": "Márk Sági-Kazár",
+                    "email": "[email protected]",
+                    "homepage": "https://sagikazarmark.hu"
                 }
             ],
             "description": "Guzzle is a PHP HTTP client library",
                 "framework",
                 "http",
                 "http client",
+                "psr-18",
+                "psr-7",
                 "rest",
                 "web service"
             ],
-            "time": "2020-06-16T21:01:06+00:00"
+            "support": {
+                "issues": "https://github.com/guzzle/guzzle/issues",
+                "source": "https://github.com/guzzle/guzzle/tree/7.2.0"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/GrahamCampbell",
+                    "type": "github"
+                },
+                {
+                    "url": "https://github.com/Nyholm",
+                    "type": "github"
+                },
+                {
+                    "url": "https://github.com/alexeyshockov",
+                    "type": "github"
+                },
+                {
+                    "url": "https://github.com/gmponos",
+                    "type": "github"
+                }
+            ],
+            "time": "2020-10-10T11:47:56+00:00"
         },
         {
             "name": "guzzlehttp/promises",
-            "version": "v1.3.1",
+            "version": "1.4.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/guzzle/promises.git",
-                "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646"
+                "reference": "60d379c243457e073cff02bc323a2a86cb355631"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/guzzle/promises/zipball/a59da6cf61d80060647ff4d3eb2c03a2bc694646",
-                "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646",
+                "url": "https://api.github.com/repos/guzzle/promises/zipball/60d379c243457e073cff02bc323a2a86cb355631",
+                "reference": "60d379c243457e073cff02bc323a2a86cb355631",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.5.0"
+                "php": ">=5.5"
             },
             "require-dev": {
-                "phpunit/phpunit": "^4.0"
+                "symfony/phpunit-bridge": "^4.4 || ^5.1"
             },
             "type": "library",
             "extra": {
             "keywords": [
                 "promise"
             ],
-            "time": "2016-12-20T10:07:11+00:00"
+            "support": {
+                "issues": "https://github.com/guzzle/promises/issues",
+                "source": "https://github.com/guzzle/promises/tree/1.4.0"
+            },
+            "time": "2020-09-30T07:37:28+00:00"
         },
         {
             "name": "guzzlehttp/psr7",
-            "version": "1.6.1",
+            "version": "1.7.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/guzzle/psr7.git",
-                "reference": "239400de7a173fe9901b9ac7c06497751f00727a"
+                "reference": "53330f47520498c0ae1f61f7e2c90f55690c06a3"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/guzzle/psr7/zipball/239400de7a173fe9901b9ac7c06497751f00727a",
-                "reference": "239400de7a173fe9901b9ac7c06497751f00727a",
+                "url": "https://api.github.com/repos/guzzle/psr7/zipball/53330f47520498c0ae1f61f7e2c90f55690c06a3",
+                "reference": "53330f47520498c0ae1f61f7e2c90f55690c06a3",
                 "shasum": ""
             },
             "require": {
             },
             "require-dev": {
                 "ext-zlib": "*",
-                "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.8"
+                "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.10"
             },
             "suggest": {
-                "zendframework/zend-httphandlerrunner": "Emit PSR-7 responses"
+                "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.6-dev"
+                    "dev-master": "1.7-dev"
                 }
             },
             "autoload": {
                 "uri",
                 "url"
             ],
-            "time": "2019-07-01T23:21:34+00:00"
+            "support": {
+                "issues": "https://github.com/guzzle/psr7/issues",
+                "source": "https://github.com/guzzle/psr7/tree/1.7.0"
+            },
+            "time": "2020-09-30T07:37:11+00:00"
         },
         {
             "name": "intervention/image",
                 "thumbnail",
                 "watermark"
             ],
+            "support": {
+                "issues": "https://github.com/Intervention/image/issues",
+                "source": "https://github.com/Intervention/image/tree/master"
+            },
             "time": "2019-11-02T09:15:47+00:00"
         },
         {
                     "email": "[email protected]"
                 }
             ],
+            "support": {
+                "issues": "https://github.com/JakubOnderka/PHP-Console-Color/issues",
+                "source": "https://github.com/JakubOnderka/PHP-Console-Color/tree/master"
+            },
             "abandoned": "php-parallel-lint/php-console-color",
             "time": "2018-09-29T17:23:10+00:00"
         },
                 }
             ],
             "description": "Highlight PHP code in terminal",
+            "support": {
+                "issues": "https://github.com/JakubOnderka/PHP-Console-Highlighter/issues",
+                "source": "https://github.com/JakubOnderka/PHP-Console-Highlighter/tree/master"
+            },
             "abandoned": "php-parallel-lint/php-console-highlighter",
             "time": "2018-09-29T18:48:56+00:00"
         },
                 "thumbnail",
                 "wkhtmltopdf"
             ],
+            "support": {
+                "issues": "https://github.com/KnpLabs/snappy/issues",
+                "source": "https://github.com/KnpLabs/snappy/tree/master"
+            },
             "time": "2020-01-20T08:30:30+00:00"
         },
         {
             "name": "laravel/framework",
-            "version": "v6.18.40",
+            "version": "v6.20.7",
             "source": {
                 "type": "git",
                 "url": "https://github.com/laravel/framework.git",
-                "reference": "e42450df0896b7130ccdb5290a114424e18887c9"
+                "reference": "bdc79701b567c5f8ed44d212dd4a261b8300b9c3"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/laravel/framework/zipball/e42450df0896b7130ccdb5290a114424e18887c9",
-                "reference": "e42450df0896b7130ccdb5290a114424e18887c9",
+                "url": "https://api.github.com/repos/laravel/framework/zipball/bdc79701b567c5f8ed44d212dd4a261b8300b9c3",
+                "reference": "bdc79701b567c5f8ed44d212dd4a261b8300b9c3",
                 "shasum": ""
             },
             "require": {
                 "doctrine/inflector": "^1.4|^2.0",
-                "dragonmantank/cron-expression": "^2.0",
+                "dragonmantank/cron-expression": "^2.3.1",
                 "egulias/email-validator": "^2.1.10",
                 "ext-json": "*",
                 "ext-mbstring": "*",
                 "ext-openssl": "*",
                 "league/commonmark": "^1.3",
-                "league/flysystem": "^1.0.34",
+                "league/flysystem": "^1.1",
                 "monolog/monolog": "^1.12|^2.0",
-                "nesbot/carbon": "^2.0",
-                "opis/closure": "^3.1",
-                "php": "^7.2",
+                "nesbot/carbon": "^2.31",
+                "opis/closure": "^3.6",
+                "php": "^7.2.5|^8.0",
                 "psr/container": "^1.0",
                 "psr/simple-cache": "^1.0",
                 "ramsey/uuid": "^3.7",
                 "illuminate/view": "self.version"
             },
             "require-dev": {
-                "aws/aws-sdk-php": "^3.0",
+                "aws/aws-sdk-php": "^3.155",
                 "doctrine/dbal": "^2.6",
-                "filp/whoops": "^2.4",
-                "guzzlehttp/guzzle": "^6.3|^7.0",
+                "filp/whoops": "^2.8",
+                "guzzlehttp/guzzle": "^6.3.1|^7.0.1",
                 "league/flysystem-cached-adapter": "^1.0",
-                "mockery/mockery": "^1.3.1",
+                "mockery/mockery": "~1.3.3|^1.4.2",
                 "moontoast/math": "^1.1",
-                "orchestra/testbench-core": "^4.0",
+                "orchestra/testbench-core": "^4.8",
                 "pda/pheanstalk": "^4.0",
-                "phpunit/phpunit": "^7.5.15|^8.4|^9.0",
+                "phpunit/phpunit": "^7.5.15|^8.4|^9.3.3",
                 "predis/predis": "^1.1.1",
                 "symfony/cache": "^4.3.4"
             },
             "suggest": {
-                "aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage and SES mail driver (^3.0).",
+                "aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage and SES mail driver (^3.155).",
                 "doctrine/dbal": "Required to rename columns and drop SQLite columns (^2.6).",
                 "ext-ftp": "Required to use the Flysystem FTP driver.",
                 "ext-gd": "Required to use Illuminate\\Http\\Testing\\FileFactory::image().",
                 "ext-pcntl": "Required to use all features of the queue worker.",
                 "ext-posix": "Required to use all features of the queue worker.",
                 "ext-redis": "Required to use the Redis cache and queue drivers (^4.0|^5.0).",
-                "filp/whoops": "Required for friendly error pages in development (^2.4).",
-                "fzaninotto/faker": "Required to use the eloquent factory builder (^1.9.1).",
-                "guzzlehttp/guzzle": "Required to use the Mailgun mail driver and the ping methods on schedules (^6.0|^7.0).",
+                "fakerphp/faker": "Required to use the eloquent factory builder (^1.9.1).",
+                "filp/whoops": "Required for friendly error pages in development (^2.8).",
+                "guzzlehttp/guzzle": "Required to use the Mailgun mail driver and the ping methods on schedules (^6.3.1|^7.0.1).",
                 "laravel/tinker": "Required to use the tinker console command (^2.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).",
                 "framework",
                 "laravel"
             ],
-            "time": "2020-09-09T15:02:20+00:00"
+            "support": {
+                "issues": "https://github.com/laravel/framework/issues",
+                "source": "https://github.com/laravel/framework"
+            },
+            "time": "2020-12-08T15:31:27+00:00"
         },
         {
             "name": "laravel/socialite",
-            "version": "v4.4.1",
+            "version": "v5.1.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/laravel/socialite.git",
-                "reference": "80951df0d93435b773aa00efe1fad6d5015fac75"
+                "reference": "19fc65ac28e0b4684a8735b14c1dc6f6ef5d62c7"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/laravel/socialite/zipball/80951df0d93435b773aa00efe1fad6d5015fac75",
-                "reference": "80951df0d93435b773aa00efe1fad6d5015fac75",
+                "url": "https://api.github.com/repos/laravel/socialite/zipball/19fc65ac28e0b4684a8735b14c1dc6f6ef5d62c7",
+                "reference": "19fc65ac28e0b4684a8735b14c1dc6f6ef5d62c7",
                 "shasum": ""
             },
             "require": {
                 "ext-json": "*",
                 "guzzlehttp/guzzle": "^6.0|^7.0",
-                "illuminate/http": "~5.7.0|~5.8.0|^6.0|^7.0",
-                "illuminate/support": "~5.7.0|~5.8.0|^6.0|^7.0",
+                "illuminate/http": "^6.0|^7.0|^8.0",
+                "illuminate/support": "^6.0|^7.0|^8.0",
                 "league/oauth1-client": "^1.0",
-                "php": "^7.1.3"
+                "php": "^7.2|^8.0"
             },
             "require-dev": {
-                "illuminate/contracts": "~5.7.0|~5.8.0|^6.0|^7.0",
+                "illuminate/contracts": "^6.0|^7.0",
                 "mockery/mockery": "^1.0",
-                "orchestra/testbench": "^3.7|^3.8|^4.0|^5.0",
-                "phpunit/phpunit": "^7.0|^8.0"
+                "orchestra/testbench": "^4.0|^5.0|^6.0",
+                "phpunit/phpunit": "^8.0|^9.3"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "4.x-dev"
+                    "dev-master": "5.x-dev"
                 },
                 "laravel": {
                     "providers": [
                 "laravel",
                 "oauth"
             ],
-            "time": "2020-06-03T13:30:03+00:00"
+            "support": {
+                "issues": "https://github.com/laravel/socialite/issues",
+                "source": "https://github.com/laravel/socialite"
+            },
+            "time": "2020-12-04T15:30:50+00:00"
         },
         {
             "name": "league/commonmark",
-            "version": "1.5.5",
+            "version": "1.5.7",
             "source": {
                 "type": "git",
                 "url": "https://github.com/thephpleague/commonmark.git",
-                "reference": "45832dfed6007b984c0d40addfac48d403dc6432"
+                "reference": "11df9b36fd4f1d2b727a73bf14931d81373b9a54"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/45832dfed6007b984c0d40addfac48d403dc6432",
-                "reference": "45832dfed6007b984c0d40addfac48d403dc6432",
+                "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/11df9b36fd4f1d2b727a73bf14931d81373b9a54",
+                "reference": "11df9b36fd4f1d2b727a73bf14931d81373b9a54",
                 "shasum": ""
             },
             "require": {
                 "md",
                 "parser"
             ],
+            "support": {
+                "docs": "https://commonmark.thephpleague.com/",
+                "issues": "https://github.com/thephpleague/commonmark/issues",
+                "rss": "https://github.com/thephpleague/commonmark/releases.atom",
+                "source": "https://github.com/thephpleague/commonmark"
+            },
             "funding": [
                 {
                     "url": "https://enjoy.gitstore.app/repositories/thephpleague/commonmark",
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-09-13T14:44:46+00:00"
+            "time": "2020-10-31T13:49:32+00:00"
         },
         {
             "name": "league/flysystem",
-            "version": "1.0.70",
+            "version": "1.1.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/thephpleague/flysystem.git",
-                "reference": "585824702f534f8d3cf7fab7225e8466cc4b7493"
+                "reference": "9be3b16c877d477357c015cec057548cf9b2a14a"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/585824702f534f8d3cf7fab7225e8466cc4b7493",
-                "reference": "585824702f534f8d3cf7fab7225e8466cc4b7493",
+                "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/9be3b16c877d477357c015cec057548cf9b2a14a",
+                "reference": "9be3b16c877d477357c015cec057548cf9b2a14a",
                 "shasum": ""
             },
             "require": {
                 "ext-fileinfo": "*",
-                "php": ">=5.5.9"
+                "league/mime-type-detection": "^1.3",
+                "php": "^7.2.5 || ^8.0"
             },
             "conflict": {
                 "league/flysystem-sftp": "<1.0.6"
             },
             "require-dev": {
-                "phpspec/phpspec": "^3.4 || ^4.0 || ^5.0 || ^6.0",
-                "phpunit/phpunit": "^5.7.26"
+                "phpspec/prophecy": "^1.11.1",
+                "phpunit/phpunit": "^8.5.8"
             },
             "suggest": {
                 "ext-fileinfo": "Required for MimeType",
                 "sftp",
                 "storage"
             ],
+            "support": {
+                "issues": "https://github.com/thephpleague/flysystem/issues",
+                "source": "https://github.com/thephpleague/flysystem/tree/1.x"
+            },
             "funding": [
                 {
                     "url": "https://offset.earth/frankdejonge",
                     "type": "other"
                 }
             ],
-            "time": "2020-07-26T07:20:36+00:00"
+            "time": "2020-08-23T07:39:11+00:00"
         },
         {
             "name": "league/flysystem-aws-s3-v3",
-            "version": "1.0.28",
+            "version": "1.0.29",
             "source": {
                 "type": "git",
                 "url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git",
-                "reference": "af7384a12f7cd7d08183390d930c9d0ec629c990"
+                "reference": "4e25cc0582a36a786c31115e419c6e40498f6972"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/af7384a12f7cd7d08183390d930c9d0ec629c990",
-                "reference": "af7384a12f7cd7d08183390d930c9d0ec629c990",
+                "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/4e25cc0582a36a786c31115e419c6e40498f6972",
+                "reference": "4e25cc0582a36a786c31115e419c6e40498f6972",
                 "shasum": ""
             },
             "require": {
                 }
             ],
             "description": "Flysystem adapter for the AWS S3 SDK v3.x",
-            "time": "2020-08-22T08:43:01+00:00"
+            "support": {
+                "issues": "https://github.com/thephpleague/flysystem-aws-s3-v3/issues",
+                "source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/1.0.29"
+            },
+            "time": "2020-10-08T18:58:37+00:00"
+        },
+        {
+            "name": "league/mime-type-detection",
+            "version": "1.5.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/thephpleague/mime-type-detection.git",
+                "reference": "353f66d7555d8a90781f6f5e7091932f9a4250aa"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/353f66d7555d8a90781f6f5e7091932f9a4250aa",
+                "reference": "353f66d7555d8a90781f6f5e7091932f9a4250aa",
+                "shasum": ""
+            },
+            "require": {
+                "ext-fileinfo": "*",
+                "php": "^7.2 || ^8.0"
+            },
+            "require-dev": {
+                "phpstan/phpstan": "^0.12.36",
+                "phpunit/phpunit": "^8.5.8"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "League\\MimeTypeDetection\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Frank de Jonge",
+                    "email": "[email protected]"
+                }
+            ],
+            "description": "Mime-type detection for Flysystem",
+            "support": {
+                "issues": "https://github.com/thephpleague/mime-type-detection/issues",
+                "source": "https://github.com/thephpleague/mime-type-detection/tree/1.5.1"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/frankdejonge",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/league/flysystem",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2020-10-18T11:50:25+00:00"
         },
         {
             "name": "league/oauth1-client",
-            "version": "v1.8.1",
+            "version": "v1.8.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/thephpleague/oauth1-client.git",
-                "reference": "3a68155c3f27a91f4b66a2dc03996cd6f3281c9f"
+                "reference": "159c3d2bf27568f9af87d6c3f4bb616a251eb12b"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/thephpleague/oauth1-client/zipball/3a68155c3f27a91f4b66a2dc03996cd6f3281c9f",
-                "reference": "3a68155c3f27a91f4b66a2dc03996cd6f3281c9f",
+                "url": "https://api.github.com/repos/thephpleague/oauth1-client/zipball/159c3d2bf27568f9af87d6c3f4bb616a251eb12b",
+                "reference": "159c3d2bf27568f9af87d6c3f4bb616a251eb12b",
                 "shasum": ""
             },
             "require": {
                 "tumblr",
                 "twitter"
             ],
-            "time": "2020-09-04T11:07:03+00:00"
+            "support": {
+                "issues": "https://github.com/thephpleague/oauth1-client/issues",
+                "source": "https://github.com/thephpleague/oauth1-client/tree/v1.8.2"
+            },
+            "time": "2020-09-28T09:39:08+00:00"
         },
         {
             "name": "monolog/monolog",
-            "version": "2.1.1",
+            "version": "2.2.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/Seldaek/monolog.git",
-                "reference": "f9eee5cec93dfb313a38b6b288741e84e53f02d5"
+                "reference": "1cb1cde8e8dd0f70cc0fe51354a59acad9302084"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/Seldaek/monolog/zipball/f9eee5cec93dfb313a38b6b288741e84e53f02d5",
-                "reference": "f9eee5cec93dfb313a38b6b288741e84e53f02d5",
+                "url": "https://api.github.com/repos/Seldaek/monolog/zipball/1cb1cde8e8dd0f70cc0fe51354a59acad9302084",
+                "reference": "1cb1cde8e8dd0f70cc0fe51354a59acad9302084",
                 "shasum": ""
             },
             "require": {
             "require-dev": {
                 "aws/aws-sdk-php": "^2.4.9 || ^3.0",
                 "doctrine/couchdb": "~1.0@dev",
-                "elasticsearch/elasticsearch": "^6.0",
+                "elasticsearch/elasticsearch": "^7",
                 "graylog2/gelf-php": "^1.4.2",
+                "mongodb/mongodb": "^1.8",
                 "php-amqplib/php-amqplib": "~2.4",
                 "php-console/php-console": "^3.1.3",
-                "php-parallel-lint/php-parallel-lint": "^1.0",
                 "phpspec/prophecy": "^1.6.1",
+                "phpstan/phpstan": "^0.12.59",
                 "phpunit/phpunit": "^8.5",
                 "predis/predis": "^1.1",
                 "rollbar/rollbar": "^1.3",
-                "ruflin/elastica": ">=0.90 <3.0",
+                "ruflin/elastica": ">=0.90 <7.0.1",
                 "swiftmailer/swiftmailer": "^5.3|^6.0"
             },
             "suggest": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "2.x-dev"
+                    "dev-main": "2.x-dev"
                 }
             },
             "autoload": {
                 {
                     "name": "Jordi Boggiano",
                     "email": "[email protected]",
-                    "homepage": "http://seld.be"
+                    "homepage": "https://seld.be"
                 }
             ],
             "description": "Sends your logs to files, sockets, inboxes, databases and various web services",
-            "homepage": "http://github.com/Seldaek/monolog",
+            "homepage": "https://github.com/Seldaek/monolog",
             "keywords": [
                 "log",
                 "logging",
                 "psr-3"
             ],
+            "support": {
+                "issues": "https://github.com/Seldaek/monolog/issues",
+                "source": "https://github.com/Seldaek/monolog/tree/2.2.0"
+            },
             "funding": [
                 {
                     "url": "https://github.com/Seldaek",
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-07-23T08:41:23+00:00"
+            "time": "2020-12-14T13:15:25+00:00"
         },
         {
             "name": "mtdowling/jmespath.php",
                 "json",
                 "jsonpath"
             ],
+            "support": {
+                "issues": "https://github.com/jmespath/jmespath.php/issues",
+                "source": "https://github.com/jmespath/jmespath.php/tree/2.6.0"
+            },
             "time": "2020-07-31T21:01:56+00:00"
         },
         {
             "name": "nesbot/carbon",
-            "version": "2.40.0",
+            "version": "2.42.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/briannesbitt/Carbon.git",
-                "reference": "6c7646154181013ecd55e80c201b9fd873c6ee5d"
+                "reference": "d0463779663437392fe42ff339ebc0213bd55498"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/6c7646154181013ecd55e80c201b9fd873c6ee5d",
-                "reference": "6c7646154181013ecd55e80c201b9fd873c6ee5d",
+                "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/d0463779663437392fe42ff339ebc0213bd55498",
+                "reference": "d0463779663437392fe42ff339ebc0213bd55498",
                 "shasum": ""
             },
             "require": {
                 "kylekatarnls/multi-tester": "^2.0",
                 "phpmd/phpmd": "^2.9",
                 "phpstan/extension-installer": "^1.0",
-                "phpstan/phpstan": "^0.12.35",
+                "phpstan/phpstan": "^0.12.54",
                 "phpunit/phpunit": "^7.5 || ^8.0",
                 "squizlabs/php_codesniffer": "^3.4"
             },
                 "datetime",
                 "time"
             ],
+            "support": {
+                "issues": "https://github.com/briannesbitt/Carbon/issues",
+                "source": "https://github.com/briannesbitt/Carbon"
+            },
             "funding": [
                 {
                     "url": "https://opencollective.com/Carbon",
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-09-11T19:00:58+00:00"
+            "time": "2020-11-28T14:25:28+00:00"
         },
         {
             "name": "nunomaduro/collision",
-            "version": "v3.0.1",
+            "version": "v3.1.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/nunomaduro/collision.git",
-                "reference": "af42d339fe2742295a54f6fdd42aaa6f8c4aca68"
+                "reference": "88b58b5bd9bdcc54756480fb3ce87234696544ee"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/nunomaduro/collision/zipball/af42d339fe2742295a54f6fdd42aaa6f8c4aca68",
-                "reference": "af42d339fe2742295a54f6fdd42aaa6f8c4aca68",
+                "url": "https://api.github.com/repos/nunomaduro/collision/zipball/88b58b5bd9bdcc54756480fb3ce87234696544ee",
+                "reference": "88b58b5bd9bdcc54756480fb3ce87234696544ee",
                 "shasum": ""
             },
             "require": {
                 "filp/whoops": "^2.1.4",
                 "jakub-onderka/php-console-highlighter": "0.3.*|0.4.*",
-                "php": "^7.1",
+                "php": "^7.1 || ^8.0",
                 "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"
+                "laravel/framework": "^6.0",
+                "phpunit/phpunit": "^8.0 || ^9.0"
             },
             "type": "library",
             "extra": {
                 "php",
                 "symfony"
             ],
-            "time": "2019-03-07T21:35:13+00:00"
+            "support": {
+                "issues": "https://github.com/nunomaduro/collision/issues",
+                "source": "https://github.com/nunomaduro/collision"
+            },
+            "funding": [
+                {
+                    "url": "https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=66BYDWAT92N6L",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/nunomaduro",
+                    "type": "github"
+                },
+                {
+                    "url": "https://www.patreon.com/nunomaduro",
+                    "type": "patreon"
+                }
+            ],
+            "time": "2020-10-29T16:05:21+00:00"
         },
         {
             "name": "onelogin/php-saml",
-            "version": "3.4.1",
+            "version": "3.5.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/onelogin/php-saml.git",
-                "reference": "5fbf3486704ac9835b68184023ab54862c95f213"
+                "reference": "593aca859b67d607923fe50d8ad7315373f5b6dd"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/onelogin/php-saml/zipball/5fbf3486704ac9835b68184023ab54862c95f213",
-                "reference": "5fbf3486704ac9835b68184023ab54862c95f213",
+                "url": "https://api.github.com/repos/onelogin/php-saml/zipball/593aca859b67d607923fe50d8ad7315373f5b6dd",
+                "reference": "593aca859b67d607923fe50d8ad7315373f5b6dd",
                 "shasum": ""
             },
             "require": {
                 "php": ">=5.4",
-                "robrichards/xmlseclibs": ">=3.0.4"
+                "robrichards/xmlseclibs": ">=3.1.1"
             },
             "require-dev": {
                 "pdepend/pdepend": "^2.5.0",
                 "onelogin",
                 "saml"
             ],
-            "time": "2019-11-25T17:30:07+00:00"
+            "support": {
+                "email": "[email protected]",
+                "issues": "https://github.com/onelogin/php-saml/issues",
+                "source": "https://github.com/onelogin/php-saml/"
+            },
+            "time": "2020-12-03T20:08:41+00:00"
         },
         {
             "name": "opis/closure",
-            "version": "3.5.7",
+            "version": "3.6.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/opis/closure.git",
-                "reference": "4531e53afe2fc660403e76fb7644e95998bff7bf"
+                "reference": "943b5d70cc5ae7483f6aff6ff43d7e34592ca0f5"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/opis/closure/zipball/4531e53afe2fc660403e76fb7644e95998bff7bf",
-                "reference": "4531e53afe2fc660403e76fb7644e95998bff7bf",
+                "url": "https://api.github.com/repos/opis/closure/zipball/943b5d70cc5ae7483f6aff6ff43d7e34592ca0f5",
+                "reference": "943b5d70cc5ae7483f6aff6ff43d7e34592ca0f5",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.4 || ^7.0"
+                "php": "^5.4 || ^7.0 || ^8.0"
             },
             "require-dev": {
                 "jeremeamia/superclosure": "^2.0",
-                "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0"
+                "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.0"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.5.x-dev"
+                    "dev-master": "3.6.x-dev"
                 }
             },
             "autoload": {
                 "serialization",
                 "serialize"
             ],
-            "time": "2020-09-06T17:02:15+00:00"
+            "support": {
+                "issues": "https://github.com/opis/closure/issues",
+                "source": "https://github.com/opis/closure/tree/3.6.1"
+            },
+            "time": "2020-11-07T02:01:34+00:00"
         },
         {
             "name": "paragonie/random_compat",
                 "pseudorandom",
                 "random"
             ],
+            "support": {
+                "email": "[email protected]",
+                "issues": "https://github.com/paragonie/random_compat/issues",
+                "source": "https://github.com/paragonie/random_compat"
+            },
             "time": "2018-07-02T15:55:56+00:00"
         },
         {
             ],
             "description": "A library to read, parse, export and make subsets of different types of font files.",
             "homepage": "https://github.com/PhenX/php-font-lib",
+            "support": {
+                "issues": "https://github.com/PhenX/php-font-lib/issues",
+                "source": "https://github.com/PhenX/php-font-lib/tree/0.5.2"
+            },
             "time": "2020-03-08T15:31:32+00:00"
         },
         {
             ],
             "description": "A library to read, parse and export to PDF SVG files.",
             "homepage": "https://github.com/PhenX/php-svg-lib",
+            "support": {
+                "issues": "https://github.com/PhenX/php-svg-lib/issues",
+                "source": "https://github.com/PhenX/php-svg-lib/tree/master"
+            },
             "time": "2019-09-11T20:02:13+00:00"
         },
         {
                 "php",
                 "type"
             ],
+            "support": {
+                "issues": "https://github.com/schmittjoh/php-option/issues",
+                "source": "https://github.com/schmittjoh/php-option/tree/1.7.5"
+            },
             "funding": [
                 {
                     "url": "https://github.com/GrahamCampbell",
                 "predis",
                 "redis"
             ],
+            "support": {
+                "issues": "https://github.com/predis/predis/issues",
+                "source": "https://github.com/predis/predis/tree/v1.1.6"
+            },
             "funding": [
                 {
                     "url": "https://github.com/sponsors/tillkruss",
                 "container-interop",
                 "psr"
             ],
+            "support": {
+                "issues": "https://github.com/php-fig/container/issues",
+                "source": "https://github.com/php-fig/container/tree/master"
+            },
             "time": "2017-02-14T16:28:37+00:00"
         },
+        {
+            "name": "psr/http-client",
+            "version": "1.0.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/http-client.git",
+                "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/http-client/zipball/2dfb5f6c5eff0e91e20e913f8c5452ed95b86621",
+                "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.0 || ^8.0",
+                "psr/http-message": "^1.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Psr\\Http\\Client\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "http://www.php-fig.org/"
+                }
+            ],
+            "description": "Common interface for HTTP clients",
+            "homepage": "https://github.com/php-fig/http-client",
+            "keywords": [
+                "http",
+                "http-client",
+                "psr",
+                "psr-18"
+            ],
+            "support": {
+                "source": "https://github.com/php-fig/http-client/tree/master"
+            },
+            "time": "2020-06-29T06:28:15+00:00"
+        },
         {
             "name": "psr/http-message",
             "version": "1.0.1",
                 "request",
                 "response"
             ],
+            "support": {
+                "source": "https://github.com/php-fig/http-message/tree/master"
+            },
             "time": "2016-08-06T14:39:51+00:00"
         },
         {
                 "psr",
                 "psr-3"
             ],
+            "support": {
+                "source": "https://github.com/php-fig/log/tree/1.1.3"
+            },
             "time": "2020-03-23T09:12:05+00:00"
         },
         {
                 "psr-16",
                 "simple-cache"
             ],
+            "support": {
+                "source": "https://github.com/php-fig/simple-cache/tree/master"
+            },
             "time": "2017-10-23T01:57:42+00:00"
         },
         {
                 }
             ],
             "description": "A polyfill for getallheaders.",
+            "support": {
+                "issues": "https://github.com/ralouphie/getallheaders/issues",
+                "source": "https://github.com/ralouphie/getallheaders/tree/develop"
+            },
             "time": "2019-03-08T08:55:37+00:00"
         },
         {
                 "identifier",
                 "uuid"
             ],
+            "support": {
+                "issues": "https://github.com/ramsey/uuid/issues",
+                "rss": "https://github.com/ramsey/uuid/releases.atom",
+                "source": "https://github.com/ramsey/uuid",
+                "wiki": "https://github.com/ramsey/uuid/wiki"
+            },
             "time": "2020-02-21T04:36:14+00:00"
         },
         {
                 "xml",
                 "xmldsig"
             ],
+            "support": {
+                "issues": "https://github.com/robrichards/xmlseclibs/issues",
+                "source": "https://github.com/robrichards/xmlseclibs/tree/3.1.1"
+            },
             "time": "2020-09-05T13:00:25+00:00"
         },
         {
                 "parser",
                 "stylesheet"
             ],
+            "support": {
+                "issues": "https://github.com/sabberworm/PHP-CSS-Parser/issues",
+                "source": "https://github.com/sabberworm/PHP-CSS-Parser/tree/8.3.1"
+            },
             "time": "2020-06-01T09:10:00+00:00"
         },
         {
             "name": "scrivo/highlight.php",
-            "version": "v9.18.1.2",
+            "version": "v9.18.1.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/scrivo/highlight.php.git",
-                "reference": "efb6e445494a9458aa59b0af5edfa4bdcc6809d9"
+                "reference": "fa75a865928a4a5d49e5e77faca6bd2f2410baaf"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/scrivo/highlight.php/zipball/efb6e445494a9458aa59b0af5edfa4bdcc6809d9",
-                "reference": "efb6e445494a9458aa59b0af5edfa4bdcc6809d9",
+                "url": "https://api.github.com/repos/scrivo/highlight.php/zipball/fa75a865928a4a5d49e5e77faca6bd2f2410baaf",
+                "reference": "fa75a865928a4a5d49e5e77faca6bd2f2410baaf",
                 "shasum": ""
             },
             "require": {
                 "highlight.php",
                 "syntax"
             ],
+            "support": {
+                "issues": "https://github.com/scrivo/highlight.php/issues",
+                "source": "https://github.com/scrivo/highlight.php"
+            },
             "funding": [
                 {
                     "url": "https://github.com/allejo",
                     "type": "github"
                 }
             ],
-            "time": "2020-08-27T03:24:44+00:00"
+            "time": "2020-11-22T06:07:40+00:00"
         },
         {
             "name": "socialiteproviders/discord",
-            "version": "v2.0.2",
+            "version": "4.1.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/SocialiteProviders/Discord.git",
-                "reference": "e0cd8895f321943b36f533e7bf21ad29bcdece9a"
+                "reference": "34c62db509c9680e120982f9239db5ce905eb027"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/SocialiteProviders/Discord/zipball/e0cd8895f321943b36f533e7bf21ad29bcdece9a",
-                "reference": "e0cd8895f321943b36f533e7bf21ad29bcdece9a",
+                "url": "https://api.github.com/repos/SocialiteProviders/Discord/zipball/34c62db509c9680e120982f9239db5ce905eb027",
+                "reference": "34c62db509c9680e120982f9239db5ce905eb027",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.6 || ^7.0",
-                "socialiteproviders/manager": "~2.0 || ~3.0"
+                "ext-json": "*",
+                "php": "^7.2 || ^8.0",
+                "socialiteproviders/manager": "~4.0"
             },
             "type": "library",
             "autoload": {
                 }
             ],
             "description": "Discord OAuth2 Provider for Laravel Socialite",
-            "time": "2018-05-26T03:40:07+00:00"
+            "support": {
+                "source": "https://github.com/SocialiteProviders/Discord/tree/4.1.0"
+            },
+            "time": "2020-12-01T23:10:59+00:00"
         },
         {
             "name": "socialiteproviders/gitlab",
-            "version": "v3.1",
+            "version": "4.1.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/SocialiteProviders/GitLab.git",
-                "reference": "69e537f6192ca15483e98b8662495384f44299ca"
+                "reference": "a8f67d3b02c9ee8c70c25c6728417c0eddcbbb9d"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/SocialiteProviders/GitLab/zipball/69e537f6192ca15483e98b8662495384f44299ca",
-                "reference": "69e537f6192ca15483e98b8662495384f44299ca",
+                "url": "https://api.github.com/repos/SocialiteProviders/GitLab/zipball/a8f67d3b02c9ee8c70c25c6728417c0eddcbbb9d",
+                "reference": "a8f67d3b02c9ee8c70c25c6728417c0eddcbbb9d",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.6 || ^7.0",
-                "socialiteproviders/manager": "~2.0 || ~3.0"
+                "ext-json": "*",
+                "php": "^7.2 || ^8.0",
+                "socialiteproviders/manager": "~4.0"
             },
             "type": "library",
             "autoload": {
                 }
             ],
             "description": "GitLab OAuth2 Provider for Laravel Socialite",
-            "time": "2018-06-27T05:10:32+00:00"
+            "support": {
+                "source": "https://github.com/SocialiteProviders/GitLab/tree/4.1.0"
+            },
+            "time": "2020-12-01T23:10:59+00:00"
         },
         {
             "name": "socialiteproviders/manager",
-            "version": "v3.6",
+            "version": "4.0.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/SocialiteProviders/Manager.git",
-                "reference": "fc8dbcf0061f12bfe0cc347e9655af932860ad36"
+                "reference": "0f5e82af0404df0080bdc5c105cef936c1711524"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/SocialiteProviders/Manager/zipball/fc8dbcf0061f12bfe0cc347e9655af932860ad36",
-                "reference": "fc8dbcf0061f12bfe0cc347e9655af932860ad36",
+                "url": "https://api.github.com/repos/SocialiteProviders/Manager/zipball/0f5e82af0404df0080bdc5c105cef936c1711524",
+                "reference": "0f5e82af0404df0080bdc5c105cef936c1711524",
                 "shasum": ""
             },
             "require": {
                 "illuminate/support": "^6.0|^7.0|^8.0",
                 "laravel/socialite": "~4.0|~5.0",
-                "php": "^7.2"
+                "php": "^7.2 || ^8.0"
             },
             "require-dev": {
                 "mockery/mockery": "^1.2",
-                "phpunit/phpunit": "^8.0"
+                "phpunit/phpunit": "^9.0"
             },
             "type": "library",
             "extra": {
             ],
             "description": "Easily add new or override built-in providers in Laravel Socialite.",
             "homepage": "https://socialiteproviders.com/",
-            "time": "2020-09-08T10:41:06+00:00"
+            "support": {
+                "issues": "https://github.com/SocialiteProviders/Manager/issues",
+                "source": "https://github.com/SocialiteProviders/Manager/tree/4.0.1"
+            },
+            "time": "2020-12-01T23:09:06+00:00"
         },
         {
             "name": "socialiteproviders/microsoft-azure",
-            "version": "v3.1.0",
+            "version": "4.1.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/SocialiteProviders/Microsoft-Azure.git",
-                "reference": "b22f4696cccecd6de902cf0bc923de7fc2e4608e"
+                "reference": "7808764f777a01df88be9ca6b14d683e50aaf88a"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/SocialiteProviders/Microsoft-Azure/zipball/b22f4696cccecd6de902cf0bc923de7fc2e4608e",
-                "reference": "b22f4696cccecd6de902cf0bc923de7fc2e4608e",
+                "url": "https://api.github.com/repos/SocialiteProviders/Microsoft-Azure/zipball/7808764f777a01df88be9ca6b14d683e50aaf88a",
+                "reference": "7808764f777a01df88be9ca6b14d683e50aaf88a",
                 "shasum": ""
             },
             "require": {
                 "ext-json": "*",
-                "php": "^5.6 || ^7.0",
-                "socialiteproviders/manager": "~2.0 || ~3.0"
+                "php": "^7.2 || ^8.0",
+                "socialiteproviders/manager": "~4.0"
             },
             "type": "library",
             "autoload": {
                 }
             ],
             "description": "Microsoft Azure OAuth2 Provider for Laravel Socialite",
-            "time": "2020-04-30T23:01:40+00:00"
+            "support": {
+                "source": "https://github.com/SocialiteProviders/Microsoft-Azure/tree/4.1.0"
+            },
+            "time": "2020-12-01T23:10:59+00:00"
         },
         {
             "name": "socialiteproviders/okta",
-            "version": "v1.1.0",
+            "version": "4.1.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/SocialiteProviders/Okta.git",
-                "reference": "7c2512f0872316b139e3eea1c50c9351747a57ea"
+                "reference": "60f88b8e8c88508889c61346af83290131b72fe7"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/SocialiteProviders/Okta/zipball/7c2512f0872316b139e3eea1c50c9351747a57ea",
-                "reference": "7c2512f0872316b139e3eea1c50c9351747a57ea",
+                "url": "https://api.github.com/repos/SocialiteProviders/Okta/zipball/60f88b8e8c88508889c61346af83290131b72fe7",
+                "reference": "60f88b8e8c88508889c61346af83290131b72fe7",
                 "shasum": ""
             },
             "require": {
                 "ext-json": "*",
-                "php": "^5.6 || ^7.0",
-                "socialiteproviders/manager": "~2.0 || ~3.0"
+                "php": "^7.2 || ^8.0",
+                "socialiteproviders/manager": "~4.0"
             },
             "type": "library",
             "autoload": {
                 }
             ],
             "description": "Okta OAuth2 Provider for Laravel Socialite",
-            "time": "2019-09-06T15:27:03+00:00"
+            "support": {
+                "source": "https://github.com/SocialiteProviders/Okta/tree/4.1.0"
+            },
+            "time": "2020-12-01T23:10:59+00:00"
         },
         {
             "name": "socialiteproviders/slack",
-            "version": "v3.1",
+            "version": "4.1.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/SocialiteProviders/Slack.git",
-                "reference": "d46826640fbeae8f34328d99c358404a1e1050a3"
+                "reference": "8efb25c71d98bedf4010a829d1e41ff9fe449bcc"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/SocialiteProviders/Slack/zipball/d46826640fbeae8f34328d99c358404a1e1050a3",
-                "reference": "d46826640fbeae8f34328d99c358404a1e1050a3",
+                "url": "https://api.github.com/repos/SocialiteProviders/Slack/zipball/8efb25c71d98bedf4010a829d1e41ff9fe449bcc",
+                "reference": "8efb25c71d98bedf4010a829d1e41ff9fe449bcc",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.6 || ^7.0",
-                "socialiteproviders/manager": "~2.0 || ~3.0"
+                "ext-json": "*",
+                "php": "^7.2|^8.0",
+                "socialiteproviders/manager": "~4.0"
             },
             "type": "library",
             "autoload": {
                 }
             ],
             "description": "Slack OAuth2 Provider for Laravel Socialite",
-            "time": "2019-01-11T19:48:14+00:00"
+            "support": {
+                "source": "https://github.com/SocialiteProviders/Slack/tree/4.1.0"
+            },
+            "time": "2020-11-26T17:57:15+00:00"
         },
         {
             "name": "socialiteproviders/twitch",
-            "version": "v5.2.0",
+            "version": "5.3.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/SocialiteProviders/Twitch.git",
-                "reference": "9ee6fe196d7c28777139b3cde04cbd537cf7e652"
+                "reference": "7accf30ae7a3139b757b4ca8f34989c09a3dbee7"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/SocialiteProviders/Twitch/zipball/9ee6fe196d7c28777139b3cde04cbd537cf7e652",
-                "reference": "9ee6fe196d7c28777139b3cde04cbd537cf7e652",
+                "url": "https://api.github.com/repos/SocialiteProviders/Twitch/zipball/7accf30ae7a3139b757b4ca8f34989c09a3dbee7",
+                "reference": "7accf30ae7a3139b757b4ca8f34989c09a3dbee7",
                 "shasum": ""
             },
             "require": {
                 "ext-json": "*",
-                "php": "^5.6 || ^7.0",
-                "socialiteproviders/manager": "~2.0 || ~3.0"
+                "php": "^7.2 || ^8.0",
+                "socialiteproviders/manager": "~4.0"
             },
             "type": "library",
             "autoload": {
                 }
             ],
             "description": "Twitch OAuth2 Provider for Laravel Socialite",
-            "time": "2020-05-06T22:51:30+00:00"
+            "support": {
+                "source": "https://github.com/SocialiteProviders/Twitch/tree/5.3.1"
+            },
+            "time": "2020-12-01T23:10:59+00:00"
+        },
+        {
+            "name": "ssddanbrown/htmldiff",
+            "version": "v1.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/ssddanbrown/HtmlDiff.git",
+                "reference": "d1978c7d1c685800997f982a0ae9cff1e45df70c"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/ssddanbrown/HtmlDiff/zipball/d1978c7d1c685800997f982a0ae9cff1e45df70c",
+                "reference": "d1978c7d1c685800997f982a0ae9cff1e45df70c",
+                "shasum": ""
+            },
+            "require": {
+                "ext-mbstring": "*",
+                "php": ">=7.2"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^8.5|^9.4.3"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Ssddanbrown\\HtmlDiff\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Dan Brown",
+                    "email": "[email protected]",
+                    "role": "Developer"
+                }
+            ],
+            "description": "HTML Content Diff Generator",
+            "homepage": "https://github.com/ssddanbrown/htmldiff",
+            "support": {
+                "issues": "https://github.com/ssddanbrown/HtmlDiff/issues",
+                "source": "https://github.com/ssddanbrown/HtmlDiff/tree/v1.0.0"
+            },
+            "time": "2020-11-29T18:38:45+00:00"
         },
         {
             "name": "swiftmailer/swiftmailer",
-            "version": "v6.2.3",
+            "version": "v6.2.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/swiftmailer/swiftmailer.git",
-                "reference": "149cfdf118b169f7840bbe3ef0d4bc795d1780c9"
+                "reference": "56f0ab23f54c4ccbb0d5dcc67ff8552e0c98d59e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/149cfdf118b169f7840bbe3ef0d4bc795d1780c9",
-                "reference": "149cfdf118b169f7840bbe3ef0d4bc795d1780c9",
+                "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/56f0ab23f54c4ccbb0d5dcc67ff8552e0c98d59e",
+                "reference": "56f0ab23f54c4ccbb0d5dcc67ff8552e0c98d59e",
                 "shasum": ""
             },
             "require": {
-                "egulias/email-validator": "~2.0",
+                "egulias/email-validator": "^2.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.4.19|^4.1.8"
+                "mockery/mockery": "^1.0",
+                "symfony/phpunit-bridge": "^4.4|^5.0"
             },
             "suggest": {
-                "ext-intl": "Needed to support internationalized email addresses",
-                "true/punycode": "Needed to support internationalized email addresses, if ext-intl is not installed"
+                "ext-intl": "Needed to support internationalized email addresses"
             },
             "type": "library",
             "extra": {
                 "mail",
                 "mailer"
             ],
-            "time": "2019-11-12T09:31:26+00:00"
+            "support": {
+                "issues": "https://github.com/swiftmailer/swiftmailer/issues",
+                "source": "https://github.com/swiftmailer/swiftmailer/tree/v6.2.4"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/swiftmailer/swiftmailer",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2020-12-08T18:02:06+00:00"
         },
         {
             "name": "symfony/console",
-            "version": "v4.4.13",
+            "version": "v4.4.18",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/console.git",
-                "reference": "b39fd99b9297b67fb7633b7d8083957a97e1e727"
+                "reference": "12e071278e396cc3e1c149857337e9e192deca0b"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/console/zipball/b39fd99b9297b67fb7633b7d8083957a97e1e727",
-                "reference": "b39fd99b9297b67fb7633b7d8083957a97e1e727",
+                "url": "https://api.github.com/repos/symfony/console/zipball/12e071278e396cc3e1c149857337e9e192deca0b",
+                "reference": "12e071278e396cc3e1c149857337e9e192deca0b",
                 "shasum": ""
             },
             "require": {
                 "symfony/process": ""
             },
             "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "4.4-dev"
-                }
-            },
             "autoload": {
                 "psr-4": {
                     "Symfony\\Component\\Console\\": ""
             ],
             "description": "Symfony Console Component",
             "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/console/tree/v4.4.18"
+            },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-09-02T07:07:21+00:00"
+            "time": "2020-12-18T07:41:31+00:00"
         },
         {
             "name": "symfony/css-selector",
-            "version": "v4.4.13",
+            "version": "v4.4.18",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/css-selector.git",
-                "reference": "bf17dc9f6ce144e41f786c32435feea4d8e11dcc"
+                "reference": "74bd82e75da256ad20851af6ded07823332216c7"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/css-selector/zipball/bf17dc9f6ce144e41f786c32435feea4d8e11dcc",
-                "reference": "bf17dc9f6ce144e41f786c32435feea4d8e11dcc",
+                "url": "https://api.github.com/repos/symfony/css-selector/zipball/74bd82e75da256ad20851af6ded07823332216c7",
+                "reference": "74bd82e75da256ad20851af6ded07823332216c7",
                 "shasum": ""
             },
             "require": {
                 "php": ">=7.1.3"
             },
             "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "4.4-dev"
-                }
-            },
             "autoload": {
                 "psr-4": {
                     "Symfony\\Component\\CssSelector\\": ""
             ],
             "description": "Symfony CssSelector Component",
             "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/css-selector/tree/v4.4.18"
+            },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-07-05T09:39:30+00:00"
+            "time": "2020-12-08T16:59:59+00:00"
         },
         {
             "name": "symfony/debug",
-            "version": "v4.4.13",
+            "version": "v4.4.18",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/debug.git",
-                "reference": "aeb73aca16a8f1fe958230fe44e6cf4c84cbb85e"
+                "reference": "5dfc7825f3bfe9bb74b23d8b8ce0e0894e32b544"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/debug/zipball/aeb73aca16a8f1fe958230fe44e6cf4c84cbb85e",
-                "reference": "aeb73aca16a8f1fe958230fe44e6cf4c84cbb85e",
+                "url": "https://api.github.com/repos/symfony/debug/zipball/5dfc7825f3bfe9bb74b23d8b8ce0e0894e32b544",
+                "reference": "5dfc7825f3bfe9bb74b23d8b8ce0e0894e32b544",
                 "shasum": ""
             },
             "require": {
                 "symfony/http-kernel": "^3.4|^4.0|^5.0"
             },
             "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "4.4-dev"
-                }
-            },
             "autoload": {
                 "psr-4": {
                     "Symfony\\Component\\Debug\\": ""
             ],
             "description": "Symfony Debug Component",
             "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/debug/tree/v4.4.18"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2020-12-10T16:34:26+00:00"
+        },
+        {
+            "name": "symfony/deprecation-contracts",
+            "version": "v2.2.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/deprecation-contracts.git",
+                "reference": "5fa56b4074d1ae755beb55617ddafe6f5d78f665"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/5fa56b4074d1ae755beb55617ddafe6f5d78f665",
+                "reference": "5fa56b4074d1ae755beb55617ddafe6f5d78f665",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.2-dev"
+                },
+                "thanks": {
+                    "name": "symfony/contracts",
+                    "url": "https://github.com/symfony/contracts"
+                }
+            },
+            "autoload": {
+                "files": [
+                    "function.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": "A generic function and convention to trigger deprecation notices",
+            "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/deprecation-contracts/tree/master"
+            },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-08-10T07:47:39+00:00"
+            "time": "2020-09-07T11:33:47+00:00"
         },
         {
             "name": "symfony/error-handler",
-            "version": "v4.4.13",
+            "version": "v4.4.18",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/error-handler.git",
-                "reference": "2434fb32851f252e4f27691eee0b77c16198db62"
+                "reference": "ef2f7ddd3b9177bbf8ff2ecd8d0e970ed48da0c3"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/error-handler/zipball/2434fb32851f252e4f27691eee0b77c16198db62",
-                "reference": "2434fb32851f252e4f27691eee0b77c16198db62",
+                "url": "https://api.github.com/repos/symfony/error-handler/zipball/ef2f7ddd3b9177bbf8ff2ecd8d0e970ed48da0c3",
+                "reference": "ef2f7ddd3b9177bbf8ff2ecd8d0e970ed48da0c3",
                 "shasum": ""
             },
             "require": {
                 "symfony/serializer": "^4.4|^5.0"
             },
             "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "4.4-dev"
-                }
-            },
             "autoload": {
                 "psr-4": {
                     "Symfony\\Component\\ErrorHandler\\": ""
             ],
             "description": "Symfony ErrorHandler Component",
             "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/error-handler/tree/v4.4.18"
+            },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-08-17T09:56:45+00:00"
+            "time": "2020-12-09T11:15:38+00:00"
         },
         {
             "name": "symfony/event-dispatcher",
-            "version": "v4.4.13",
+            "version": "v4.4.18",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/event-dispatcher.git",
-                "reference": "3e8ea5ccddd00556b86d69d42f99f1061a704030"
+                "reference": "5d4c874b0eb1c32d40328a09dbc37307a5a910b0"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/3e8ea5ccddd00556b86d69d42f99f1061a704030",
-                "reference": "3e8ea5ccddd00556b86d69d42f99f1061a704030",
+                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/5d4c874b0eb1c32d40328a09dbc37307a5a910b0",
+                "reference": "5d4c874b0eb1c32d40328a09dbc37307a5a910b0",
                 "shasum": ""
             },
             "require": {
                 "psr/log": "~1.0",
                 "symfony/config": "^3.4|^4.0|^5.0",
                 "symfony/dependency-injection": "^3.4|^4.0|^5.0",
+                "symfony/error-handler": "~3.4|~4.4",
                 "symfony/expression-language": "^3.4|^4.0|^5.0",
                 "symfony/http-foundation": "^3.4|^4.0|^5.0",
                 "symfony/service-contracts": "^1.1|^2",
                 "symfony/http-kernel": ""
             },
             "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "4.4-dev"
-                }
-            },
             "autoload": {
                 "psr-4": {
                     "Symfony\\Component\\EventDispatcher\\": ""
             ],
             "description": "Symfony EventDispatcher Component",
             "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/event-dispatcher/tree/v4.4.18"
+            },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-08-13T14:18:44+00:00"
+            "time": "2020-12-18T07:41:31+00:00"
         },
         {
             "name": "symfony/event-dispatcher-contracts",
                 "interoperability",
                 "standards"
             ],
+            "support": {
+                "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v1.1.9"
+            },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
         },
         {
             "name": "symfony/finder",
-            "version": "v4.4.13",
+            "version": "v4.4.18",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/finder.git",
-                "reference": "2a78590b2c7e3de5c429628457c47541c58db9c7"
+                "reference": "ebd0965f2dc2d4e0f11487c16fbb041e50b5c09b"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/finder/zipball/2a78590b2c7e3de5c429628457c47541c58db9c7",
-                "reference": "2a78590b2c7e3de5c429628457c47541c58db9c7",
+                "url": "https://api.github.com/repos/symfony/finder/zipball/ebd0965f2dc2d4e0f11487c16fbb041e50b5c09b",
+                "reference": "ebd0965f2dc2d4e0f11487c16fbb041e50b5c09b",
                 "shasum": ""
             },
             "require": {
                 "php": ">=7.1.3"
             },
             "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "4.4-dev"
-                }
-            },
             "autoload": {
                 "psr-4": {
                     "Symfony\\Component\\Finder\\": ""
             ],
             "description": "Symfony Finder Component",
             "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/finder/tree/v4.4.18"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2020-12-08T16:59:59+00:00"
+        },
+        {
+            "name": "symfony/http-client-contracts",
+            "version": "v2.3.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/http-client-contracts.git",
+                "reference": "41db680a15018f9c1d4b23516059633ce280ca33"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/41db680a15018f9c1d4b23516059633ce280ca33",
+                "reference": "41db680a15018f9c1d4b23516059633ce280ca33",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.2.5"
+            },
+            "suggest": {
+                "symfony/http-client-implementation": ""
+            },
+            "type": "library",
+            "extra": {
+                "branch-version": "2.3",
+                "branch-alias": {
+                    "dev-main": "2.3-dev"
+                },
+                "thanks": {
+                    "name": "symfony/contracts",
+                    "url": "https://github.com/symfony/contracts"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Contracts\\HttpClient\\": ""
+                }
+            },
+            "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 HTTP clients",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "abstractions",
+                "contracts",
+                "decoupling",
+                "interfaces",
+                "interoperability",
+                "standards"
+            ],
+            "support": {
+                "source": "https://github.com/symfony/http-client-contracts/tree/v2.3.1"
+            },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-08-17T09:56:45+00:00"
+            "time": "2020-10-14T17:08:19+00:00"
         },
         {
             "name": "symfony/http-foundation",
-            "version": "v4.4.13",
+            "version": "v4.4.18",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/http-foundation.git",
-                "reference": "e3e5a62a6631a461954d471e7206e3750dbe8ee1"
+                "reference": "5ebda66b51612516bf338d5f87da2f37ff74cf34"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/http-foundation/zipball/e3e5a62a6631a461954d471e7206e3750dbe8ee1",
-                "reference": "e3e5a62a6631a461954d471e7206e3750dbe8ee1",
+                "url": "https://api.github.com/repos/symfony/http-foundation/zipball/5ebda66b51612516bf338d5f87da2f37ff74cf34",
+                "reference": "5ebda66b51612516bf338d5f87da2f37ff74cf34",
                 "shasum": ""
             },
             "require": {
                 "php": ">=7.1.3",
                 "symfony/mime": "^4.3|^5.0",
-                "symfony/polyfill-mbstring": "~1.1"
+                "symfony/polyfill-mbstring": "~1.1",
+                "symfony/polyfill-php80": "^1.15"
             },
             "require-dev": {
                 "predis/predis": "~1.0",
                 "symfony/expression-language": "^3.4|^4.0|^5.0"
             },
             "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "4.4-dev"
-                }
-            },
             "autoload": {
                 "psr-4": {
                     "Symfony\\Component\\HttpFoundation\\": ""
             ],
             "description": "Symfony HttpFoundation Component",
             "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/http-foundation/tree/v4.4.18"
+            },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-08-17T07:39:58+00:00"
+            "time": "2020-12-18T07:41:31+00:00"
         },
         {
             "name": "symfony/http-kernel",
-            "version": "v4.4.13",
+            "version": "v4.4.18",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/http-kernel.git",
-                "reference": "2bb7b90ecdc79813c0bf237b7ff20e79062b5188"
+                "reference": "eaff9a43e74513508867ecfa66ef94fbb96ab128"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/http-kernel/zipball/2bb7b90ecdc79813c0bf237b7ff20e79062b5188",
-                "reference": "2bb7b90ecdc79813c0bf237b7ff20e79062b5188",
+                "url": "https://api.github.com/repos/symfony/http-kernel/zipball/eaff9a43e74513508867ecfa66ef94fbb96ab128",
+                "reference": "eaff9a43e74513508867ecfa66ef94fbb96ab128",
                 "shasum": ""
             },
             "require": {
                 "psr/log": "~1.0",
                 "symfony/error-handler": "^4.4",
                 "symfony/event-dispatcher": "^4.4",
+                "symfony/http-client-contracts": "^1.1|^2",
                 "symfony/http-foundation": "^4.4|^5.0",
                 "symfony/polyfill-ctype": "^1.8",
                 "symfony/polyfill-php73": "^1.9",
                 "symfony/dependency-injection": ""
             },
             "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "4.4-dev"
-                }
-            },
             "autoload": {
                 "psr-4": {
                     "Symfony\\Component\\HttpKernel\\": ""
             ],
             "description": "Symfony HttpKernel Component",
             "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/http-kernel/tree/v4.4.18"
+            },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-09-02T08:09:29+00:00"
+            "time": "2020-12-18T13:32:33+00:00"
         },
         {
             "name": "symfony/mime",
-            "version": "v4.4.13",
+            "version": "v5.2.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/mime.git",
-                "reference": "50ad671306d3d3ffb888d95b4fb1859496831e3a"
+                "reference": "de97005aef7426ba008c46ba840fc301df577ada"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/mime/zipball/50ad671306d3d3ffb888d95b4fb1859496831e3a",
-                "reference": "50ad671306d3d3ffb888d95b4fb1859496831e3a",
+                "url": "https://api.github.com/repos/symfony/mime/zipball/de97005aef7426ba008c46ba840fc301df577ada",
+                "reference": "de97005aef7426ba008c46ba840fc301df577ada",
                 "shasum": ""
             },
             "require": {
-                "php": ">=7.1.3",
+                "php": ">=7.2.5",
+                "symfony/deprecation-contracts": "^2.1",
                 "symfony/polyfill-intl-idn": "^1.10",
-                "symfony/polyfill-mbstring": "^1.0"
+                "symfony/polyfill-mbstring": "^1.0",
+                "symfony/polyfill-php80": "^1.15"
             },
             "conflict": {
                 "symfony/mailer": "<4.4"
             },
             "require-dev": {
                 "egulias/email-validator": "^2.1.10",
-                "symfony/dependency-injection": "^3.4|^4.1|^5.0"
+                "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0",
+                "symfony/dependency-injection": "^4.4|^5.0",
+                "symfony/property-access": "^4.4|^5.1",
+                "symfony/property-info": "^4.4|^5.1",
+                "symfony/serializer": "^5.2"
             },
             "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "4.4-dev"
-                }
-            },
             "autoload": {
                 "psr-4": {
                     "Symfony\\Component\\Mime\\": ""
                 "mime",
                 "mime-type"
             ],
+            "support": {
+                "source": "https://github.com/symfony/mime/tree/v5.2.1"
+            },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-08-17T09:56:45+00:00"
+            "time": "2020-12-09T18:54:12+00:00"
         },
         {
             "name": "symfony/polyfill-ctype",
-            "version": "v1.18.1",
+            "version": "v1.20.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-ctype.git",
-                "reference": "1c302646f6efc070cd46856e600e5e0684d6b454"
+                "reference": "f4ba089a5b6366e453971d3aad5fe8e897b37f41"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/1c302646f6efc070cd46856e600e5e0684d6b454",
-                "reference": "1c302646f6efc070cd46856e600e5e0684d6b454",
+                "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/f4ba089a5b6366e453971d3aad5fe8e897b37f41",
+                "reference": "f4ba089a5b6366e453971d3aad5fe8e897b37f41",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.3.3"
+                "php": ">=7.1"
             },
             "suggest": {
                 "ext-ctype": "For best performance"
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.18-dev"
+                    "dev-main": "1.20-dev"
                 },
                 "thanks": {
                     "name": "symfony/polyfill",
                 "polyfill",
                 "portable"
             ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-ctype/tree/v1.20.0"
+            },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-07-14T12:35:20+00:00"
+            "time": "2020-10-23T14:02:19+00:00"
         },
         {
             "name": "symfony/polyfill-iconv",
-            "version": "v1.18.1",
+            "version": "v1.20.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-iconv.git",
-                "reference": "6c2f78eb8f5ab8eaea98f6d414a5915f2e0fce36"
+                "reference": "c536646fdb4f29104dd26effc2fdcb9a5b085024"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/6c2f78eb8f5ab8eaea98f6d414a5915f2e0fce36",
-                "reference": "6c2f78eb8f5ab8eaea98f6d414a5915f2e0fce36",
+                "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/c536646fdb4f29104dd26effc2fdcb9a5b085024",
+                "reference": "c536646fdb4f29104dd26effc2fdcb9a5b085024",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.3.3"
+                "php": ">=7.1"
             },
             "suggest": {
                 "ext-iconv": "For best performance"
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.18-dev"
+                    "dev-main": "1.20-dev"
                 },
                 "thanks": {
                     "name": "symfony/polyfill",
                 "portable",
                 "shim"
             ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-iconv/tree/v1.20.0"
+            },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-07-14T12:35:20+00:00"
+            "time": "2020-10-23T14:02:19+00:00"
         },
         {
             "name": "symfony/polyfill-intl-idn",
-            "version": "v1.18.1",
+            "version": "v1.20.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-intl-idn.git",
-                "reference": "5dcab1bc7146cf8c1beaa4502a3d9be344334251"
+                "reference": "3b75acd829741c768bc8b1f84eb33265e7cc5117"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/5dcab1bc7146cf8c1beaa4502a3d9be344334251",
-                "reference": "5dcab1bc7146cf8c1beaa4502a3d9be344334251",
+                "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/3b75acd829741c768bc8b1f84eb33265e7cc5117",
+                "reference": "3b75acd829741c768bc8b1f84eb33265e7cc5117",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.3.3",
+                "php": ">=7.1",
                 "symfony/polyfill-intl-normalizer": "^1.10",
-                "symfony/polyfill-php70": "^1.10",
                 "symfony/polyfill-php72": "^1.10"
             },
             "suggest": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.18-dev"
+                    "dev-main": "1.20-dev"
                 },
                 "thanks": {
                     "name": "symfony/polyfill",
                 "portable",
                 "shim"
             ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.20.0"
+            },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-08-04T06:02:08+00:00"
+            "time": "2020-10-23T14:02:19+00:00"
         },
         {
             "name": "symfony/polyfill-intl-normalizer",
-            "version": "v1.18.1",
+            "version": "v1.20.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-intl-normalizer.git",
-                "reference": "37078a8dd4a2a1e9ab0231af7c6cb671b2ed5a7e"
+                "reference": "727d1096295d807c309fb01a851577302394c897"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/37078a8dd4a2a1e9ab0231af7c6cb671b2ed5a7e",
-                "reference": "37078a8dd4a2a1e9ab0231af7c6cb671b2ed5a7e",
+                "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/727d1096295d807c309fb01a851577302394c897",
+                "reference": "727d1096295d807c309fb01a851577302394c897",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.3.3"
+                "php": ">=7.1"
             },
             "suggest": {
                 "ext-intl": "For best performance"
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.18-dev"
+                    "dev-main": "1.20-dev"
                 },
                 "thanks": {
                     "name": "symfony/polyfill",
                 "portable",
                 "shim"
             ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.20.0"
+            },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-07-14T12:35:20+00:00"
+            "time": "2020-10-23T14:02:19+00:00"
         },
         {
             "name": "symfony/polyfill-mbstring",
-            "version": "v1.18.1",
+            "version": "v1.20.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-mbstring.git",
-                "reference": "a6977d63bf9a0ad4c65cd352709e230876f9904a"
+                "reference": "39d483bdf39be819deabf04ec872eb0b2410b531"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/a6977d63bf9a0ad4c65cd352709e230876f9904a",
-                "reference": "a6977d63bf9a0ad4c65cd352709e230876f9904a",
+                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/39d483bdf39be819deabf04ec872eb0b2410b531",
+                "reference": "39d483bdf39be819deabf04ec872eb0b2410b531",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.3.3"
+                "php": ">=7.1"
             },
             "suggest": {
                 "ext-mbstring": "For best performance"
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.18-dev"
+                    "dev-main": "1.20-dev"
                 },
                 "thanks": {
                     "name": "symfony/polyfill",
                 "portable",
                 "shim"
             ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.20.0"
+            },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-07-14T12:35:20+00:00"
+            "time": "2020-10-23T14:02:19+00:00"
         },
         {
-            "name": "symfony/polyfill-php70",
-            "version": "v1.18.1",
+            "name": "symfony/polyfill-php72",
+            "version": "v1.20.0",
             "source": {
                 "type": "git",
-                "url": "https://github.com/symfony/polyfill-php70.git",
-                "reference": "0dd93f2c578bdc9c72697eaa5f1dd25644e618d3"
+                "url": "https://github.com/symfony/polyfill-php72.git",
+                "reference": "cede45fcdfabdd6043b3592e83678e42ec69e930"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/0dd93f2c578bdc9c72697eaa5f1dd25644e618d3",
-                "reference": "0dd93f2c578bdc9c72697eaa5f1dd25644e618d3",
+                "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/cede45fcdfabdd6043b3592e83678e42ec69e930",
+                "reference": "cede45fcdfabdd6043b3592e83678e42ec69e930",
                 "shasum": ""
             },
             "require": {
-                "paragonie/random_compat": "~1.0|~2.0|~9.99",
-                "php": ">=5.3.3"
+                "php": ">=7.1"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.18-dev"
+                    "dev-main": "1.20-dev"
                 },
                 "thanks": {
                     "name": "symfony/polyfill",
             },
             "autoload": {
                 "psr-4": {
-                    "Symfony\\Polyfill\\Php70\\": ""
+                    "Symfony\\Polyfill\\Php72\\": ""
                 },
                 "files": [
                     "bootstrap.php"
-                ],
-                "classmap": [
-                    "Resources/stubs"
                 ]
             },
             "notification-url": "https://packagist.org/downloads/",
                     "homepage": "https://symfony.com/contributors"
                 }
             ],
-            "description": "Symfony polyfill backporting some PHP 7.0+ features to lower PHP versions",
+            "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions",
             "homepage": "https://symfony.com",
             "keywords": [
                 "compatibility",
                 "portable",
                 "shim"
             ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-php72/tree/v1.20.0"
+            },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-07-14T12:35:20+00:00"
+            "time": "2020-10-23T14:02:19+00:00"
         },
         {
-            "name": "symfony/polyfill-php72",
-            "version": "v1.18.1",
+            "name": "symfony/polyfill-php73",
+            "version": "v1.20.0",
             "source": {
                 "type": "git",
-                "url": "https://github.com/symfony/polyfill-php72.git",
-                "reference": "639447d008615574653fb3bc60d1986d7172eaae"
+                "url": "https://github.com/symfony/polyfill-php73.git",
+                "reference": "8ff431c517be11c78c48a39a66d37431e26a6bed"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/639447d008615574653fb3bc60d1986d7172eaae",
-                "reference": "639447d008615574653fb3bc60d1986d7172eaae",
+                "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/8ff431c517be11c78c48a39a66d37431e26a6bed",
+                "reference": "8ff431c517be11c78c48a39a66d37431e26a6bed",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.3.3"
+                "php": ">=7.1"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.18-dev"
+                    "dev-main": "1.20-dev"
                 },
                 "thanks": {
                     "name": "symfony/polyfill",
             },
             "autoload": {
                 "psr-4": {
-                    "Symfony\\Polyfill\\Php72\\": ""
+                    "Symfony\\Polyfill\\Php73\\": ""
                 },
                 "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"
-            ],
-            "funding": [
-                {
-                    "url": "https://symfony.com/sponsor",
-                    "type": "custom"
-                },
-                {
-                    "url": "https://github.com/fabpot",
-                    "type": "github"
-                },
-                {
-                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
-                    "type": "tidelift"
-                }
-            ],
-            "time": "2020-07-14T12:35:20+00:00"
-        },
-        {
-            "name": "symfony/polyfill-php73",
-            "version": "v1.18.1",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/symfony/polyfill-php73.git",
-                "reference": "fffa1a52a023e782cdcc221d781fe1ec8f87fcca"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/fffa1a52a023e782cdcc221d781fe1ec8f87fcca",
-                "reference": "fffa1a52a023e782cdcc221d781fe1ec8f87fcca",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=5.3.3"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "1.18-dev"
-                },
-                "thanks": {
-                    "name": "symfony/polyfill",
-                    "url": "https://github.com/symfony/polyfill"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "Symfony\\Polyfill\\Php73\\": ""
-                },
-                "files": [
-                    "bootstrap.php"
-                ],
-                "classmap": [
-                    "Resources/stubs"
+                ],
+                "classmap": [
+                    "Resources/stubs"
                 ]
             },
             "notification-url": "https://packagist.org/downloads/",
                 "portable",
                 "shim"
             ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-php73/tree/v1.20.0"
+            },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-07-14T12:35:20+00:00"
+            "time": "2020-10-23T14:02:19+00:00"
         },
         {
             "name": "symfony/polyfill-php80",
-            "version": "v1.18.1",
+            "version": "v1.20.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-php80.git",
-                "reference": "d87d5766cbf48d72388a9f6b85f280c8ad51f981"
+                "reference": "e70aa8b064c5b72d3df2abd5ab1e90464ad009de"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/d87d5766cbf48d72388a9f6b85f280c8ad51f981",
-                "reference": "d87d5766cbf48d72388a9f6b85f280c8ad51f981",
+                "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/e70aa8b064c5b72d3df2abd5ab1e90464ad009de",
+                "reference": "e70aa8b064c5b72d3df2abd5ab1e90464ad009de",
                 "shasum": ""
             },
             "require": {
-                "php": ">=7.0.8"
+                "php": ">=7.1"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.18-dev"
+                    "dev-main": "1.20-dev"
                 },
                 "thanks": {
                     "name": "symfony/polyfill",
                 "portable",
                 "shim"
             ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-php80/tree/v1.20.0"
+            },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-07-14T12:35:20+00:00"
+            "time": "2020-10-23T14:02:19+00:00"
         },
         {
             "name": "symfony/process",
-            "version": "v4.4.13",
+            "version": "v4.4.18",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/process.git",
-                "reference": "65e70bab62f3da7089a8d4591fb23fbacacb3479"
+                "reference": "075316ff72233ce3d04a9743414292e834f2cb4a"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/process/zipball/65e70bab62f3da7089a8d4591fb23fbacacb3479",
-                "reference": "65e70bab62f3da7089a8d4591fb23fbacacb3479",
+                "url": "https://api.github.com/repos/symfony/process/zipball/075316ff72233ce3d04a9743414292e834f2cb4a",
+                "reference": "075316ff72233ce3d04a9743414292e834f2cb4a",
                 "shasum": ""
             },
             "require": {
                 "php": ">=7.1.3"
             },
             "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "4.4-dev"
-                }
-            },
             "autoload": {
                 "psr-4": {
                     "Symfony\\Component\\Process\\": ""
             ],
             "description": "Symfony Process Component",
             "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/process/tree/v4.4.18"
+            },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-07-23T08:31:43+00:00"
+            "time": "2020-12-08T16:59:59+00:00"
         },
         {
             "name": "symfony/routing",
-            "version": "v4.4.13",
+            "version": "v4.4.18",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/routing.git",
-                "reference": "e3387963565da9bae51d1d3ab8041646cc93bd04"
+                "reference": "80b042c20b035818daec844723e23b9825134ba0"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/routing/zipball/e3387963565da9bae51d1d3ab8041646cc93bd04",
-                "reference": "e3387963565da9bae51d1d3ab8041646cc93bd04",
+                "url": "https://api.github.com/repos/symfony/routing/zipball/80b042c20b035818daec844723e23b9825134ba0",
+                "reference": "80b042c20b035818daec844723e23b9825134ba0",
                 "shasum": ""
             },
             "require": {
                 "symfony/yaml": "For using the YAML loader"
             },
             "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "4.4-dev"
-                }
-            },
             "autoload": {
                 "psr-4": {
                     "Symfony\\Component\\Routing\\": ""
                 "uri",
                 "url"
             ],
+            "support": {
+                "source": "https://github.com/symfony/routing/tree/v4.4.18"
+            },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-08-10T07:27:51+00:00"
+            "time": "2020-12-08T16:59:59+00:00"
         },
         {
             "name": "symfony/service-contracts",
-            "version": "v1.1.9",
+            "version": "v2.2.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/service-contracts.git",
-                "reference": "b776d18b303a39f56c63747bcb977ad4b27aca26"
+                "reference": "d15da7ba4957ffb8f1747218be9e1a121fd298a1"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/service-contracts/zipball/b776d18b303a39f56c63747bcb977ad4b27aca26",
-                "reference": "b776d18b303a39f56c63747bcb977ad4b27aca26",
+                "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d15da7ba4957ffb8f1747218be9e1a121fd298a1",
+                "reference": "d15da7ba4957ffb8f1747218be9e1a121fd298a1",
                 "shasum": ""
             },
             "require": {
-                "php": ">=7.1.3",
+                "php": ">=7.2.5",
                 "psr/container": "^1.0"
             },
             "suggest": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.1-dev"
+                    "dev-master": "2.2-dev"
                 },
                 "thanks": {
                     "name": "symfony/contracts",
                 "interoperability",
                 "standards"
             ],
+            "support": {
+                "source": "https://github.com/symfony/service-contracts/tree/master"
+            },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-07-06T13:19:58+00:00"
+            "time": "2020-09-07T11:33:47+00:00"
         },
         {
             "name": "symfony/translation",
-            "version": "v4.4.13",
+            "version": "v4.4.18",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/translation.git",
-                "reference": "700e6e50174b0cdcf0fa232773bec5c314680575"
+                "reference": "c1001b7d75b3136648f94b245588209d881c6939"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/translation/zipball/700e6e50174b0cdcf0fa232773bec5c314680575",
-                "reference": "700e6e50174b0cdcf0fa232773bec5c314680575",
+                "url": "https://api.github.com/repos/symfony/translation/zipball/c1001b7d75b3136648f94b245588209d881c6939",
+                "reference": "c1001b7d75b3136648f94b245588209d881c6939",
                 "shasum": ""
             },
             "require": {
                 "symfony/yaml": ""
             },
             "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "4.4-dev"
-                }
-            },
             "autoload": {
                 "psr-4": {
                     "Symfony\\Component\\Translation\\": ""
             ],
             "description": "Symfony Translation Component",
             "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/translation/tree/v4.4.18"
+            },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-08-17T09:56:45+00:00"
+            "time": "2020-12-08T16:59:59+00:00"
         },
         {
             "name": "symfony/translation-contracts",
-            "version": "v1.1.10",
+            "version": "v2.3.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/translation-contracts.git",
-                "reference": "84180a25fad31e23bebd26ca09d89464f082cacc"
+                "reference": "e2eaa60b558f26a4b0354e1bbb25636efaaad105"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/84180a25fad31e23bebd26ca09d89464f082cacc",
-                "reference": "84180a25fad31e23bebd26ca09d89464f082cacc",
+                "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/e2eaa60b558f26a4b0354e1bbb25636efaaad105",
+                "reference": "e2eaa60b558f26a4b0354e1bbb25636efaaad105",
                 "shasum": ""
             },
             "require": {
-                "php": ">=7.1.3"
+                "php": ">=7.2.5"
             },
             "suggest": {
                 "symfony/translation-implementation": ""
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.1-dev"
+                    "dev-master": "2.3-dev"
                 },
                 "thanks": {
                     "name": "symfony/contracts",
                 "interoperability",
                 "standards"
             ],
+            "support": {
+                "source": "https://github.com/symfony/translation-contracts/tree/v2.3.0"
+            },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-09-02T16:08:58+00:00"
+            "time": "2020-09-28T13:05:58+00:00"
         },
         {
             "name": "symfony/var-dumper",
-            "version": "v4.4.13",
+            "version": "v4.4.18",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/var-dumper.git",
-                "reference": "1bef32329f3166486ab7cb88599cae4875632b99"
+                "reference": "4f31364bbc8177f2a6dbc125ac3851634ebe2a03"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/var-dumper/zipball/1bef32329f3166486ab7cb88599cae4875632b99",
-                "reference": "1bef32329f3166486ab7cb88599cae4875632b99",
+                "url": "https://api.github.com/repos/symfony/var-dumper/zipball/4f31364bbc8177f2a6dbc125ac3851634ebe2a03",
+                "reference": "4f31364bbc8177f2a6dbc125ac3851634ebe2a03",
                 "shasum": ""
             },
             "require": {
                 "Resources/bin/var-dump-server"
             ],
             "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "4.4-dev"
-                }
-            },
             "autoload": {
                 "files": [
                     "Resources/functions/dump.php"
                 "debug",
                 "dump"
             ],
+            "support": {
+                "source": "https://github.com/symfony/var-dumper/tree/v4.4.18"
+            },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-08-17T07:31:35+00:00"
+            "time": "2020-12-08T16:59:59+00:00"
         },
         {
             "name": "tijsverkoyen/css-to-inline-styles",
             ],
             "description": "CssToInlineStyles is a class that enables you to convert HTML-pages/files into HTML-pages/files with inline styles. This is very useful when you're sending emails.",
             "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles",
+            "support": {
+                "issues": "https://github.com/tijsverkoyen/CssToInlineStyles/issues",
+                "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/2.2.3"
+            },
             "time": "2020-07-13T06:12:54+00:00"
         },
         {
                 "env",
                 "environment"
             ],
+            "support": {
+                "issues": "https://github.com/vlucas/phpdotenv/issues",
+                "source": "https://github.com/vlucas/phpdotenv/tree/v3.6.7"
+            },
             "funding": [
                 {
                     "url": "https://github.com/GrahamCampbell",
                 "profiler",
                 "webprofiler"
             ],
+            "support": {
+                "issues": "https://github.com/barryvdh/laravel-debugbar/issues",
+                "source": "https://github.com/barryvdh/laravel-debugbar/tree/v3.5.1"
+            },
             "funding": [
                 {
                     "url": "https://github.com/barryvdh",
         },
         {
             "name": "barryvdh/laravel-ide-helper",
-            "version": "v2.8.1",
+            "version": "v2.8.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/barryvdh/laravel-ide-helper.git",
-                "reference": "affa55122f83575888d4ebf1728992686e8223de"
+                "reference": "5515cabea39b9cf55f98980d0f269dc9d85cfcca"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/affa55122f83575888d4ebf1728992686e8223de",
-                "reference": "affa55122f83575888d4ebf1728992686e8223de",
+                "url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/5515cabea39b9cf55f98980d0f269dc9d85cfcca",
+                "reference": "5515cabea39b9cf55f98980d0f269dc9d85cfcca",
                 "shasum": ""
             },
             "require": {
                 "barryvdh/reflection-docblock": "^2.0.6",
-                "composer/composer": "^1.6 || ^2.0@dev",
+                "composer/composer": "^1.6 || ^2",
                 "doctrine/dbal": "~2.3",
                 "ext-json": "*",
                 "illuminate/console": "^6 || ^7 || ^8",
                 "phpdocumentor/type-resolver": "^1.1.0"
             },
             "require-dev": {
+                "ext-pdo_sqlite": "*",
                 "friendsofphp/php-cs-fixer": "^2",
                 "illuminate/config": "^6 || ^7 || ^8",
                 "illuminate/view": "^6 || ^7 || ^8",
-                "mockery/mockery": "^1.3",
+                "mockery/mockery": "^1.3.3",
                 "orchestra/testbench": "^4 || ^5 || ^6",
                 "phpunit/phpunit": "^8.5 || ^9",
-                "spatie/phpunit-snapshot-assertions": "^1.4 || ^2.2 || ^3",
+                "spatie/phpunit-snapshot-assertions": "^1.4 || ^2.2 || ^3 || ^4",
                 "vimeo/psalm": "^3.12"
             },
             "type": "library",
                 "phpstorm",
                 "sublime"
             ],
+            "support": {
+                "issues": "https://github.com/barryvdh/laravel-ide-helper/issues",
+                "source": "https://github.com/barryvdh/laravel-ide-helper/tree/v2.8.2"
+            },
             "funding": [
                 {
                     "url": "https://github.com/barryvdh",
                     "type": "github"
                 }
             ],
-            "time": "2020-09-07T07:36:37+00:00"
+            "time": "2020-12-06T08:55:05+00:00"
         },
         {
             "name": "barryvdh/reflection-docblock",
                     "email": "[email protected]"
                 }
             ],
+            "support": {
+                "source": "https://github.com/barryvdh/ReflectionDocBlock/tree/v2.0.6"
+            },
             "time": "2018-12-13T10:34:14+00:00"
         },
         {
                 "ssl",
                 "tls"
             ],
+            "support": {
+                "irc": "irc://irc.freenode.org/composer",
+                "issues": "https://github.com/composer/ca-bundle/issues",
+                "source": "https://github.com/composer/ca-bundle/tree/1.2.8"
+            },
             "funding": [
                 {
                     "url": "https://packagist.com",
         },
         {
             "name": "composer/composer",
-            "version": "1.10.13",
+            "version": "2.0.8",
             "source": {
                 "type": "git",
                 "url": "https://github.com/composer/composer.git",
-                "reference": "47c841ba3b2d3fc0b4b13282cf029ea18b66d78b"
+                "reference": "62139b2806178adb979d76bd3437534a1a9fd490"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/composer/composer/zipball/47c841ba3b2d3fc0b4b13282cf029ea18b66d78b",
-                "reference": "47c841ba3b2d3fc0b4b13282cf029ea18b66d78b",
+                "url": "https://api.github.com/repos/composer/composer/zipball/62139b2806178adb979d76bd3437534a1a9fd490",
+                "reference": "62139b2806178adb979d76bd3437534a1a9fd490",
                 "shasum": ""
             },
             "require": {
                 "composer/ca-bundle": "^1.0",
-                "composer/semver": "^1.0",
+                "composer/semver": "^3.0",
                 "composer/spdx-licenses": "^1.2",
                 "composer/xdebug-handler": "^1.1",
                 "justinrainbow/json-schema": "^5.2.10",
-                "php": "^5.3.2 || ^7.0",
+                "php": "^5.3.2 || ^7.0 || ^8.0",
                 "psr/log": "^1.0",
+                "react/promise": "^1.2 || ^2.7",
                 "seld/jsonlint": "^1.4",
                 "seld/phar-utils": "^1.0",
-                "symfony/console": "^2.7 || ^3.0 || ^4.0 || ^5.0",
-                "symfony/filesystem": "^2.7 || ^3.0 || ^4.0 || ^5.0",
-                "symfony/finder": "^2.7 || ^3.0 || ^4.0 || ^5.0",
-                "symfony/process": "^2.7 || ^3.0 || ^4.0 || ^5.0"
-            },
-            "conflict": {
-                "symfony/console": "2.8.38"
+                "symfony/console": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0",
+                "symfony/filesystem": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0",
+                "symfony/finder": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0",
+                "symfony/process": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0"
             },
             "require-dev": {
                 "phpspec/prophecy": "^1.10",
-                "symfony/phpunit-bridge": "^4.2"
+                "symfony/phpunit-bridge": "^4.2 || ^5.0"
             },
             "suggest": {
                 "ext-openssl": "Enabling the openssl extension allows you to access https URLs for repositories and packages",
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.10-dev"
+                    "dev-master": "2.0-dev"
                 }
             },
             "autoload": {
                 {
                     "name": "Nils Adermann",
                     "email": "[email protected]",
-                    "homepage": "http://www.naderman.de"
+                    "homepage": "https://www.naderman.de"
                 },
                 {
                     "name": "Jordi Boggiano",
                     "email": "[email protected]",
-                    "homepage": "http://seld.be"
+                    "homepage": "https://seld.be"
                 }
             ],
             "description": "Composer helps you declare, manage and install dependencies of PHP projects. It ensures you have the right stack everywhere.",
                 "dependency",
                 "package"
             ],
+            "support": {
+                "irc": "irc://irc.freenode.org/composer",
+                "issues": "https://github.com/composer/composer/issues",
+                "source": "https://github.com/composer/composer/tree/2.0.8"
+            },
             "funding": [
                 {
                     "url": "https://packagist.com",
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-09-09T09:46:34+00:00"
+            "time": "2020-12-03T16:20:39+00:00"
         },
         {
             "name": "composer/semver",
-            "version": "1.7.0",
+            "version": "3.2.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/composer/semver.git",
-                "reference": "114f819054a2ea7db03287f5efb757e2af6e4079"
+                "reference": "a02fdf930a3c1c3ed3a49b5f63859c0c20e10464"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/composer/semver/zipball/114f819054a2ea7db03287f5efb757e2af6e4079",
-                "reference": "114f819054a2ea7db03287f5efb757e2af6e4079",
+                "url": "https://api.github.com/repos/composer/semver/zipball/a02fdf930a3c1c3ed3a49b5f63859c0c20e10464",
+                "reference": "a02fdf930a3c1c3ed3a49b5f63859c0c20e10464",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.3.2 || ^7.0"
+                "php": "^5.3.2 || ^7.0 || ^8.0"
             },
             "require-dev": {
-                "phpunit/phpunit": "^4.5 || ^5.0.5"
+                "phpstan/phpstan": "^0.12.54",
+                "symfony/phpunit-bridge": "^4.2 || ^5"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.x-dev"
+                    "dev-main": "3.x-dev"
                 }
             },
             "autoload": {
                 "validation",
                 "versioning"
             ],
+            "support": {
+                "irc": "irc://irc.freenode.org/composer",
+                "issues": "https://github.com/composer/semver/issues",
+                "source": "https://github.com/composer/semver/tree/3.2.4"
+            },
             "funding": [
                 {
                     "url": "https://packagist.com",
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-09-09T09:34:06+00:00"
+            "time": "2020-11-13T08:59:24+00:00"
         },
         {
             "name": "composer/spdx-licenses",
-            "version": "1.5.4",
+            "version": "1.5.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/composer/spdx-licenses.git",
-                "reference": "6946f785871e2314c60b4524851f3702ea4f2223"
+                "reference": "de30328a7af8680efdc03e396aad24befd513200"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/composer/spdx-licenses/zipball/6946f785871e2314c60b4524851f3702ea4f2223",
-                "reference": "6946f785871e2314c60b4524851f3702ea4f2223",
+                "url": "https://api.github.com/repos/composer/spdx-licenses/zipball/de30328a7af8680efdc03e396aad24befd513200",
+                "reference": "de30328a7af8680efdc03e396aad24befd513200",
                 "shasum": ""
             },
             "require": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.x-dev"
+                    "dev-main": "1.x-dev"
                 }
             },
             "autoload": {
                 "spdx",
                 "validator"
             ],
+            "support": {
+                "irc": "irc://irc.freenode.org/composer",
+                "issues": "https://github.com/composer/spdx-licenses/issues",
+                "source": "https://github.com/composer/spdx-licenses/tree/1.5.5"
+            },
             "funding": [
                 {
                     "url": "https://packagist.com",
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-07-15T15:35:07+00:00"
+            "time": "2020-12-03T16:04:16+00:00"
         },
         {
             "name": "composer/xdebug-handler",
-            "version": "1.4.3",
+            "version": "1.4.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/composer/xdebug-handler.git",
-                "reference": "ebd27a9866ae8254e873866f795491f02418c5a5"
+                "reference": "f28d44c286812c714741478d968104c5e604a1d4"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/ebd27a9866ae8254e873866f795491f02418c5a5",
-                "reference": "ebd27a9866ae8254e873866f795491f02418c5a5",
+                "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/f28d44c286812c714741478d968104c5e604a1d4",
+                "reference": "f28d44c286812c714741478d968104c5e604a1d4",
                 "shasum": ""
             },
             "require": {
                 "Xdebug",
                 "performance"
             ],
+            "support": {
+                "irc": "irc://irc.freenode.org/composer",
+                "issues": "https://github.com/composer/xdebug-handler/issues",
+                "source": "https://github.com/composer/xdebug-handler/tree/1.4.5"
+            },
             "funding": [
                 {
                     "url": "https://packagist.com",
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-08-19T10:27:58+00:00"
+            "time": "2020-11-13T08:04:11+00:00"
         },
         {
             "name": "doctrine/instantiator",
-            "version": "1.3.1",
+            "version": "1.4.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/doctrine/instantiator.git",
-                "reference": "f350df0268e904597e3bd9c4685c53e0e333feea"
+                "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/doctrine/instantiator/zipball/f350df0268e904597e3bd9c4685c53e0e333feea",
-                "reference": "f350df0268e904597e3bd9c4685c53e0e333feea",
+                "url": "https://api.github.com/repos/doctrine/instantiator/zipball/d56bf6102915de5702778fe20f2de3b2fe570b5b",
+                "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b",
                 "shasum": ""
             },
             "require": {
                 "php": "^7.1 || ^8.0"
             },
             "require-dev": {
-                "doctrine/coding-standard": "^6.0",
+                "doctrine/coding-standard": "^8.0",
                 "ext-pdo": "*",
                 "ext-phar": "*",
-                "phpbench/phpbench": "^0.13",
-                "phpstan/phpstan-phpunit": "^0.11",
-                "phpstan/phpstan-shim": "^0.11",
-                "phpunit/phpunit": "^7.0"
+                "phpbench/phpbench": "^0.13 || 1.0.0-alpha2",
+                "phpstan/phpstan": "^0.12",
+                "phpstan/phpstan-phpunit": "^0.12",
+                "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0"
             },
             "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "1.2.x-dev"
-                }
-            },
             "autoload": {
                 "psr-4": {
                     "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/"
                 {
                     "name": "Marco Pivetta",
                     "email": "[email protected]",
-                    "homepage": "http://ocramius.github.com/"
+                    "homepage": "https://ocramius.github.io/"
                 }
             ],
             "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors",
                 "constructor",
                 "instantiate"
             ],
+            "support": {
+                "issues": "https://github.com/doctrine/instantiator/issues",
+                "source": "https://github.com/doctrine/instantiator/tree/1.4.0"
+            },
             "funding": [
                 {
                     "url": "https://www.doctrine-project.org/sponsorship.html",
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-05-29T17:27:14+00:00"
+            "time": "2020-11-10T18:47:58+00:00"
         },
         {
-            "name": "fzaninotto/faker",
-            "version": "v1.9.1",
+            "name": "fakerphp/faker",
+            "version": "v1.13.0",
             "source": {
                 "type": "git",
-                "url": "https://github.com/fzaninotto/Faker.git",
-                "reference": "fc10d778e4b84d5bd315dad194661e091d307c6f"
+                "url": "https://github.com/FakerPHP/Faker.git",
+                "reference": "ab3f5364d01f2c2c16113442fb987d26e4004913"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/fzaninotto/Faker/zipball/fc10d778e4b84d5bd315dad194661e091d307c6f",
-                "reference": "fc10d778e4b84d5bd315dad194661e091d307c6f",
+                "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/ab3f5364d01f2c2c16113442fb987d26e4004913",
+                "reference": "ab3f5364d01f2c2c16113442fb987d26e4004913",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.3.3 || ^7.0"
+                "php": "^7.1 || ^8.0"
+            },
+            "conflict": {
+                "fzaninotto/faker": "*"
             },
             "require-dev": {
+                "bamarni/composer-bin-plugin": "^1.4.1",
                 "ext-intl": "*",
-                "phpunit/phpunit": "^4.8.35 || ^5.7",
-                "squizlabs/php_codesniffer": "^2.9.2"
+                "phpunit/phpunit": "^7.5.20 || ^8.5.8 || ^9.4.2"
             },
             "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "1.9-dev"
-                }
-            },
             "autoload": {
                 "psr-4": {
                     "Faker\\": "src/Faker/"
                 "faker",
                 "fixtures"
             ],
-            "time": "2019-12-12T13:22:17+00:00"
+            "support": {
+                "issues": "https://github.com/FakerPHP/Faker/issues",
+                "source": "https://github.com/FakerPHP/Faker/tree/v1.13.0"
+            },
+            "time": "2020-12-18T16:50:48+00:00"
         },
         {
             "name": "hamcrest/hamcrest-php",
             "keywords": [
                 "test"
             ],
+            "support": {
+                "issues": "https://github.com/hamcrest/hamcrest-php/issues",
+                "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.0.1"
+            },
             "time": "2020-07-09T08:09:16+00:00"
         },
         {
                 "json",
                 "schema"
             ],
+            "support": {
+                "issues": "https://github.com/justinrainbow/json-schema/issues",
+                "source": "https://github.com/justinrainbow/json-schema/tree/5.2.10"
+            },
             "time": "2020-05-27T16:41:55+00:00"
         },
         {
             "name": "laravel/browser-kit-testing",
-            "version": "v5.1.4",
+            "version": "v5.2.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/laravel/browser-kit-testing.git",
-                "reference": "7664a30d2dbabcdb0315bfaa867fef2df8cb8fb1"
+                "reference": "fa0efb279c009e2a276f934f8aff946caf66edc7"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/laravel/browser-kit-testing/zipball/7664a30d2dbabcdb0315bfaa867fef2df8cb8fb1",
-                "reference": "7664a30d2dbabcdb0315bfaa867fef2df8cb8fb1",
+                "url": "https://api.github.com/repos/laravel/browser-kit-testing/zipball/fa0efb279c009e2a276f934f8aff946caf66edc7",
+                "reference": "fa0efb279c009e2a276f934f8aff946caf66edc7",
                 "shasum": ""
             },
             "require": {
                 "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.5|^8.0",
+                "php": "^7.1.3|^8.0",
+                "phpunit/phpunit": "^7.5|^8.0|^9.3",
                 "symfony/console": "^4.2",
                 "symfony/css-selector": "^4.2",
                 "symfony/dom-crawler": "^4.2",
                 "laravel",
                 "testing"
             ],
-            "time": "2020-08-25T16:54:44+00:00"
+            "support": {
+                "issues": "https://github.com/laravel/browser-kit-testing/issues",
+                "source": "https://github.com/laravel/browser-kit-testing/tree/v5.2.0"
+            },
+            "time": "2020-10-30T08:49:09+00:00"
         },
         {
             "name": "maximebf/debugbar",
-            "version": "v1.16.3",
+            "version": "v1.16.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/maximebf/php-debugbar.git",
-                "reference": "1a1605b8e9bacb34cc0c6278206d699772e1d372"
+                "reference": "c86c717e4bf3c6d98422da5c38bfa7b0f494b04c"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/1a1605b8e9bacb34cc0c6278206d699772e1d372",
-                "reference": "1a1605b8e9bacb34cc0c6278206d699772e1d372",
+                "url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/c86c717e4bf3c6d98422da5c38bfa7b0f494b04c",
+                "reference": "c86c717e4bf3c6d98422da5c38bfa7b0f494b04c",
                 "shasum": ""
             },
             "require": {
-                "php": "^7.1",
+                "php": "^7.1|^8",
                 "psr/log": "^1.0",
                 "symfony/var-dumper": "^2.6|^3|^4|^5"
             },
             "require-dev": {
-                "phpunit/phpunit": "^5"
+                "phpunit/phpunit": "^7.5.20 || ^9.4.2"
             },
             "suggest": {
                 "kriswallsmith/assetic": "The best way to manage assets",
                 "debug",
                 "debugbar"
             ],
-            "time": "2020-05-06T07:06:27+00:00"
+            "support": {
+                "issues": "https://github.com/maximebf/php-debugbar/issues",
+                "source": "https://github.com/maximebf/php-debugbar/tree/v1.16.4"
+            },
+            "time": "2020-12-07T10:48:48+00:00"
         },
         {
             "name": "mockery/mockery",
                 "test double",
                 "testing"
             ],
+            "support": {
+                "issues": "https://github.com/mockery/mockery/issues",
+                "source": "https://github.com/mockery/mockery/tree/1.3.3"
+            },
             "time": "2020-08-11T18:10:21+00:00"
         },
         {
             "name": "myclabs/deep-copy",
-            "version": "1.10.1",
+            "version": "1.10.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/myclabs/DeepCopy.git",
-                "reference": "969b211f9a51aa1f6c01d1d2aef56d3bd91598e5"
+                "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/969b211f9a51aa1f6c01d1d2aef56d3bd91598e5",
-                "reference": "969b211f9a51aa1f6c01d1d2aef56d3bd91598e5",
+                "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/776f831124e9c62e1a2c601ecc52e776d8bb7220",
+                "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220",
                 "shasum": ""
             },
             "require": {
                 "object",
                 "object graph"
             ],
+            "support": {
+                "issues": "https://github.com/myclabs/DeepCopy/issues",
+                "source": "https://github.com/myclabs/DeepCopy/tree/1.10.2"
+            },
             "funding": [
                 {
                     "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-06-29T13:22:24+00:00"
+            "time": "2020-11-13T09:40:50+00:00"
         },
         {
             "name": "phar-io/manifest",
-            "version": "1.0.3",
+            "version": "2.0.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phar-io/manifest.git",
-                "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4"
+                "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phar-io/manifest/zipball/7761fcacf03b4d4f16e7ccb606d4879ca431fcf4",
-                "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4",
+                "url": "https://api.github.com/repos/phar-io/manifest/zipball/85265efd3af7ba3ca4b2a2c34dbfc5788dd29133",
+                "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133",
                 "shasum": ""
             },
             "require": {
                 "ext-dom": "*",
                 "ext-phar": "*",
-                "phar-io/version": "^2.0",
-                "php": "^5.6 || ^7.0"
+                "ext-xmlwriter": "*",
+                "phar-io/version": "^3.0.1",
+                "php": "^7.2 || ^8.0"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.0.x-dev"
+                    "dev-master": "2.0.x-dev"
                 }
             },
             "autoload": {
             "authors": [
                 {
                     "name": "Arne Blankerts",
-                    "role": "Developer",
-                    "email": "[email protected]"
+                    "email": "[email protected]",
+                    "role": "Developer"
                 },
                 {
                     "name": "Sebastian Heuer",
-                    "role": "Developer",
-                    "email": "[email protected]"
+                    "email": "[email protected]",
+                    "role": "Developer"
                 },
                 {
                     "name": "Sebastian Bergmann",
-                    "role": "Developer",
-                    "email": "[email protected]"
+                    "email": "[email protected]",
+                    "role": "Developer"
                 }
             ],
             "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)",
-            "time": "2018-07-08T19:23:20+00:00"
+            "support": {
+                "issues": "https://github.com/phar-io/manifest/issues",
+                "source": "https://github.com/phar-io/manifest/tree/master"
+            },
+            "time": "2020-06-27T14:33:11+00:00"
         },
         {
             "name": "phar-io/version",
-            "version": "2.0.1",
+            "version": "3.0.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phar-io/version.git",
-                "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6"
+                "reference": "e4782611070e50613683d2b9a57730e9a3ba5451"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phar-io/version/zipball/45a2ec53a73c70ce41d55cedef9063630abaf1b6",
-                "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6",
+                "url": "https://api.github.com/repos/phar-io/version/zipball/e4782611070e50613683d2b9a57730e9a3ba5451",
+                "reference": "e4782611070e50613683d2b9a57730e9a3ba5451",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.6 || ^7.0"
+                "php": "^7.2 || ^8.0"
             },
             "type": "library",
             "autoload": {
                 }
             ],
             "description": "Library for handling version information and constraints",
-            "time": "2018-07-08T19:19:57+00:00"
+            "support": {
+                "issues": "https://github.com/phar-io/version/issues",
+                "source": "https://github.com/phar-io/version/tree/3.0.4"
+            },
+            "time": "2020-12-13T23:18:30+00:00"
         },
         {
             "name": "phpdocumentor/reflection-common",
                 "reflection",
                 "static analysis"
             ],
+            "support": {
+                "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues",
+                "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x"
+            },
             "time": "2020-06-27T09:03:43+00:00"
         },
         {
                 }
             ],
             "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
+            "support": {
+                "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues",
+                "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/master"
+            },
             "time": "2020-09-03T19:13:55+00:00"
         },
         {
                 }
             ],
             "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names",
-            "time": "2020-09-17T18:55:26+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"
-                }
+            "support": {
+                "issues": "https://github.com/phpDocumentor/TypeResolver/issues",
+                "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.4.0"
             },
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "[email protected]",
-                    "role": "lead"
-                }
-            ],
-            "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"
+            "time": "2020-09-17T18:55:26+00:00"
         },
         {
             "name": "phpspec/prophecy",
-            "version": "1.11.1",
+            "version": "1.12.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpspec/prophecy.git",
-                "reference": "b20034be5efcdab4fb60ca3a29cba2949aead160"
+                "reference": "8ce87516be71aae9b956f81906aaf0338e0d8a2d"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpspec/prophecy/zipball/b20034be5efcdab4fb60ca3a29cba2949aead160",
-                "reference": "b20034be5efcdab4fb60ca3a29cba2949aead160",
+                "url": "https://api.github.com/repos/phpspec/prophecy/zipball/8ce87516be71aae9b956f81906aaf0338e0d8a2d",
+                "reference": "8ce87516be71aae9b956f81906aaf0338e0d8a2d",
                 "shasum": ""
             },
             "require": {
                 "doctrine/instantiator": "^1.2",
-                "php": "^7.2",
-                "phpdocumentor/reflection-docblock": "^5.0",
+                "php": "^7.2 || ~8.0, <8.1",
+                "phpdocumentor/reflection-docblock": "^5.2",
                 "sebastian/comparator": "^3.0 || ^4.0",
                 "sebastian/recursion-context": "^3.0 || ^4.0"
             },
             "require-dev": {
                 "phpspec/phpspec": "^6.0",
-                "phpunit/phpunit": "^8.0"
+                "phpunit/phpunit": "^8.0 || ^9.0 <9.3"
             },
             "type": "library",
             "extra": {
                 "spy",
                 "stub"
             ],
-            "time": "2020-07-08T12:44:21+00:00"
+            "support": {
+                "issues": "https://github.com/phpspec/prophecy/issues",
+                "source": "https://github.com/phpspec/prophecy/tree/1.12.1"
+            },
+            "time": "2020-09-29T09:10:42+00:00"
         },
         {
             "name": "phpunit/php-code-coverage",
-            "version": "7.0.10",
+            "version": "7.0.14",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
-                "reference": "f1884187926fbb755a9aaf0b3836ad3165b478bf"
+                "reference": "bb7c9a210c72e4709cdde67f8b7362f672f2225c"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/f1884187926fbb755a9aaf0b3836ad3165b478bf",
-                "reference": "f1884187926fbb755a9aaf0b3836ad3165b478bf",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/bb7c9a210c72e4709cdde67f8b7362f672f2225c",
+                "reference": "bb7c9a210c72e4709cdde67f8b7362f672f2225c",
                 "shasum": ""
             },
             "require": {
                 "ext-dom": "*",
                 "ext-xmlwriter": "*",
-                "php": "^7.2",
+                "php": ">=7.2",
                 "phpunit/php-file-iterator": "^2.0.2",
                 "phpunit/php-text-template": "^1.2.1",
-                "phpunit/php-token-stream": "^3.1.1",
+                "phpunit/php-token-stream": "^3.1.1 || ^4.0",
                 "sebastian/code-unit-reverse-lookup": "^1.0.1",
                 "sebastian/environment": "^4.2.2",
                 "sebastian/version": "^2.0.1",
                 "testing",
                 "xunit"
             ],
-            "time": "2019-11-20T13:55:58+00:00"
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
+                "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/7.0.14"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2020-12-02T13:39:03+00:00"
         },
         {
             "name": "phpunit/php-file-iterator",
-            "version": "2.0.2",
+            "version": "2.0.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/php-file-iterator.git",
-                "reference": "050bedf145a257b1ff02746c31894800e5122946"
+                "reference": "4b49fb70f067272b659ef0174ff9ca40fdaa6357"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/050bedf145a257b1ff02746c31894800e5122946",
-                "reference": "050bedf145a257b1ff02746c31894800e5122946",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/4b49fb70f067272b659ef0174ff9ca40fdaa6357",
+                "reference": "4b49fb70f067272b659ef0174ff9ca40fdaa6357",
                 "shasum": ""
             },
             "require": {
-                "php": "^7.1"
+                "php": ">=7.1"
             },
             "require-dev": {
-                "phpunit/phpunit": "^7.1"
+                "phpunit/phpunit": "^8.5"
             },
             "type": "library",
             "extra": {
                 "filesystem",
                 "iterator"
             ],
-            "time": "2018-09-13T20:33:42+00:00"
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues",
+                "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/2.0.3"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2020-11-30T08:25:21+00:00"
         },
         {
             "name": "phpunit/php-text-template",
             "keywords": [
                 "template"
             ],
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/php-text-template/issues",
+                "source": "https://github.com/sebastianbergmann/php-text-template/tree/1.2.1"
+            },
             "time": "2015-06-21T13:50:34+00:00"
         },
         {
             "name": "phpunit/php-timer",
-            "version": "2.1.2",
+            "version": "2.1.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/php-timer.git",
-                "reference": "1038454804406b0b5f5f520358e78c1c2f71501e"
+                "reference": "2454ae1765516d20c4ffe103d85a58a9a3bd5662"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/1038454804406b0b5f5f520358e78c1c2f71501e",
-                "reference": "1038454804406b0b5f5f520358e78c1c2f71501e",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/2454ae1765516d20c4ffe103d85a58a9a3bd5662",
+                "reference": "2454ae1765516d20c4ffe103d85a58a9a3bd5662",
                 "shasum": ""
             },
             "require": {
-                "php": "^7.1"
+                "php": ">=7.1"
             },
             "require-dev": {
-                "phpunit/phpunit": "^7.0"
+                "phpunit/phpunit": "^8.5"
             },
             "type": "library",
             "extra": {
             "keywords": [
                 "timer"
             ],
-            "time": "2019-06-07T04:22:29+00:00"
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/php-timer/issues",
+                "source": "https://github.com/sebastianbergmann/php-timer/tree/2.1.3"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2020-11-30T08:20:02+00:00"
         },
         {
             "name": "phpunit/php-token-stream",
-            "version": "3.1.1",
+            "version": "3.1.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/php-token-stream.git",
-                "reference": "995192df77f63a59e47f025390d2d1fdf8f425ff"
+                "reference": "472b687829041c24b25f475e14c2f38a09edf1c2"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/995192df77f63a59e47f025390d2d1fdf8f425ff",
-                "reference": "995192df77f63a59e47f025390d2d1fdf8f425ff",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/472b687829041c24b25f475e14c2f38a09edf1c2",
+                "reference": "472b687829041c24b25f475e14c2f38a09edf1c2",
                 "shasum": ""
             },
             "require": {
                 "ext-tokenizer": "*",
-                "php": "^7.1"
+                "php": ">=7.1"
             },
             "require-dev": {
                 "phpunit/phpunit": "^7.0"
             "keywords": [
                 "tokenizer"
             ],
-            "time": "2019-09-17T06:23:10+00:00"
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/php-token-stream/issues",
+                "source": "https://github.com/sebastianbergmann/php-token-stream/tree/3.1.2"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "abandoned": true,
+            "time": "2020-11-30T08:38:46+00:00"
         },
         {
             "name": "phpunit/phpunit",
-            "version": "8.5.8",
+            "version": "8.5.13",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/phpunit.git",
-                "reference": "34c18baa6a44f1d1fbf0338907139e9dce95b997"
+                "reference": "8e86be391a58104ef86037ba8a846524528d784e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/34c18baa6a44f1d1fbf0338907139e9dce95b997",
-                "reference": "34c18baa6a44f1d1fbf0338907139e9dce95b997",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/8e86be391a58104ef86037ba8a846524528d784e",
+                "reference": "8e86be391a58104ef86037ba8a846524528d784e",
                 "shasum": ""
             },
             "require": {
-                "doctrine/instantiator": "^1.2.0",
+                "doctrine/instantiator": "^1.3.1",
                 "ext-dom": "*",
                 "ext-json": "*",
                 "ext-libxml": "*",
                 "ext-mbstring": "*",
                 "ext-xml": "*",
                 "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",
+                "myclabs/deep-copy": "^1.10.0",
+                "phar-io/manifest": "^2.0.1",
+                "phar-io/version": "^3.0.2",
+                "php": ">=7.2",
+                "phpspec/prophecy": "^1.10.3",
+                "phpunit/php-code-coverage": "^7.0.12",
                 "phpunit/php-file-iterator": "^2.0.2",
                 "phpunit/php-text-template": "^1.2.1",
                 "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/environment": "^4.2.3",
+                "sebastian/exporter": "^3.1.2",
                 "sebastian/global-state": "^3.0.0",
                 "sebastian/object-enumerator": "^3.0.3",
                 "sebastian/resource-operations": "^2.0.1",
                 "testing",
                 "xunit"
             ],
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/phpunit/issues",
+                "source": "https://github.com/sebastianbergmann/phpunit/tree/8.5.13"
+            },
             "funding": [
                 {
                     "url": "https://phpunit.de/donate.html",
                     "type": "github"
                 }
             ],
-            "time": "2020-06-22T07:06:58+00:00"
+            "time": "2020-12-01T04:53:52+00:00"
+        },
+        {
+            "name": "react/promise",
+            "version": "v2.8.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/reactphp/promise.git",
+                "reference": "f3cff96a19736714524ca0dd1d4130de73dbbbc4"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/reactphp/promise/zipball/f3cff96a19736714524ca0dd1d4130de73dbbbc4",
+                "reference": "f3cff96a19736714524ca0dd1d4130de73dbbbc4",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.4.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^7.0 || ^6.5 || ^5.7 || ^4.8.36"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "React\\Promise\\": "src/"
+                },
+                "files": [
+                    "src/functions_include.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Jan Sorgalla",
+                    "email": "[email protected]"
+                }
+            ],
+            "description": "A lightweight implementation of CommonJS Promises/A for PHP",
+            "keywords": [
+                "promise",
+                "promises"
+            ],
+            "support": {
+                "issues": "https://github.com/reactphp/promise/issues",
+                "source": "https://github.com/reactphp/promise/tree/v2.8.0"
+            },
+            "time": "2020-05-12T15:16:56+00:00"
         },
         {
             "name": "sebastian/code-unit-reverse-lookup",
-            "version": "1.0.1",
+            "version": "1.0.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git",
-                "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18"
+                "reference": "1de8cd5c010cb153fcd68b8d0f64606f523f7619"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/4419fcdb5eabb9caa61a27c7a1db532a6b55dd18",
-                "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18",
+                "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/1de8cd5c010cb153fcd68b8d0f64606f523f7619",
+                "reference": "1de8cd5c010cb153fcd68b8d0f64606f523f7619",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.6 || ^7.0"
+                "php": ">=5.6"
             },
             "require-dev": {
-                "phpunit/phpunit": "^5.7 || ^6.0"
+                "phpunit/phpunit": "^8.5"
             },
             "type": "library",
             "extra": {
             ],
             "description": "Looks up which function or method a line of code belongs to",
             "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/",
-            "time": "2017-03-04T06:30:41+00:00"
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues",
+                "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/1.0.2"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2020-11-30T08:15:22+00:00"
         },
         {
             "name": "sebastian/comparator",
-            "version": "3.0.2",
+            "version": "3.0.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/comparator.git",
-                "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da"
+                "reference": "1071dfcef776a57013124ff35e1fc41ccd294758"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/5de4fc177adf9bce8df98d8d141a7559d7ccf6da",
-                "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da",
+                "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/1071dfcef776a57013124ff35e1fc41ccd294758",
+                "reference": "1071dfcef776a57013124ff35e1fc41ccd294758",
                 "shasum": ""
             },
             "require": {
-                "php": "^7.1",
+                "php": ">=7.1",
                 "sebastian/diff": "^3.0",
                 "sebastian/exporter": "^3.1"
             },
             "require-dev": {
-                "phpunit/phpunit": "^7.1"
+                "phpunit/phpunit": "^8.5"
             },
             "type": "library",
             "extra": {
                 "BSD-3-Clause"
             ],
             "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "[email protected]"
+                },
                 {
                     "name": "Jeff Welch",
                     "email": "[email protected]"
                 {
                     "name": "Bernhard Schussek",
                     "email": "[email protected]"
-                },
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "[email protected]"
                 }
             ],
             "description": "Provides the functionality to compare PHP values for equality",
                 "compare",
                 "equality"
             ],
-            "time": "2018-07-12T15:12:46+00:00"
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/comparator/issues",
+                "source": "https://github.com/sebastianbergmann/comparator/tree/3.0.3"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2020-11-30T08:04:30+00:00"
         },
         {
             "name": "sebastian/diff",
-            "version": "3.0.2",
+            "version": "3.0.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/diff.git",
-                "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29"
+                "reference": "14f72dd46eaf2f2293cbe79c93cc0bc43161a211"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/720fcc7e9b5cf384ea68d9d930d480907a0c1a29",
-                "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29",
+                "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/14f72dd46eaf2f2293cbe79c93cc0bc43161a211",
+                "reference": "14f72dd46eaf2f2293cbe79c93cc0bc43161a211",
                 "shasum": ""
             },
             "require": {
-                "php": "^7.1"
+                "php": ">=7.1"
             },
             "require-dev": {
                 "phpunit/phpunit": "^7.5 || ^8.0",
                 "BSD-3-Clause"
             ],
             "authors": [
-                {
-                    "name": "Kore Nordmann",
-                    "email": "[email protected]"
-                },
                 {
                     "name": "Sebastian Bergmann",
                     "email": "[email protected]"
+                },
+                {
+                    "name": "Kore Nordmann",
+                    "email": "[email protected]"
                 }
             ],
             "description": "Diff implementation",
                 "unidiff",
                 "unified diff"
             ],
-            "time": "2019-02-04T06:01:07+00:00"
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/diff/issues",
+                "source": "https://github.com/sebastianbergmann/diff/tree/3.0.3"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2020-11-30T07:59:04+00:00"
         },
         {
             "name": "sebastian/environment",
-            "version": "4.2.3",
+            "version": "4.2.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/environment.git",
-                "reference": "464c90d7bdf5ad4e8a6aea15c091fec0603d4368"
+                "reference": "d47bbbad83711771f167c72d4e3f25f7fcc1f8b0"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/464c90d7bdf5ad4e8a6aea15c091fec0603d4368",
-                "reference": "464c90d7bdf5ad4e8a6aea15c091fec0603d4368",
+                "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/d47bbbad83711771f167c72d4e3f25f7fcc1f8b0",
+                "reference": "d47bbbad83711771f167c72d4e3f25f7fcc1f8b0",
                 "shasum": ""
             },
             "require": {
-                "php": "^7.1"
+                "php": ">=7.1"
             },
             "require-dev": {
                 "phpunit/phpunit": "^7.5"
                 "environment",
                 "hhvm"
             ],
-            "time": "2019-11-20T08:46:58+00:00"
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/environment/issues",
+                "source": "https://github.com/sebastianbergmann/environment/tree/4.2.4"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2020-11-30T07:53:42+00:00"
         },
         {
             "name": "sebastian/exporter",
-            "version": "3.1.2",
+            "version": "3.1.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/exporter.git",
-                "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e"
+                "reference": "6b853149eab67d4da22291d36f5b0631c0fd856e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/68609e1261d215ea5b21b7987539cbfbe156ec3e",
-                "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e",
+                "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/6b853149eab67d4da22291d36f5b0631c0fd856e",
+                "reference": "6b853149eab67d4da22291d36f5b0631c0fd856e",
                 "shasum": ""
             },
             "require": {
-                "php": "^7.0",
+                "php": ">=7.0",
                 "sebastian/recursion-context": "^3.0"
             },
             "require-dev": {
                 "export",
                 "exporter"
             ],
-            "time": "2019-09-14T09:02:43+00:00"
-        },
-        {
-            "name": "sebastian/finder-facade",
-            "version": "1.2.3",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/sebastianbergmann/finder-facade.git",
-                "reference": "167c45d131f7fc3d159f56f191a0a22228765e16"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/finder-facade/zipball/167c45d131f7fc3d159f56f191a0a22228765e16",
-                "reference": "167c45d131f7fc3d159f56f191a0a22228765e16",
-                "shasum": ""
-            },
-            "require": {
-                "php": "^7.1",
-                "symfony/finder": "^2.3|^3.0|^4.0|^5.0",
-                "theseer/fdomdocument": "^1.6"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": []
-            },
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/exporter/issues",
+                "source": "https://github.com/sebastianbergmann/exporter/tree/3.1.3"
             },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
+            "funding": [
                 {
-                    "name": "Sebastian Bergmann",
-                    "email": "[email protected]",
-                    "role": "lead"
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
                 }
             ],
-            "description": "FinderFacade is a convenience wrapper for Symfony's Finder component.",
-            "homepage": "https://github.com/sebastianbergmann/finder-facade",
-            "time": "2020-01-16T08:08:45+00:00"
+            "time": "2020-11-30T07:47:53+00:00"
         },
         {
             "name": "sebastian/global-state",
-            "version": "3.0.0",
+            "version": "3.0.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/global-state.git",
-                "reference": "edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4"
+                "reference": "474fb9edb7ab891665d3bfc6317f42a0a150454b"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4",
-                "reference": "edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4",
+                "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/474fb9edb7ab891665d3bfc6317f42a0a150454b",
+                "reference": "474fb9edb7ab891665d3bfc6317f42a0a150454b",
                 "shasum": ""
             },
             "require": {
-                "php": "^7.2",
+                "php": ">=7.2",
                 "sebastian/object-reflector": "^1.1.1",
                 "sebastian/recursion-context": "^3.0"
             },
             "keywords": [
                 "global state"
             ],
-            "time": "2019-02-01T05:30:01+00:00"
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/global-state/issues",
+                "source": "https://github.com/sebastianbergmann/global-state/tree/3.0.1"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2020-11-30T07:43:24+00:00"
         },
         {
             "name": "sebastian/object-enumerator",
-            "version": "3.0.3",
+            "version": "3.0.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/object-enumerator.git",
-                "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5"
+                "reference": "e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/7cfd9e65d11ffb5af41198476395774d4c8a84c5",
-                "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5",
+                "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2",
+                "reference": "e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2",
                 "shasum": ""
             },
             "require": {
-                "php": "^7.0",
+                "php": ">=7.0",
                 "sebastian/object-reflector": "^1.1.1",
                 "sebastian/recursion-context": "^3.0"
             },
             ],
             "description": "Traverses array structures and object graphs to enumerate all referenced objects",
             "homepage": "https://github.com/sebastianbergmann/object-enumerator/",
-            "time": "2017-08-03T12:35:26+00:00"
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/object-enumerator/issues",
+                "source": "https://github.com/sebastianbergmann/object-enumerator/tree/3.0.4"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2020-11-30T07:40:27+00:00"
         },
         {
             "name": "sebastian/object-reflector",
-            "version": "1.1.1",
+            "version": "1.1.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/object-reflector.git",
-                "reference": "773f97c67f28de00d397be301821b06708fca0be"
+                "reference": "9b8772b9cbd456ab45d4a598d2dd1a1bced6363d"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/773f97c67f28de00d397be301821b06708fca0be",
-                "reference": "773f97c67f28de00d397be301821b06708fca0be",
+                "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/9b8772b9cbd456ab45d4a598d2dd1a1bced6363d",
+                "reference": "9b8772b9cbd456ab45d4a598d2dd1a1bced6363d",
                 "shasum": ""
             },
             "require": {
-                "php": "^7.0"
+                "php": ">=7.0"
             },
             "require-dev": {
                 "phpunit/phpunit": "^6.0"
             ],
             "description": "Allows reflection of object attributes, including inherited and non-public ones",
             "homepage": "https://github.com/sebastianbergmann/object-reflector/",
-            "time": "2017-03-29T09:07:27+00:00"
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/object-reflector/issues",
+                "source": "https://github.com/sebastianbergmann/object-reflector/tree/1.1.2"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2020-11-30T07:37:18+00:00"
         },
         {
             "name": "sebastian/recursion-context",
-            "version": "3.0.0",
+            "version": "3.0.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/recursion-context.git",
-                "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8"
+                "reference": "367dcba38d6e1977be014dc4b22f47a484dac7fb"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8",
-                "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8",
+                "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/367dcba38d6e1977be014dc4b22f47a484dac7fb",
+                "reference": "367dcba38d6e1977be014dc4b22f47a484dac7fb",
                 "shasum": ""
             },
             "require": {
-                "php": "^7.0"
+                "php": ">=7.0"
             },
             "require-dev": {
                 "phpunit/phpunit": "^6.0"
                 "BSD-3-Clause"
             ],
             "authors": [
-                {
-                    "name": "Jeff Welch",
-                    "email": "[email protected]"
-                },
                 {
                     "name": "Sebastian Bergmann",
                     "email": "[email protected]"
                 },
+                {
+                    "name": "Jeff Welch",
+                    "email": "[email protected]"
+                },
                 {
                     "name": "Adam Harvey",
                     "email": "[email protected]"
             ],
             "description": "Provides functionality to recursively process PHP variables",
             "homepage": "http://www.github.com/sebastianbergmann/recursion-context",
-            "time": "2017-03-03T06:23:57+00:00"
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/recursion-context/issues",
+                "source": "https://github.com/sebastianbergmann/recursion-context/tree/3.0.1"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2020-11-30T07:34:24+00:00"
         },
         {
             "name": "sebastian/resource-operations",
-            "version": "2.0.1",
+            "version": "2.0.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/resource-operations.git",
-                "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9"
+                "reference": "31d35ca87926450c44eae7e2611d45a7a65ea8b3"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/4d7a795d35b889bf80a0cc04e08d77cedfa917a9",
-                "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9",
+                "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/31d35ca87926450c44eae7e2611d45a7a65ea8b3",
+                "reference": "31d35ca87926450c44eae7e2611d45a7a65ea8b3",
                 "shasum": ""
             },
             "require": {
-                "php": "^7.1"
+                "php": ">=7.1"
             },
             "type": "library",
             "extra": {
             ],
             "description": "Provides a list of PHP built-in functions that operate on resources",
             "homepage": "https://www.github.com/sebastianbergmann/resource-operations",
-            "time": "2018-10-04T04:07:39+00:00"
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/resource-operations/issues",
+                "source": "https://github.com/sebastianbergmann/resource-operations/tree/2.0.2"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2020-11-30T07:30:19+00:00"
         },
         {
             "name": "sebastian/type",
-            "version": "1.1.3",
+            "version": "1.1.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/type.git",
-                "reference": "3aaaa15fa71d27650d62a948be022fe3b48541a3"
+                "reference": "0150cfbc4495ed2df3872fb31b26781e4e077eb4"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/3aaaa15fa71d27650d62a948be022fe3b48541a3",
-                "reference": "3aaaa15fa71d27650d62a948be022fe3b48541a3",
+                "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/0150cfbc4495ed2df3872fb31b26781e4e077eb4",
+                "reference": "0150cfbc4495ed2df3872fb31b26781e4e077eb4",
                 "shasum": ""
             },
             "require": {
-                "php": "^7.2"
+                "php": ">=7.2"
             },
             "require-dev": {
                 "phpunit/phpunit": "^8.2"
             ],
             "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"
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/type/issues",
+                "source": "https://github.com/sebastianbergmann/type/tree/1.1.4"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2020-11-30T07:25:11+00:00"
         },
         {
             "name": "sebastian/version",
             ],
             "description": "Library that helps with managing the version number of Git-hosted PHP projects",
             "homepage": "https://github.com/sebastianbergmann/version",
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/version/issues",
+                "source": "https://github.com/sebastianbergmann/version/tree/master"
+            },
             "time": "2016-10-03T07:35:21+00:00"
         },
         {
             "name": "seld/jsonlint",
-            "version": "1.8.2",
+            "version": "1.8.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/Seldaek/jsonlint.git",
-                "reference": "590cfec960b77fd55e39b7d9246659e95dd6d337"
+                "reference": "9ad6ce79c342fbd44df10ea95511a1b24dee5b57"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/590cfec960b77fd55e39b7d9246659e95dd6d337",
-                "reference": "590cfec960b77fd55e39b7d9246659e95dd6d337",
+                "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/9ad6ce79c342fbd44df10ea95511a1b24dee5b57",
+                "reference": "9ad6ce79c342fbd44df10ea95511a1b24dee5b57",
                 "shasum": ""
             },
             "require": {
                 "parser",
                 "validator"
             ],
+            "support": {
+                "issues": "https://github.com/Seldaek/jsonlint/issues",
+                "source": "https://github.com/Seldaek/jsonlint/tree/1.8.3"
+            },
             "funding": [
                 {
                     "url": "https://github.com/Seldaek",
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-08-25T06:56:57+00:00"
+            "time": "2020-11-11T09:19:24+00:00"
         },
         {
             "name": "seld/phar-utils",
             "keywords": [
                 "phar"
             ],
+            "support": {
+                "issues": "https://github.com/Seldaek/phar-utils/issues",
+                "source": "https://github.com/Seldaek/phar-utils/tree/master"
+            },
             "time": "2020-07-07T18:42:57+00:00"
         },
         {
             "name": "squizlabs/php_codesniffer",
-            "version": "3.5.6",
+            "version": "3.5.8",
             "source": {
                 "type": "git",
                 "url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
-                "reference": "e97627871a7eab2f70e59166072a6b767d5834e0"
+                "reference": "9d583721a7157ee997f235f327de038e7ea6dac4"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/e97627871a7eab2f70e59166072a6b767d5834e0",
-                "reference": "e97627871a7eab2f70e59166072a6b767d5834e0",
+                "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/9d583721a7157ee997f235f327de038e7ea6dac4",
+                "reference": "9d583721a7157ee997f235f327de038e7ea6dac4",
                 "shasum": ""
             },
             "require": {
                 "phpcs",
                 "standards"
             ],
-            "time": "2020-08-10T04:50:15+00:00"
+            "support": {
+                "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues",
+                "source": "https://github.com/squizlabs/PHP_CodeSniffer",
+                "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki"
+            },
+            "time": "2020-10-23T02:01:07+00:00"
         },
         {
             "name": "symfony/dom-crawler",
-            "version": "v4.4.13",
+            "version": "v4.4.18",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/dom-crawler.git",
-                "reference": "6dd1e7adef4b7efeeb9691fd619279027d4dcf85"
+                "reference": "d44fbb02b458fe18d00fea18f24c97cefb87577e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/6dd1e7adef4b7efeeb9691fd619279027d4dcf85",
-                "reference": "6dd1e7adef4b7efeeb9691fd619279027d4dcf85",
+                "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/d44fbb02b458fe18d00fea18f24c97cefb87577e",
+                "reference": "d44fbb02b458fe18d00fea18f24c97cefb87577e",
                 "shasum": ""
             },
             "require": {
                 "symfony/css-selector": ""
             },
             "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "4.4-dev"
-                }
-            },
             "autoload": {
                 "psr-4": {
                     "Symfony\\Component\\DomCrawler\\": ""
             ],
             "description": "Symfony DomCrawler Component",
             "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/dom-crawler/tree/v4.4.18"
+            },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-08-12T06:20:35+00:00"
+            "time": "2020-12-18T07:41:31+00:00"
         },
         {
             "name": "symfony/filesystem",
-            "version": "v4.4.13",
+            "version": "v5.2.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/filesystem.git",
-                "reference": "27575bcbc68db1f6d06218891296572c9b845704"
+                "reference": "fa8f8cab6b65e2d99a118e082935344c5ba8c60d"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/filesystem/zipball/27575bcbc68db1f6d06218891296572c9b845704",
-                "reference": "27575bcbc68db1f6d06218891296572c9b845704",
+                "url": "https://api.github.com/repos/symfony/filesystem/zipball/fa8f8cab6b65e2d99a118e082935344c5ba8c60d",
+                "reference": "fa8f8cab6b65e2d99a118e082935344c5ba8c60d",
                 "shasum": ""
             },
             "require": {
-                "php": ">=7.1.3",
+                "php": ">=7.2.5",
                 "symfony/polyfill-ctype": "~1.8"
             },
             "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "4.4-dev"
-                }
-            },
             "autoload": {
                 "psr-4": {
                     "Symfony\\Component\\Filesystem\\": ""
             ],
             "description": "Symfony Filesystem Component",
             "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/filesystem/tree/v5.2.1"
+            },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-08-21T17:19:37+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"
+            "time": "2020-11-30T17:05:38+00:00"
         },
         {
             "name": "theseer/tokenizer",
                 }
             ],
             "description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
+            "support": {
+                "issues": "https://github.com/theseer/tokenizer/issues",
+                "source": "https://github.com/theseer/tokenizer/tree/master"
+            },
             "funding": [
                 {
                     "url": "https://github.com/theseer",
                 "check",
                 "validate"
             ],
-            "time": "2020-07-08T17:02:28+00:00"
-        },
-        {
-            "name": "wnx/laravel-stats",
-            "version": "v2.0.2",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/stefanzweifel/laravel-stats.git",
-                "reference": "e86ebfdd149383b18a41fe3efa1601d82d447140"
+            "support": {
+                "issues": "https://github.com/webmozart/assert/issues",
+                "source": "https://github.com/webmozart/assert/tree/master"
             },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/stefanzweifel/laravel-stats/zipball/e86ebfdd149383b18a41fe3efa1601d82d447140",
-                "reference": "e86ebfdd149383b18a41fe3efa1601d82d447140",
-                "shasum": ""
-            },
-            "require": {
-                "illuminate/console": "~5.8.0|^6.0|^7.0",
-                "illuminate/support": "~5.8.0|^6.0|^7.0",
-                "php": ">=7.2.0",
-                "phploc/phploc": "~5.0|~6.0",
-                "symfony/finder": "~4.0"
-            },
-            "require-dev": {
-                "friendsofphp/php-cs-fixer": "^2.15",
-                "laravel/browser-kit-testing": "~5.0",
-                "laravel/dusk": "~5.0",
-                "mockery/mockery": "^1.1",
-                "orchestra/testbench": "^3.8|^4.0|^5.0",
-                "phpunit/phpunit": "8.*|9.*"
-            },
-            "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": "2020-02-22T19:09:14+00:00"
+            "time": "2020-07-08T17:02:28+00:00"
         }
     ],
     "aliases": [],
     "prefer-stable": true,
     "prefer-lowest": false,
     "platform": {
-        "php": "^7.2",
+        "php": "^7.2.5",
         "ext-curl": "*",
         "ext-dom": "*",
         "ext-gd": "*",
         "ext-json": "*",
         "ext-mbstring": "*",
-        "ext-tidy": "*",
         "ext-xml": "*"
     },
     "platform-dev": [],
     "platform-overrides": {
-        "php": "7.2.0"
+        "php": "7.2.5"
     },
-    "plugin-api-version": "1.1.0"
+    "plugin-api-version": "2.0.0"
 }
index ddf3c295d22e09023fad7e62eaefc5c198f454a1..405e5fcf4490a062408dd79569fd41099847ead3 100644 (file)
@@ -21,7 +21,7 @@ $factory->define(\BookStack\Auth\User::class, function ($faker) {
     ];
 });
 
-$factory->define(\BookStack\Entities\Bookshelf::class, function ($faker) {
+$factory->define(\BookStack\Entities\Models\Bookshelf::class, function ($faker) {
     return [
         'name' => $faker->sentence,
         'slug' => Str::random(10),
@@ -29,7 +29,7 @@ $factory->define(\BookStack\Entities\Bookshelf::class, function ($faker) {
     ];
 });
 
-$factory->define(\BookStack\Entities\Book::class, function ($faker) {
+$factory->define(\BookStack\Entities\Models\Book::class, function ($faker) {
     return [
         'name' => $faker->sentence,
         'slug' => Str::random(10),
@@ -37,7 +37,7 @@ $factory->define(\BookStack\Entities\Book::class, function ($faker) {
     ];
 });
 
-$factory->define(\BookStack\Entities\Chapter::class, function ($faker) {
+$factory->define(\BookStack\Entities\Models\Chapter::class, function ($faker) {
     return [
         'name' => $faker->sentence,
         'slug' => Str::random(10),
@@ -45,7 +45,7 @@ $factory->define(\BookStack\Entities\Chapter::class, function ($faker) {
     ];
 });
 
-$factory->define(\BookStack\Entities\Page::class, function ($faker) {
+$factory->define(\BookStack\Entities\Models\Page::class, function ($faker) {
     $html = '<p>' . implode('</p>', $faker->paragraphs(5)) . '</p>';
     return [
         'name' => $faker->sentence,
index eab3216bbdfa6cc02e2b180ff094683a73f5cfdd..9efba0071c3689c6fe5ce13cdc8faa59ae0f7142 100644 (file)
@@ -119,11 +119,11 @@ class CreateBookshelvesTable extends Migration
         Schema::dropIfExists('bookshelves');
 
         // Drop related polymorphic items
-        DB::table('activities')->where('entity_type', '=', 'BookStack\Entities\Bookshelf')->delete();
-        DB::table('views')->where('viewable_type', '=', 'BookStack\Entities\Bookshelf')->delete();
-        DB::table('entity_permissions')->where('restrictable_type', '=', 'BookStack\Entities\Bookshelf')->delete();
-        DB::table('tags')->where('entity_type', '=', 'BookStack\Entities\Bookshelf')->delete();
-        DB::table('search_terms')->where('entity_type', '=', 'BookStack\Entities\Bookshelf')->delete();
-        DB::table('comments')->where('entity_type', '=', 'BookStack\Entities\Bookshelf')->delete();
+        DB::table('activities')->where('entity_type', '=', 'BookStack\Entities\Models\Bookshelf')->delete();
+        DB::table('views')->where('viewable_type', '=', 'BookStack\Entities\Models\Bookshelf')->delete();
+        DB::table('entity_permissions')->where('restrictable_type', '=', 'BookStack\Entities\Models\Bookshelf')->delete();
+        DB::table('tags')->where('entity_type', '=', 'BookStack\Entities\Models\Bookshelf')->delete();
+        DB::table('search_terms')->where('entity_type', '=', 'BookStack\Entities\Models\Bookshelf')->delete();
+        DB::table('comments')->where('entity_type', '=', 'BookStack\Entities\Models\Bookshelf')->delete();
     }
 }
diff --git a/database/migrations/2020_09_27_210059_add_entity_soft_deletes.php b/database/migrations/2020_09_27_210059_add_entity_soft_deletes.php
new file mode 100644 (file)
index 0000000..d2b63e8
--- /dev/null
@@ -0,0 +1,50 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class AddEntitySoftDeletes extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::table('bookshelves', function(Blueprint  $table) {
+            $table->softDeletes();
+        });
+        Schema::table('books', function(Blueprint  $table) {
+            $table->softDeletes();
+        });
+        Schema::table('chapters', function(Blueprint  $table) {
+            $table->softDeletes();
+        });
+        Schema::table('pages', function(Blueprint  $table) {
+            $table->softDeletes();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::table('bookshelves', function(Blueprint  $table) {
+            $table->dropSoftDeletes();
+        });
+        Schema::table('books', function(Blueprint  $table) {
+            $table->dropSoftDeletes();
+        });
+        Schema::table('chapters', function(Blueprint  $table) {
+            $table->dropSoftDeletes();
+        });
+        Schema::table('pages', function(Blueprint  $table) {
+            $table->dropSoftDeletes();
+        });
+    }
+}
diff --git a/database/migrations/2020_09_27_210528_create_deletions_table.php b/database/migrations/2020_09_27_210528_create_deletions_table.php
new file mode 100644 (file)
index 0000000..c38a935
--- /dev/null
@@ -0,0 +1,38 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class CreateDeletionsTable extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::create('deletions', function (Blueprint $table) {
+            $table->increments('id');
+            $table->integer('deleted_by');
+            $table->string('deletable_type', 100);
+            $table->integer('deletable_id');
+            $table->timestamps();
+
+            $table->index('deleted_by');
+            $table->index('deletable_type');
+            $table->index('deletable_id');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::dropIfExists('deletions');
+    }
+}
diff --git a/database/migrations/2020_11_07_232321_simplify_activities_table.php b/database/migrations/2020_11_07_232321_simplify_activities_table.php
new file mode 100644 (file)
index 0000000..828dbc6
--- /dev/null
@@ -0,0 +1,58 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+use Illuminate\Support\Facades\DB;
+
+class SimplifyActivitiesTable extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::table('activities', function (Blueprint $table) {
+            $table->renameColumn('key', 'type');
+            $table->renameColumn('extra', 'detail');
+            $table->dropColumn('book_id');
+            $table->integer('entity_id')->nullable()->change();
+            $table->string('entity_type', 191)->nullable()->change();
+        });
+
+        DB::table('activities')
+            ->where('entity_id', '=', 0)
+            ->update([
+                'entity_id' => null,
+                'entity_type' => null,
+            ]);
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        DB::table('activities')
+            ->whereNull('entity_id')
+            ->update([
+                'entity_id' => 0,
+                'entity_type' => '',
+            ]);
+
+        Schema::table('activities', function (Blueprint $table) {
+            $table->renameColumn('type', 'key');
+            $table->renameColumn('detail', 'extra');
+            $table->integer('book_id');
+
+            $table->integer('entity_id')->change();
+            $table->string('entity_type', 191)->change();
+
+            $table->index('book_id');
+        });
+    }
+}
diff --git a/database/migrations/2020_12_30_173528_add_owned_by_field_to_entities.php b/database/migrations/2020_12_30_173528_add_owned_by_field_to_entities.php
new file mode 100644 (file)
index 0000000..bf8bf28
--- /dev/null
@@ -0,0 +1,49 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+use Illuminate\Support\Facades\DB;
+
+class AddOwnedByFieldToEntities extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        $tables = ['pages', 'books', 'chapters', 'bookshelves'];
+        foreach ($tables as $table) {
+            Schema::table($table, function (Blueprint $table) {
+                $table->integer('owned_by')->unsigned()->index();
+            });
+
+            DB::table($table)->update(['owned_by' => DB::raw('`created_by`')]);
+        }
+
+        Schema::table('joint_permissions', function (Blueprint $table) {
+            $table->renameColumn('created_by', 'owned_by');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        $tables = ['pages', 'books', 'chapters', 'bookshelves'];
+        foreach ($tables as $table) {
+            Schema::table($table, function (Blueprint $table) {
+                $table->dropColumn('owned_by');
+            });
+        }
+
+        Schema::table('joint_permissions', function (Blueprint $table) {
+            $table->renameColumn('owned_by', 'created_by');
+        });
+    }
+}
index 6d902a19632a7e7f7983d6710550854654faaf4e..611c05246244b426c3c8d711b29909bb8ed6256b 100644 (file)
@@ -5,10 +5,10 @@ use BookStack\Auth\Permissions\PermissionService;
 use BookStack\Auth\Permissions\RolePermission;
 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 BookStack\Entities\Models\Bookshelf;
+use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Models\Page;
+use BookStack\Entities\Tools\SearchIndex;
 use Illuminate\Database\Seeder;
 use Illuminate\Support\Str;
 
@@ -31,9 +31,9 @@ class DummyContentSeeder extends Seeder
         $role = Role::getRole('viewer');
         $viewerUser->attachRole($role);
 
-        $byData = ['created_by' => $editorUser->id, 'updated_by' => $editorUser->id];
+        $byData = ['created_by' => $editorUser->id, 'updated_by' => $editorUser->id, 'owned_by' => $editorUser->id];
 
-        factory(\BookStack\Entities\Book::class, 5)->create($byData)
+        factory(\BookStack\Entities\Models\Book::class, 5)->create($byData)
             ->each(function($book) use ($editorUser, $byData) {
                 $chapters = factory(Chapter::class, 3)->create($byData)
                     ->each(function($chapter) use ($editorUser, $book, $byData){
@@ -45,7 +45,7 @@ class DummyContentSeeder extends Seeder
                 $book->pages()->saveMany($pages);
             });
 
-        $largeBook = factory(\BookStack\Entities\Book::class)->create(array_merge($byData, ['name' => 'Large book' . Str::random(10)]));
+        $largeBook = factory(\BookStack\Entities\Models\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);
@@ -67,6 +67,6 @@ class DummyContentSeeder extends Seeder
         $token->save();
 
         app(PermissionService::class)->buildJointPermissions();
-        app(SearchService::class)->indexAllEntities();
+        app(SearchIndex::class)->indexAllEntities();
     }
 }
index 4db10395adf037a48aac650a19b2cc02748f3f84..535626b8f794e8c74fb3d28a8b0bfe6ce7612975 100644 (file)
@@ -3,9 +3,9 @@
 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 BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Models\Page;
+use BookStack\Entities\Tools\SearchIndex;
 use Illuminate\Database\Seeder;
 use Illuminate\Support\Str;
 
@@ -23,12 +23,12 @@ class LargeContentSeeder extends Seeder
         $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]);
+        $largeBook = factory(\BookStack\Entities\Models\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(PermissionService::class)->buildJointPermissions();
-        app(SearchService::class)->indexAllEntities();
+        app(SearchIndex::class)->indexAllEntities();
     }
 }
diff --git a/dev/api/requests/pages-create.json b/dev/api/requests/pages-create.json
new file mode 100644 (file)
index 0000000..1f53b42
--- /dev/null
@@ -0,0 +1,9 @@
+{
+       "book_id": 1,
+       "name": "My API Page",
+       "html": "<p>my new API page</p>",
+       "tags": [
+               {"name": "Category", "value": "Not Bad Content"},
+               {"name": "Rating", "value": "Average"}
+       ]
+}
\ No newline at end of file
diff --git a/dev/api/requests/pages-update.json b/dev/api/requests/pages-update.json
new file mode 100644 (file)
index 0000000..b9bfeb6
--- /dev/null
@@ -0,0 +1,9 @@
+{
+       "chapter_id": 1,
+       "name": "My updated API Page",
+       "html": "<p>my new API page - Updated</p>",
+       "tags": [
+               {"name": "Category", "value": "API Examples"},
+               {"name": "Rating", "value": "Alright"}
+       ]
+}
\ No newline at end of file
index 2e43f5f87fc810163bc8323f53e304c6cbb070db..815a71c3573d139806145654a5547ef21dccd410 100644 (file)
   "tags": [
     {
       "id": 13,
-      "entity_id": 16,
-      "entity_type": "BookStack\\Book",
       "name": "Category",
       "value": "Guide",
-      "order": 0,
-      "created_at": "2020-01-12 14:11:51",
-      "updated_at": "2020-01-12 14:11:51"
+      "order": 0
     }
   ],
   "cover": {
index 2eddad8955070a36b15df967df96a3b7e7aa7dd7..0d16f4b6a30ffadc438e0c3dc9e7d0eafc6b5c98 100644 (file)
@@ -19,9 +19,7 @@
     {
       "name": "Category",
       "value": "Guide",
-      "order": 0,
-      "created_at": "2020-05-22 22:51:51",
-      "updated_at": "2020-05-22 22:51:51"
+      "order": 0
     }
   ],
   "pages": [
@@ -36,9 +34,9 @@
       "updated_at": "2019-08-26 14:32:59",
       "created_by": 1,
       "updated_by": 1,
-      "draft": 0,
+      "draft": false,
       "revision_count": 2,
-      "template": 0
+      "template": false
     },
     {
       "id": 7,
@@ -51,9 +49,9 @@
       "updated_at": "2019-06-06 12:03:04",
       "created_by": 3,
       "updated_by": 3,
-      "draft": 0,
+      "draft": false,
       "revision_count": 1,
-      "template": 0
+      "template": false
     }
   ]
 }
\ No newline at end of file
diff --git a/dev/api/responses/pages-create.json b/dev/api/responses/pages-create.json
new file mode 100644 (file)
index 0000000..1f6c970
--- /dev/null
@@ -0,0 +1,35 @@
+{
+       "id": 358,
+       "book_id": 1,
+       "chapter_id": 0,
+       "name": "My API Page",
+       "slug": "my-api-page",
+       "html": "<p id=\"bkmrk-my-new-api-page\">my new API page</p>",
+       "priority": 14,
+       "created_at": "2020-11-28 15:01:39",
+       "updated_at": "2020-11-28 15:01:39",
+       "created_by": {
+               "id": 1,
+               "name": "Admin"
+       },
+       "updated_by": {
+               "id": 1,
+               "name": "Admin"
+       },
+       "draft": false,
+       "markdown": "",
+       "revision_count": 1,
+       "template": false,
+       "tags": [
+               {
+                       "name": "Category",
+                       "value": "Not Bad Content",
+                       "order": 0
+               },
+               {
+                       "name": "Rating",
+                       "value": "Average",
+                       "order": 1
+               }
+       ]
+}
\ No newline at end of file
diff --git a/dev/api/responses/pages-list.json b/dev/api/responses/pages-list.json
new file mode 100644 (file)
index 0000000..9739345
--- /dev/null
@@ -0,0 +1,47 @@
+{
+       "data": [
+               {
+                       "id": 1,
+                       "book_id": 1,
+                       "chapter_id": 1,
+                       "name": "How to create page content",
+                       "slug": "how-to-create-page-content",
+                       "priority": 0,
+                       "draft": false,
+                       "template": false,
+                       "created_at": "2019-05-05 21:49:58",
+                       "updated_at": "2020-07-04 15:50:58",
+                       "created_by": 1,
+                       "updated_by": 1
+               },
+               {
+                       "id": 2,
+                       "book_id": 1,
+                       "chapter_id": 1,
+                       "name": "How to use images",
+                       "slug": "how-to-use-images",
+                       "priority": 2,
+                       "draft": false,
+                       "template": false,
+                       "created_at": "2019-05-05 21:53:30",
+                       "updated_at": "2019-06-06 12:03:04",
+                       "created_by": 1,
+                       "updated_by": 1
+               },
+               {
+                       "id": 3,
+                       "book_id": 1,
+                       "chapter_id": 1,
+                       "name": "Drawings via draw.io",
+                       "slug": "drawings-via-drawio",
+                       "priority": 3,
+                       "draft": false,
+                       "template": false,
+                       "created_at": "2019-05-05 21:53:49",
+                       "updated_at": "2019-12-18 21:56:52",
+                       "created_by": 1,
+                       "updated_by": 1
+               }
+       ],
+       "total": 322
+}
\ No newline at end of file
diff --git a/dev/api/responses/pages-read.json b/dev/api/responses/pages-read.json
new file mode 100644 (file)
index 0000000..c8acb52
--- /dev/null
@@ -0,0 +1,35 @@
+{
+       "id": 306,
+       "book_id": 1,
+       "chapter_id": 0,
+       "name": "A page written in markdown",
+       "slug": "a-page-written-in-markdown",
+       "html": "<h1 id=\"bkmrk-how-this-is-built\">How this is built</h1>\r\n<p id=\"bkmrk-this-page-is-written\">This page is written in markdown. BookStack stores the page data in HTML.</p>\r\n<p id=\"bkmrk-here%27s-a-cute-pictur\">Here's a cute picture of my cat:</p>\r\n<p id=\"bkmrk-\"><a href=\"http://example.com/uploads/images/gallery/2020-04/yXSrubes.jpg\"><img src=\"http://example.com/uploads/images/gallery/2020-04/scaled-1680-/yXSrubes.jpg\" alt=\"yXSrubes.jpg\"></a></p>",
+       "priority": 13,
+       "created_at": "2020-02-02 21:40:38",
+       "updated_at": "2020-11-28 14:43:20",
+       "created_by": {
+               "id": 1,
+               "name": "Admin"
+       },
+       "updated_by": {
+               "id": 1,
+               "name": "Admin"
+       },
+       "draft": false,
+       "markdown": "# How this is built\r\n\r\nThis page is written in markdown. BookStack stores the page data in HTML.\r\n\r\nHere's a cute picture of my cat:\r\n\r\n[![yXSrubes.jpg](http://example.com/uploads/images/gallery/2020-04/scaled-1680-/yXSrubes.jpg)](http://example.com/uploads/images/gallery/2020-04/yXSrubes.jpg)",
+       "revision_count": 5,
+       "template": false,
+       "tags": [
+               {
+                       "name": "Category",
+                       "value": "Top Content",
+                       "order": 0
+               },
+               {
+                       "name": "Animal",
+                       "value": "Cat",
+                       "order": 1
+               }
+       ]
+}
\ No newline at end of file
diff --git a/dev/api/responses/pages-update.json b/dev/api/responses/pages-update.json
new file mode 100644 (file)
index 0000000..23f8d22
--- /dev/null
@@ -0,0 +1,35 @@
+{
+       "id": 361,
+       "book_id": 1,
+       "chapter_id": 1,
+       "name": "My updated API Page",
+       "slug": "my-updated-api-page",
+       "html": "<p id=\"bkmrk-my-new-api-page---up\">my new API page - Updated</p>",
+       "priority": 16,
+       "created_at": "2020-11-28 15:10:54",
+       "updated_at": "2020-11-28 15:13:03",
+       "created_by": {
+               "id": 1,
+               "name": "Admin"
+       },
+       "updated_by": {
+               "id": 1,
+               "name": "Admin"
+       },
+       "draft": false,
+       "markdown": "",
+       "revision_count": 5,
+       "template": false,
+       "tags": [
+               {
+                       "name": "Category",
+                       "value": "API Examples",
+                       "order": 0
+               },
+               {
+                       "name": "Rating",
+                       "value": "Alright",
+                       "order": 0
+               }
+       ]
+}
\ No newline at end of file
index 634fbb5a53c6fde235e72c2516b111a73f645451..b0487debe7f0a98796beab3b9b5892c4d1f0aab8 100644 (file)
   "tags": [
     {
       "id": 16,
-      "entity_id": 14,
-      "entity_type": "BookStack\\Bookshelf",
       "name": "Category",
       "value": "Guide",
-      "order": 0,
-      "created_at": "2020-04-10 13:31:04",
-      "updated_at": "2020-04-10 13:31:04"
+      "order": 0
     }
   ],
   "cover": {
index ad7c6f43a5d551eec767dadda32f047d64ded014..8d69a5fddd3a8eee35101cceff94c6e3acd567ec 100644 (file)
@@ -24,6 +24,8 @@
         <server name="APP_LANG" value="en"/>
         <server name="APP_THEME" value="none"/>
         <server name="APP_AUTO_LANG_PUBLIC" value="true"/>
+        <server name="APP_URL" value="http://bookstack.dev"/>
+        <server name="ALLOWED_IFRAME_HOSTS" value=""/>
         <server name="CACHE_DRIVER" value="array"/>
         <server name="SESSION_DRIVER" value="array"/>
         <server name="QUEUE_CONNECTION" value="sync"/>
@@ -35,6 +37,7 @@
         <server name="DISABLE_EXTERNAL_SERVICES" value="true"/>
         <server name="AVATAR_URL" value=""/>
         <server name="LDAP_VERSION" value="3"/>
+        <server name="SESSION_SECURE_COOKIE" value="null"/>
         <server name="STORAGE_TYPE" value="local"/>
         <server name="STORAGE_ATTACHMENT_TYPE" value="local"/>
         <server name="STORAGE_IMAGE_TYPE" value="local"/>
@@ -47,7 +50,6 @@
         <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"/>
         <server name="SAML2_ENABLED" value="false"/>
         <server name="API_REQUESTS_PER_MIN" value="180"/>
index bf6dfac2ddc584439e357be29e6d9dadabcb3da1..fd61a62c7611d72701f84277e02388371d65f3a7 100644 (file)
--- a/readme.md
+++ b/readme.md
@@ -43,13 +43,13 @@ BookStack releases are each assigned a version number, such as "v0.25.2", in the
 
 Each BookStack release will have a [milestone](https://github.com/BookStackApp/BookStack/milestones) created with issues & pull requests assigned to it to define what will be in that release. Milestones are built up then worked through until complete at which point, after some testing and documentation updates, the release will be deployed. 
 
-For feature releases, and some patch releases, the release will be accompanied by a post on the [BookStack blog](https://www.bookstackapp.com/blog/) which will provide additional detail on features, changes & updates otherwise the [GitHub release page](https://github.com/BookStackApp/BookStack/releases) will show a list of changes. You can sign up to be alerted to new BookStack blogs posts (once per week maximum) [at this link](http://eepurl.com/cmmq5j).
+For feature releases, and some patch releases, the release will be accompanied by a post on the [BookStack blog](https://www.bookstackapp.com/blog/) which will provide additional detail on features, changes & updates otherwise the [GitHub release page](https://github.com/BookStackApp/BookStack/releases) will show a list of changes. You can sign up to be alerted to new BookStack blogs posts (once per week maximum) [at this link](https://updates.bookstackapp.com/signup/bookstack-news-and-updates).
 
 ## 🛠️ Development & Testing
 
 All development on BookStack is currently done on the master branch. When it's time for a release the master branch is merged into release with built & minified CSS & JS then tagged at its version. Here are the current development requirements:
 
-* [Node.js](https://nodejs.org/en/) v10.0+
+* [Node.js](https://nodejs.org/en/) v12.0+
 
 This project uses SASS for CSS development and this is built, along with the JavaScript, using a range of npm scripts. The below npm commands can be used to install the dependencies & run the build tasks:
 
@@ -129,7 +129,7 @@ The project's code of conduct [can be found here](https://github.com/BookStackAp
 
 Security information for administering a BookStack instance can be found on the [documentation site here](https://www.bookstackapp.com/docs/admin/security/).
 
-If you'd like to be notified of new potential security concerns you can [sign-up to the BookStack security mailing list](http://eepurl.com/glIh8z).
+If you'd like to be notified of new potential security concerns you can [sign-up to the BookStack security mailing list](https://updates.bookstackapp.com/signup/bookstack-security-updates).
 
 If you would like to report a security concern in a more confidential manner than via a GitHub issue, You can directly email the lead maintainer [ssddanbrown](https://github.com/ssddanbrown). You will need to login to be able to see the email address on the [GitHub profile page](https://github.com/ssddanbrown). Alternatively you can send a DM via twitter to [@ssddanbrown](https://twitter.com/ssddanbrown).
 
@@ -168,5 +168,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)
 * [diagrams.net](https://github.com/jgraph/drawio)
-* [Laravel Stats](https://github.com/stefanzweifel/laravel-stats)
 * [OneLogin's SAML PHP Toolkit](https://github.com/onelogin/php-saml)
diff --git a/resources/js/components/breadcrumb-listing.js b/resources/js/components/breadcrumb-listing.js
deleted file mode 100644 (file)
index 7f4344b..0000000
+++ /dev/null
@@ -1,58 +0,0 @@
-
-
-class BreadcrumbListing {
-
-    constructor(elem) {
-        this.elem = elem;
-        this.searchInput = elem.querySelector('input');
-        this.loadingElem = elem.querySelector('.loading-container');
-        this.entityListElem = elem.querySelector('.breadcrumb-listing-entity-list');
-
-        // this.loadingElem.style.display = 'none';
-        const entityDescriptor = elem.getAttribute('breadcrumb-listing').split(':');
-        this.entityType = entityDescriptor[0];
-        this.entityId = Number(entityDescriptor[1]);
-
-        this.elem.addEventListener('show', this.onShow.bind(this));
-        this.searchInput.addEventListener('input', this.onSearch.bind(this));
-    }
-
-    onShow() {
-        this.loadEntityView();
-    }
-
-    onSearch() {
-        const input = this.searchInput.value.toLowerCase().trim();
-        const listItems = this.entityListElem.querySelectorAll('.entity-list-item');
-        for (let listItem of listItems) {
-            const match = !input || listItem.textContent.toLowerCase().includes(input);
-            listItem.style.display = match ? 'flex' : 'none';
-            listItem.classList.toggle('hidden', !match);
-        }
-    }
-
-    loadEntityView() {
-        this.toggleLoading(true);
-
-        const params = {
-            'entity_id': this.entityId,
-            'entity_type': this.entityType,
-        };
-
-        window.$http.get('/search/entity/siblings', params).then(resp => {
-            this.entityListElem.innerHTML = resp.data;
-        }).catch(err => {
-            console.error(err);
-        }).then(() => {
-            this.toggleLoading(false);
-            this.onSearch();
-        });
-    }
-
-    toggleLoading(show = false) {
-        this.loadingElem.style.display = show ? 'block' : 'none';
-    }
-
-}
-
-export default BreadcrumbListing;
\ No newline at end of file
diff --git a/resources/js/components/dropdown-search.js b/resources/js/components/dropdown-search.js
new file mode 100644 (file)
index 0000000..8c81aae
--- /dev/null
@@ -0,0 +1,79 @@
+import {debounce} from "../services/util";
+
+class DropdownSearch {
+
+    setup() {
+        this.elem = this.$el;
+        this.searchInput = this.$refs.searchInput;
+        this.loadingElem = this.$refs.loading;
+        this.listContainerElem = this.$refs.listContainer;
+
+        this.localSearchSelector = this.$opts.localSearchSelector;
+        this.url = this.$opts.url;
+
+        this.elem.addEventListener('show', this.onShow.bind(this));
+        this.searchInput.addEventListener('input', this.onSearch.bind(this));
+
+        this.runAjaxSearch = debounce(this.runAjaxSearch, 300, false);
+    }
+
+    onShow() {
+        this.loadList();
+    }
+
+    onSearch() {
+        const input = this.searchInput.value.toLowerCase().trim();
+        if (this.localSearchSelector) {
+            this.runLocalSearch(input);
+        } else {
+            this.toggleLoading(true);
+            this.runAjaxSearch(input);
+        }
+    }
+
+    runAjaxSearch(searchTerm) {
+        this.loadList(searchTerm);
+    }
+
+    runLocalSearch(searchTerm) {
+        const listItems = this.listContainerElem.querySelectorAll(this.localSearchSelector);
+        for (let listItem of listItems) {
+            const match = !searchTerm || listItem.textContent.toLowerCase().includes(searchTerm);
+            listItem.style.display = match ? 'flex' : 'none';
+            listItem.classList.toggle('hidden', !match);
+        }
+    }
+
+    async loadList(searchTerm = '') {
+        this.listContainerElem.innerHTML = '';
+        this.toggleLoading(true);
+
+        try {
+            const resp = await window.$http.get(this.getAjaxUrl(searchTerm));
+            this.listContainerElem.innerHTML = resp.data;
+        } catch (err) {
+            console.error(err);
+        }
+
+        this.toggleLoading(false);
+        if (this.localSearchSelector) {
+            this.onSearch();
+        }
+    }
+
+    getAjaxUrl(searchTerm = null) {
+        if (!searchTerm) {
+            return this.url;
+        }
+
+        const joiner = this.url.includes('?') ? '&' : '?';
+        return `${this.url}${joiner}search=${encodeURIComponent(searchTerm)}`;
+    }
+
+    toggleLoading(show = false) {
+        this.loadingElem.style.display = show ? 'block' : 'none';
+    }
+
+}
+
+export default DropdownSearch;
\ No newline at end of file
index 7b1ce30556d41bde39b961065b47f87dcc5783f4..22402d483902b2148918150e1d371611dd1abf9b 100644 (file)
@@ -17,6 +17,7 @@ class DropDown {
         this.body = document.body;
         this.showing = false;
         this.setupListeners();
+        this.hide = this.hide.bind(this);
     }
 
     show(event = null) {
index 87c496c91c591718a269b663d3ac329230a778ab..91ccdaf3aa6f23fca3a923283634c75eb42a0265 100644 (file)
@@ -5,7 +5,6 @@ import attachments from "./attachments.js"
 import autoSuggest from "./auto-suggest.js"
 import backToTop from "./back-to-top.js"
 import bookSort from "./book-sort.js"
-import breadcrumbListing from "./breadcrumb-listing.js"
 import chapterToggle from "./chapter-toggle.js"
 import codeEditor from "./code-editor.js"
 import codeHighlighter from "./code-highlighter.js"
@@ -13,6 +12,7 @@ import collapsible from "./collapsible.js"
 import customCheckbox from "./custom-checkbox.js"
 import detailsHighlighter from "./details-highlighter.js"
 import dropdown from "./dropdown.js"
+import dropdownSearch from "./dropdown-search.js"
 import dropzone from "./dropzone.js"
 import editorToolbox from "./editor-toolbox.js"
 import entityPermissionsEditor from "./entity-permissions-editor.js"
@@ -48,6 +48,7 @@ import tagManager from "./tag-manager.js"
 import templateManager from "./template-manager.js"
 import toggleSwitch from "./toggle-switch.js"
 import triLayout from "./tri-layout.js"
+import userSelect from "./user-select.js"
 import wysiwygEditor from "./wysiwyg-editor.js"
 
 const componentMapping = {
@@ -58,7 +59,6 @@ const componentMapping = {
     "auto-suggest": autoSuggest,
     "back-to-top": backToTop,
     "book-sort": bookSort,
-    "breadcrumb-listing": breadcrumbListing,
     "chapter-toggle": chapterToggle,
     "code-editor": codeEditor,
     "code-highlighter": codeHighlighter,
@@ -66,6 +66,7 @@ const componentMapping = {
     "custom-checkbox": customCheckbox,
     "details-highlighter": detailsHighlighter,
     "dropdown": dropdown,
+    "dropdown-search": dropdownSearch,
     "dropzone": dropzone,
     "editor-toolbox": editorToolbox,
     "entity-permissions-editor": entityPermissionsEditor,
@@ -101,6 +102,7 @@ const componentMapping = {
     "template-manager": templateManager,
     "toggle-switch": toggleSwitch,
     "tri-layout": triLayout,
+    "user-select": userSelect,
     "wysiwyg-editor": wysiwygEditor,
 };
 
index 19d26d4a987f561f2aa0a3d007283f0655463d83..bd107f2bf7a00f53ed3404a27c95bcf3c1c7d1c3 100644 (file)
@@ -13,6 +13,7 @@ class MarkdownEditor {
 
         this.pageId = this.$opts.pageId;
         this.textDirection = this.$opts.textDirection;
+        this.imageUploadErrorText = this.$opts.imageUploadErrorText;
 
         this.markdown = new MarkdownIt({html: true});
         this.markdown.use(mdTasksLists, {label: true});
@@ -373,7 +374,7 @@ class MarkdownEditor {
                 const newContent = `[![${selectedText}](${resp.data.thumbs.display})](${resp.data.url})`;
                 replaceContent(placeHolderText, newContent);
             }).catch(err => {
-                window.$events.emit('error', trans('errors.image_upload_error'));
+                window.$events.emit('error', context.imageUploadErrorText);
                 replaceContent(placeHolderText, selectedText);
                 console.log(err);
             });
@@ -492,7 +493,7 @@ class MarkdownEditor {
                 this.cm.focus();
                 DrawIO.close();
             }).catch(err => {
-                window.$events.emit('error', trans('errors.image_upload_error'));
+                window.$events.emit('error', this.imageUploadErrorText);
                 console.log(err);
             });
         });
diff --git a/resources/js/components/user-select.js b/resources/js/components/user-select.js
new file mode 100644 (file)
index 0000000..477c11d
--- /dev/null
@@ -0,0 +1,24 @@
+import {onChildEvent} from "../services/dom";
+
+class UserSelect {
+
+    setup() {
+
+        this.input = this.$refs.input;
+        this.userInfoContainer = this.$refs.userInfo;
+
+        this.hide = this.$el.components.dropdown.hide;
+
+        onChildEvent(this.$el, 'a.dropdown-search-item', 'click', this.selectUser.bind(this));
+    }
+
+    selectUser(event, userEl) {
+        const id = userEl.getAttribute('data-id');
+        this.input.value = id;
+        this.userInfoContainer.innerHTML = userEl.innerHTML;
+        this.hide();
+    }
+
+}
+
+export default UserSelect;
\ No newline at end of file
index a32e78161649e6337ee45bb6d9ab76f8bbf5fc9c..bae70ad14e97c20beef6bab0990a2ba58ee0aa56 100644 (file)
@@ -38,7 +38,7 @@ function editorPaste(event, editor, wysiwygComponent) {
                 editor.dom.replace(newEl, id);
             }).catch(err => {
                 editor.dom.remove(id);
-                window.$events.emit('error', trans('errors.image_upload_error'));
+                window.$events.emit('error', wysiwygComponent.imageUploadErrorText);
                 console.log(err);
             });
         }, 10);
@@ -236,7 +236,7 @@ function codePlugin() {
     });
 }
 
-function drawIoPlugin(drawioUrl, isDarkMode, pageId) {
+function drawIoPlugin(drawioUrl, isDarkMode, pageId, wysiwygComponent) {
 
     let pageEditor = null;
     let currentNode = null;
@@ -280,7 +280,7 @@ function drawIoPlugin(drawioUrl, isDarkMode, pageId) {
                 pageEditor.dom.setAttrib(imgElem, 'src', img.url);
                 pageEditor.dom.setAttrib(currentNode, 'drawio-diagram', img.id);
             } catch (err) {
-                window.$events.emit('error', trans('errors.image_upload_error'));
+                window.$events.emit('error', wysiwygComponent.imageUploadErrorText);
                 console.log(err);
             }
             return;
@@ -295,7 +295,7 @@ function drawIoPlugin(drawioUrl, isDarkMode, pageId) {
                 pageEditor.dom.get(id).parentNode.setAttribute('drawio-diagram', img.id);
             } catch (err) {
                 pageEditor.dom.remove(id);
-                window.$events.emit('error', trans('errors.image_upload_error'));
+                window.$events.emit('error', wysiwygComponent.imageUploadErrorText);
                 console.log(err);
             }
         }, 5);
@@ -414,12 +414,12 @@ function listenForBookStackEditorEvents(editor) {
 
 class WysiwygEditor {
 
-
     setup() {
         this.elem = this.$el;
 
         this.pageId = this.$opts.pageId;
         this.textDirection = this.$opts.textDirection;
+        this.imageUploadErrorText = this.$opts.imageUploadErrorText;
         this.isDarkMode = document.documentElement.classList.contains('dark-mode');
 
         this.plugins = "image table textcolor paste link autolink fullscreen code customhr autosave lists codeeditor media";
@@ -437,7 +437,7 @@ class WysiwygEditor {
         const drawioUrlElem = document.querySelector('[drawio-url]');
         if (drawioUrlElem) {
             const url = drawioUrlElem.getAttribute('drawio-url');
-            drawIoPlugin(url, this.isDarkMode, this.pageId);
+            drawIoPlugin(url, this.isDarkMode, this.pageId, this);
             this.plugins += ' drawio';
         }
 
index 4cac54b2a706efa35cb8873ebc247c20420e65b5..fe937b061930262a060465699446647adab763d9 100644 (file)
@@ -45,4 +45,5 @@ return [
 
     // Other
     'commented_on'                => 'commented on',
+    'permissions_update'          => 'updated permissions',
 ];
index 6de8328afc864de3ac0fef1a72588c7d5ffb3c96..f668c618162c413b236c56f224bc8a5806d0ec88 100644 (file)
@@ -22,6 +22,7 @@ return [
     'meta_created_name' => 'Created :timeLength by :user',
     'meta_updated' => 'Updated :timeLength',
     'meta_updated_name' => 'Updated :timeLength by :user',
+    'meta_owned_name' => 'Owned by :user',
     'entity_select' => 'Entity Select',
     'images' => 'Images',
     'my_recent_drafts' => 'My Recent Drafts',
@@ -39,6 +40,7 @@ return [
     'permissions_intro' => 'Once enabled, These permissions will take priority over any set role permissions.',
     'permissions_enable' => 'Enable Custom Permissions',
     'permissions_save' => 'Save Permissions',
+    'permissions_owner' => 'Owner',
 
     // Search
     'search_results' => 'Search Results',
@@ -146,7 +148,7 @@ return [
     'chapters_create' => 'Create New Chapter',
     'chapters_delete' => 'Delete Chapter',
     'chapters_delete_named' => 'Delete Chapter :chapterName',
-    'chapters_delete_explain' => 'This will delete the chapter with the name \':chapterName\'. All pages will be removed and added directly to the parent book.',
+    'chapters_delete_explain' => 'This will delete the chapter with the name \':chapterName\'. All pages that exist within this chapter will also be deleted.',
     'chapters_delete_confirm' => 'Are you sure you want to delete this chapter?',
     'chapters_edit' => 'Edit Chapter',
     'chapters_edit_named' => 'Edit Chapter :chapterName',
index e280396a25fb136154aabf2552a364e59fa99476..414650d21bac0c5187afc841de93f0f0ea40aaed 100755 (executable)
@@ -68,7 +68,7 @@ return [
     'maint' => 'Maintenance',
     'maint_image_cleanup' => 'Cleanup Images',
     'maint_image_cleanup_desc' => "Scans page & revision content to check which images and drawings are currently in use and which images are redundant. Ensure you create a full database and image backup before running this.",
-    'maint_image_cleanup_ignore_revisions' => 'Ignore images in revisions',
+    'maint_delete_images_only_in_revisions' => 'Also delete images that only exist in old page revisions',
     'maint_image_cleanup_run' => 'Run Cleanup',
     'maint_image_cleanup_warning' => ':count potentially unused images were found. Are you sure you want to delete these images?',
     'maint_image_cleanup_success' => ':count potentially unused images found and deleted!',
@@ -80,6 +80,27 @@ return [
     'maint_send_test_email_mail_subject' => 'Test Email',
     'maint_send_test_email_mail_greeting' => 'Email delivery seems to work!',
     'maint_send_test_email_mail_text' => 'Congratulations! As you received this email notification, your email settings seem to be configured properly.',
+    'maint_recycle_bin_desc' => 'Deleted shelves, books, chapters & pages are sent to the recycle bin so they can be restored or permanently deleted. Older items in the recycle bin may be automatically removed after a while depending on system configuration.',
+    'maint_recycle_bin_open' => 'Open Recycle Bin',
+
+    // Recycle Bin
+    'recycle_bin' => 'Recycle Bin',
+    'recycle_bin_desc' => 'Here you can restore items that have been deleted or choose to permanently remove them from the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.',
+    'recycle_bin_deleted_item' => 'Deleted Item',
+    'recycle_bin_deleted_by' => 'Deleted By',
+    'recycle_bin_deleted_at' => 'Deletion Time',
+    'recycle_bin_permanently_delete' => 'Permanently Delete',
+    'recycle_bin_restore' => 'Restore',
+    'recycle_bin_contents_empty' => 'The recycle bin is currently empty',
+    'recycle_bin_empty' => 'Empty Recycle Bin',
+    'recycle_bin_empty_confirm' => 'This will permanently destroy all items in the recycle bin including content contained within each item. Are you sure you want to empty the recycle bin?',
+    'recycle_bin_destroy_confirm' => 'This action will permanently delete this item, along with any child elements listed below, from the system and you will not be able to restore this content. Are you sure you want to permanently delete this item?',
+    'recycle_bin_destroy_list' => 'Items to be Destroyed',
+    'recycle_bin_restore_list' => 'Items to be Restored',
+    'recycle_bin_restore_confirm' => 'This action will restore the deleted item, including any child elements, to their original location. If the original location has since been deleted, and is now in the recycle bin, the parent item will also need to be restored.',
+    'recycle_bin_restore_deleted_parent' => 'The parent of this item has also been deleted. These will remain deleted until that parent is also restored.',
+    'recycle_bin_destroy_notification' => 'Deleted :count total items from the recycle bin.',
+    'recycle_bin_restore_notification' => 'Restored :count total items from the recycle bin.',
 
     // Audit Log
     'audit' => 'Audit Log',
@@ -90,7 +111,7 @@ return [
     'audit_deleted_item_name' => 'Name: :name',
     'audit_table_user' => 'User',
     'audit_table_event' => 'Event',
-    'audit_table_item' => 'Related Item',
+    'audit_table_related' => 'Related Item or Detail',
     'audit_table_date' => 'Activity Date',
     'audit_date_from' => 'Date Range From',
     'audit_date_to' => 'Date Range To',
@@ -136,6 +157,7 @@ return [
     'user_profile' => 'User Profile',
     'users_add_new' => 'Add New User',
     'users_search' => 'Search Users',
+    'users_latest_activity' => 'Latest Activity',
     'users_details' => 'User Details',
     'users_details_desc' => 'Set a display name and an email address for this user. The email address will be used for logging into the application.',
     'users_details_desc_no_email' => 'Set a display name for this user so others can recognise them.',
@@ -153,7 +175,10 @@ return [
     'users_delete_named' => 'Delete user :userName',
     'users_delete_warning' => 'This will fully delete this user with the name \':userName\' from the system.',
     'users_delete_confirm' => 'Are you sure you want to delete this user?',
-    'users_delete_success' => 'Users successfully removed',
+    'users_migrate_ownership' => 'Migrate Ownership',
+    'users_migrate_ownership_desc' => 'Select a user here if you want another user to become the owner of all items currently owned by this user.',
+    'users_none_selected' => 'No user selected',
+    'users_delete_success' => 'User successfully removed',
     'users_edit' => 'Edit User',
     'users_edit_profile' => 'Edit Profile',
     'users_edit_success' => 'User successfully updated',
@@ -214,6 +239,7 @@ return [
         'ja' => '日本語',
         'ko' => '한국어',
         'nl' => 'Nederlands',
+        'nb' => 'Norsk (Bokmål)',
         'pl' => 'Polski',
         'pt_BR' => 'Português do Brasil',
         'ru' => 'Русский',
diff --git a/resources/lang/nb/activities.php b/resources/lang/nb/activities.php
new file mode 100644 (file)
index 0000000..3c254a6
--- /dev/null
@@ -0,0 +1,49 @@
+<?php
+/**
+ * Activity text strings.
+ * Is used for all the text within activity logs & notifications.
+ */
+return [
+
+    // Pages
+    'page_create'                 => 'opprettet side',
+    'page_create_notification'    => 'Siden ble opprettet',
+    'page_update'                 => 'oppdaterte side',
+    'page_update_notification'    => 'Siden ble oppdatert',
+    'page_delete'                 => 'slettet side',
+    'page_delete_notification'    => 'Siden ble slettet',
+    'page_restore'                => 'gjenopprettet side',
+    'page_restore_notification'   => 'Siden ble gjenopprettet',
+    'page_move'                   => 'flyttet side',
+
+    // Chapters
+    'chapter_create'              => 'opprettet kapittel',
+    'chapter_create_notification' => 'Kapittelet ble opprettet',
+    'chapter_update'              => 'oppdaterte kapittel',
+    'chapter_update_notification' => 'Kapittelet ble oppdatert',
+    'chapter_delete'              => 'slettet kapittel',
+    'chapter_delete_notification' => 'Kapittelet ble slettet',
+    'chapter_move'                => 'flyttet kapittel
+    ',
+
+    // Books
+    'book_create'                 => 'opprettet bok',
+    'book_create_notification'    => 'Boken ble opprettet',
+    'book_update'                 => 'oppdaterte bok',
+    'book_update_notification'    => 'Boken ble oppdatert',
+    'book_delete'                 => 'slettet bok',
+    'book_delete_notification'    => 'Boken ble slettet',
+    'book_sort'                   => 'sorterte bok',
+    'book_sort_notification'      => 'Boken ble omsortert',
+
+    // Bookshelves
+    'bookshelf_create'            => 'opprettet bokhylle',
+    'bookshelf_create_notification'    => 'Bokhyllen ble opprettet',
+    'bookshelf_update'                 => 'oppdaterte bokhylle',
+    'bookshelf_update_notification'    => 'Bokhyllen ble oppdatert',
+    'bookshelf_delete'                 => 'slettet bokhylle',
+    'bookshelf_delete_notification'    => 'Bokhyllen ble slettet',
+
+    // Other
+    'commented_on'                => 'kommenterte på',
+];
diff --git a/resources/lang/nb/auth.php b/resources/lang/nb/auth.php
new file mode 100644 (file)
index 0000000..ae145d2
--- /dev/null
@@ -0,0 +1,77 @@
+<?php
+/**
+ * Authentication Language Lines
+ * The following language lines are used during authentication for various
+ * messages that we need to display to the user.
+ */
+return [
+
+    'failed' => 'Disse detaljene samsvarer ikke med det vi har på bok.',
+    'throttle' => 'For mange forsøk, prøv igjen om :seconds sekunder.',
+
+    // Login & Register
+    'sign_up' => 'Registrer deg',
+    'log_in' => 'Logg inn',
+    'log_in_with' => 'Logg inn med :socialDriver',
+    'sign_up_with' => 'Registrer med :socialDriver',
+    'logout' => 'Logg ut',
+
+    'name' => 'Navn',
+    'username' => 'Brukernavn',
+    'email' => 'E-post',
+    'password' => 'Passord',
+    'password_confirm' => 'Bekreft passord',
+    'password_hint' => 'Må inneholde 7 tegn',
+    'forgot_password' => 'Glemt passord?',
+    'remember_me' => 'Husk meg',
+    'ldap_email_hint' => 'Oppgi en e-post for denne kontoen.',
+    'create_account' => 'Opprett konto',
+    'already_have_account' => 'Har du allerede en konto?',
+    'dont_have_account' => 'Mangler du en konto?',
+    'social_login' => 'Sosiale kontoer',
+    'social_registration' => 'Registrer via sosiale kontoer',
+    'social_registration_text' => 'Bruk en annen tjeneste for å registrere deg.',
+
+    'register_thanks' => 'Takk for at du registrerte deg!',
+    'register_confirm' => 'Sjekk e-posten din for informasjon som gir deg tilgang til :appName.',
+    'registrations_disabled' => 'Registrering er deaktivert.',
+    'registration_email_domain_invalid' => 'Du kan ikke bruke det domenet for å registrere en konto.',
+    'register_success' => 'Takk for registreringen! Du kan nå logge inn på tjenesten.',
+
+
+    // Password Reset
+    'reset_password' => 'Nullstille passord',
+    'reset_password_send_instructions' => 'Oppgi e-posten som er koblet til kontoen din, så sender vi en epost hvor du kan nullstille passordet.',
+    'reset_password_send_button' => 'Send nullstillingslenke',
+    'reset_password_sent' => 'En nullstillingslenke ble sendt til :email om den eksisterer i systemet.',
+    'reset_password_success' => 'Passordet ble nullstilt.',
+    'email_reset_subject' => 'Nullstill ditt :appName passord',
+    'email_reset_text' => 'Du mottar denne eposten fordi det er blitt bedt om en nullstilling av passord på denne kontoen.',
+    'email_reset_not_requested' => 'Om det ikke var deg, så trenger du ikke foreta deg noe.',
+
+
+    // Email Confirmation
+    'email_confirm_subject' => 'Bekreft epost-adressen for :appName',
+    'email_confirm_greeting' => 'Takk for at du registrerte deg for :appName!',
+    'email_confirm_text' => 'Bekreft e-posten din ved å trykke på knappen nedenfor:',
+    'email_confirm_action' => 'Bekreft e-post',
+    'email_confirm_send_error' => 'Bekreftelse er krevd av systemet, men systemet kan ikke sende disse. Kontakt admin for å løse problemet.',
+    'email_confirm_success' => 'E-posten din er bekreftet!',
+    'email_confirm_resent' => 'Bekreftelsespost ble sendt, sjekk innboksen din.',
+
+    'email_not_confirmed' => 'E-posten er ikke bekreftet.',
+    'email_not_confirmed_text' => 'Epost-adressen er ennå ikke bekreftet.',
+    'email_not_confirmed_click_link' => 'Trykk på lenken i e-posten du fikk vedrørende din registrering.',
+    'email_not_confirmed_resend' => 'Om du ikke finner den i innboksen eller søppelboksen, kan du få tilsendt ny ved å trykke på knappen under.',
+    'email_not_confirmed_resend_button' => 'Send bekreftelsespost på nytt',
+
+    // User Invite
+    'user_invite_email_subject' => 'Du har blitt invitert til :appName!',
+    'user_invite_email_greeting' => 'En konto har blitt opprettet for deg på :appName.',
+    'user_invite_email_text' => 'Trykk på knappen under for å opprette et sikkert passord:',
+    'user_invite_email_action' => 'Angi passord',
+    'user_invite_page_welcome' => 'Velkommen til :appName!',
+    'user_invite_page_text' => 'For å fullføre prosessen må du oppgi et passord som sikrer din konto på :appName for fremtidige besøk.',
+    'user_invite_page_confirm_button' => 'Bekreft passord',
+    'user_invite_success' => 'Passordet er angitt, du kan nå bruke :appName!'
+];
\ No newline at end of file
diff --git a/resources/lang/nb/common.php b/resources/lang/nb/common.php
new file mode 100644 (file)
index 0000000..8bda991
--- /dev/null
@@ -0,0 +1,80 @@
+<?php
+/**
+ * Common elements found throughout many areas of BookStack.
+ */
+return [
+
+    // Buttons
+    'cancel' => 'Avbryt',
+    'confirm' => 'Bekreft',
+    'back' => 'Tilbake',
+    'save' => 'Lagre',
+    'continue' => 'Fortsett',
+    'select' => 'Velg',
+    'toggle_all' => 'Bytt alle',
+    'more' => 'Mer',
+
+    // Form Labels
+    'name' => 'Navn',
+    'description' => 'Beskrivelse',
+    'role' => 'Rolle',
+  'cover_image' => 'Bokomslag',
+    'cover_image_description' => 'Bildet bør være ca. 440x250px.',
+    
+    // Actions
+    'actions' => 'Handlinger',
+    'view' => 'Vis',
+    'view_all' => 'Vis alle',
+    'create' => 'Opprett',
+    'update' => 'Oppdater',
+    'edit' => 'Rediger',
+    'sort' => 'Sorter',
+    'move' => 'Flytt',
+    'copy' => 'Kopier',
+    'reply' => 'Svar',
+    'delete' => 'Slett',
+    'delete_confirm' => 'Bekreft sletting',
+    'search' => 'Søk',
+    'search_clear' => 'Nullstill søk',
+    'reset' => 'Nullstill',
+    'remove' => 'Fjern',
+    'add' => 'Legg til',
+    'fullscreen' => 'Fullskjerm',
+
+    // Sort Options
+    'sort_options' => 'Sorteringsalternativer',
+    'sort_direction_toggle' => 'Sorteringsretning',
+    'sort_ascending' => 'Stigende sortering',
+    'sort_descending' => 'Synkende sortering',
+    'sort_name' => 'Navn',
+    'sort_created_at' => 'Dato opprettet',
+    'sort_updated_at' => 'Dato oppdatert',
+
+    // Misc
+    'deleted_user' => 'Slett bruker',
+    'no_activity' => 'Ingen aktivitet å vise',
+    'no_items' => 'Ingen ting å vise',
+    'back_to_top' => 'Hopp til toppen',
+    'toggle_details' => 'Vis/skjul detaljer',
+    'toggle_thumbnails' => 'Vis/skjul miniatyrbilder',
+    'details' => 'Detaljer',
+    'grid_view' => 'Rutenettvisning',
+    'list_view' => 'Listevisning',
+    'default' => 'Standard',
+    'breadcrumb' => 'Brødsmuler',
+
+    // Header
+    'profile_menu' => 'Profilmeny',
+    'view_profile' => 'Vis profil',
+    'edit_profile' => 'Endre Profile',
+    'dark_mode' => 'Kveldsmodus',
+    'light_mode' => 'Dagmodus',
+
+    // Layout tabs
+    'tab_info' => 'Informasjon',
+    'tab_content' => 'Innhold',
+
+    // Email Content
+    'email_action_help' => 'Om du har problemer med å trykke på «:actionText»-knappen, bruk nettadressen under for å gå direkte dit:',
+    'email_rights' => 'Kopibeskyttet',
+];
diff --git a/resources/lang/nb/components.php b/resources/lang/nb/components.php
new file mode 100644 (file)
index 0000000..cfc28c4
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+/**
+ * Text used in custom JavaScript driven components.
+ */
+return [
+
+    // Image Manager
+    'image_select' => 'Velg bilde',
+    'image_all' => 'Alle',
+    'image_all_title' => 'Vis alle bilder',
+    'image_book_title' => 'Vis bilder som er lastet opp i denne boken',
+    'image_page_title' => 'Vis bilder lastet opp til denne siden',
+    'image_search_hint' => 'Søk på bilder etter navn',
+    'image_uploaded' => 'Opplastet :uploadedDate',
+    'image_load_more' => 'Last in flere',
+    'image_image_name' => 'Bildenavn',
+    'image_delete_used' => 'Dette bildet er brukt på sidene nedenfor.',
+    'image_delete_confirm_text' => 'Vil du slette dette bildet?',
+    'image_select_image' => 'Velg bilde',
+    'image_dropzone' => 'Dra og slipp eller trykk her for å laste opp bilder',
+    'images_deleted' => 'Bilder slettet',
+    'image_preview' => 'Hurtigvisning av bilder',
+    'image_upload_success' => 'Bilde ble lastet opp',
+    'image_update_success' => 'Bildedetaljer ble oppdatert',
+    'image_delete_success' => 'Bilde ble slettet',
+    'image_upload_remove' => 'Fjern',
+
+    // Code Editor
+    'code_editor' => 'Endre kode',
+    'code_language' => 'Kodespråk',
+    'code_content' => 'Kodeinnhold',
+    'code_session_history' => 'Sesjonshistorikk',
+    'code_save' => 'Lagre kode',
+];
diff --git a/resources/lang/nb/entities.php b/resources/lang/nb/entities.php
new file mode 100644 (file)
index 0000000..56ded16
--- /dev/null
@@ -0,0 +1,316 @@
+<?php
+/**
+ * Text used for 'Entities' (Document Structure Elements) such as
+ * Books, Shelves, Chapters & Pages
+ */
+return [
+
+    // Shared
+    'recently_created' => 'Nylig opprettet',
+    'recently_created_pages' => 'Nylig opprettede sider',
+    'recently_updated_pages' => 'Nylig oppdaterte sider',
+    'recently_created_chapters' => 'Nylig opprettede kapitler',
+    'recently_created_books' => 'Nylig opprettede bøker',
+    'recently_created_shelves' => 'Nylig opprettede bokhyller',
+    'recently_update' => 'Nylig oppdatert',
+    'recently_viewed' => 'Nylig vist',
+    'recent_activity' => 'Nylig aktivitet',
+    'create_now' => 'Opprett en nå',
+    'revisions' => 'Revisjoner',
+    'meta_revision' => 'Revisjon #:revisionCount',
+    'meta_created' => 'Opprettet :timeLength',
+    'meta_created_name' => 'Opprettet :timeLength av :user',
+    'meta_updated' => 'Oppdatert :timeLength',
+    'meta_updated_name' => 'Oppdatert :timeLength av :user',
+    'entity_select' => 'Velg entitet',
+    'images' => 'Bilder',
+    'my_recent_drafts' => 'Mine nylige utkast',
+    'my_recently_viewed' => 'Mine nylige visninger',
+    'no_pages_viewed' => 'Du har ikke sett på noen sider',
+    'no_pages_recently_created' => 'Ingen sider har nylig blitt opprettet',
+    'no_pages_recently_updated' => 'Ingen sider har nylig blitt oppdatert',
+    'export' => 'Eksporter',
+    'export_html' => 'Nettside med alt',
+    'export_pdf' => 'PDF Fil',
+    'export_text' => 'Tekstfil',
+
+    // Permissions and restrictions
+    'permissions' => 'Tilganger',
+    'permissions_intro' => 'Når disse er tillatt, vil disse tillatelsene ha prioritet over alle angitte rolletillatelser.',
+    'permissions_enable' => 'Aktiver egendefinerte tillatelser',
+    'permissions_save' => 'Lagre tillatelser',
+
+    // Search
+    'search_results' => 'Søkeresultater',
+    'search_total_results_found' => ':count resultater funnet|:count totalt',
+    'search_clear' => 'Nullstill søk',
+    'search_no_pages' => 'Ingen sider passer med søket',
+    'search_for_term' => 'Søk etter :term',
+    'search_more' => 'Flere resultater',
+    'search_advanced' => 'Avansert søk',
+    'search_terms' => 'Søkeord',
+    'search_content_type' => 'Innholdstype',
+    'search_exact_matches' => 'Eksakte ord',
+    'search_tags' => 'Søk på merker',
+    'search_options' => 'ALternativer',
+    'search_viewed_by_me' => 'Sett av meg',
+    'search_not_viewed_by_me' => 'Ikke sett av meg',
+    'search_permissions_set' => 'Tilganger er angitt',
+    'search_created_by_me' => 'Opprettet av meg',
+    'search_updated_by_me' => 'Oppdatert av meg',
+    'search_date_options' => 'Datoalternativer',
+    'search_updated_before' => 'Oppdatert før',
+    'search_updated_after' => 'Oppdatert etter',
+    'search_created_before' => 'Opprettet før',
+    'search_created_after' => 'Opprettet etter',
+    'search_set_date' => 'Angi dato',
+    'search_update' => 'Oppdater søk',
+
+    // Shelves
+    'shelf' => 'Hylle',
+    'shelves' => 'Hyller',
+    'x_shelves' => ':count hylle|:count hyller',
+    'shelves_long' => 'Bokhyller',
+    'shelves_empty' => 'Ingen bokhyller er opprettet',
+    'shelves_create' => 'Opprett ny bokhylle',
+    'shelves_popular' => 'Populære bokhyller',
+    'shelves_new' => 'Nye bokhyller',
+    'shelves_new_action' => 'Ny bokhylle',
+    'shelves_popular_empty' => 'De mest populære bokhyllene blir vist her.',
+    'shelves_new_empty' => 'Nylig opprettede bokhyller vises her.',
+    'shelves_save' => 'Lagre hylle',
+    'shelves_books' => 'Bøker på denne hyllen',
+    'shelves_add_books' => 'Legg til bøker på denne hyllen',
+    'shelves_drag_books' => 'Dra bøker hit for å stable dem i denne hylla',
+    'shelves_empty_contents' => 'INgen bøker er stablet i denne hylla',
+    'shelves_edit_and_assign' => 'Endre hylla for å legge til bøker',
+    'shelves_edit_named' => 'Endre hyllen :name',
+    'shelves_edit' => 'Endre bokhylle',
+    'shelves_delete' => 'Fjern bokhylle',
+    'shelves_delete_named' => 'Fjern bokhyllen :name',
+    'shelves_delete_explain' => "Dette vil fjerne bokhyllen ':name'. Bøkene vil ikke fjernes fra systemet.",
+    'shelves_delete_confirmation' => 'Er du helt sikker på at du vil skru ned hylla?',
+    'shelves_permissions' => 'Tilganger til hylla',
+    'shelves_permissions_updated' => 'Hyllas tilganger er oppdatert',
+    'shelves_permissions_active' => 'Hyllas tilganger er aktive',
+    'shelves_copy_permissions_to_books' => 'Kopier tilganger til bøkene på hylla',
+    'shelves_copy_permissions' => 'Kopier tilganger',
+    'shelves_copy_permissions_explain' => 'Dette vil angi gjeldende tillatelsesinnstillinger for denne bokhyllen på alle bøkene som finnes på den. Før du aktiverer, må du forsikre deg om at endringer i tillatelsene til denne bokhyllen er lagret.',
+    'shelves_copy_permission_success' => 'Tilgangene ble overført til :count bøker',
+
+    // Books
+    'book' => 'Bok',
+    'books' => 'Bøker',
+    'x_books' => ':count bok|:count bøker',
+    'books_empty' => 'Ingen bøker er skrevet',
+    'books_popular' => 'Populære bøker',
+    'books_recent' => 'Nylige bøker',
+    'books_new' => 'Nye bøker',
+    'books_new_action' => 'Ny bok',
+    'books_popular_empty' => 'De mest populære bøkene',
+    'books_new_empty' => 'Siste utgivelser vises her.',
+    'books_create' => 'Skriv ny bok',
+    'books_delete' => 'Brenn bok',
+    'books_delete_named' => 'Brenn boken :bookName',
+    'books_delete_explain' => 'Dette vil brenne boken «:bookName». Alle sider i boken vil fordufte for godt.',
+    'books_delete_confirmation' => 'Er du sikker på at du vil brenne boken?',
+    'books_edit' => 'Endre bok',
+    'books_edit_named' => 'Endre boken :bookName',
+    'books_form_book_name' => 'Boktittel',
+    'books_save' => 'Lagre bok',
+    'books_permissions' => 'Boktilganger',
+    'books_permissions_updated' => 'Boktilganger oppdatert',
+    'books_empty_contents' => 'Ingen sider eller kapitler finnes i denne boken.',
+    'books_empty_create_page' => 'Skriv en ny side',
+    'books_empty_sort_current_book' => 'Sorter innholdet i boken',
+    'books_empty_add_chapter' => 'Start på nytt kapittel',
+    'books_permissions_active' => 'Boktilganger er aktive',
+    'books_search_this' => 'Søk i boken',
+    'books_navigation' => 'Boknavigasjon',
+    'books_sort' => 'Sorter bokinnhold',
+    'books_sort_named' => 'Sorter boken :bookName',
+    'books_sort_name' => 'Sorter på navn',
+    'books_sort_created' => 'Sorter på opprettet dato',
+    'books_sort_updated' => 'Sorter på oppdatert dato',
+    'books_sort_chapters_first' => 'Kapitler først',
+    'books_sort_chapters_last' => 'Kapitler sist',
+    'books_sort_show_other' => 'Vis andre bøker',
+    'books_sort_save' => 'Lagre sortering',
+
+    // Chapters
+    'chapter' => 'Kapittel',
+    'chapters' => 'Kapitler',
+    'x_chapters' => ':count Kapittel|:count Kapitler',
+    'chapters_popular' => 'Populære kapittler',
+    'chapters_new' => 'Nytt kapittel',
+    'chapters_create' => 'Skriv nytt kapittel',
+    'chapters_delete' => 'Riv ut kapittel',
+    'chapters_delete_named' => 'Riv ut kapittelet :chapterName',
+    'chapters_delete_explain' => 'Du ønsker å rive ut kapittelet «:chapterName». Alle sidene vil bli flyttet ut av kapittelet og vil ligge direkte i boka.',
+    'chapters_delete_confirm' => 'Er du sikker på at du vil rive ut dette kapittelet?',
+    'chapters_edit' => 'Endre kapittel',
+    'chapters_edit_named' => 'Endre kapittelet :chapterName',
+    'chapters_save' => 'Lagre kapittel',
+    'chapters_move' => 'Flytt kapittel',
+    'chapters_move_named' => 'Flytt kapittelet :chapterName',
+    'chapter_move_success' => 'Kapittelet ble flyttet til :bookName',
+    'chapters_permissions' => 'Kapitteltilganger',
+    'chapters_empty' => 'Det finnes ingen sider i dette kapittelet.',
+    'chapters_permissions_active' => 'Kapitteltilganger er aktivert',
+    'chapters_permissions_success' => 'Kapitteltilgager er oppdatert',
+    'chapters_search_this' => 'Søk i dette kapittelet',
+
+    // Pages
+    'page' => 'Side',
+    'pages' => 'Sider',
+    'x_pages' => ':count side|:count sider',
+    'pages_popular' => 'Populære sider',
+    'pages_new' => 'Ny side',
+    'pages_attachments' => 'Vedlegg',
+    'pages_navigation' => 'Sidenavigasjon',
+    'pages_delete' => 'Riv ut side',
+    'pages_delete_named' => 'Riv ut siden :pageName',
+    'pages_delete_draft_named' => 'Kast sideutkast :pageName',
+    'pages_delete_draft' => 'Kast sideutkast',
+    'pages_delete_success' => 'Siden er revet ut',
+    'pages_delete_draft_success' => 'Sideutkast er kastet',
+    'pages_delete_confirm' => 'Er du sikker på at du vil rive ut siden?',
+    'pages_delete_draft_confirm' => 'Er du sikker på at du vil forkaste utkastet?',
+    'pages_editing_named' => 'Endrer :pageName',
+    'pages_edit_draft_options' => 'Utkastsalternativer',
+    'pages_edit_save_draft' => 'Lagre utkast',
+    'pages_edit_draft' => 'Endre utkast',
+    'pages_editing_draft' => 'Redigerer utkast',
+    'pages_editing_page' => 'Redigerer side',
+    'pages_edit_draft_save_at' => 'Ukast lagret under ',
+    'pages_edit_delete_draft' => 'Forkast utkast',
+    'pages_edit_discard_draft' => 'Gi opp utkast',
+    'pages_edit_set_changelog' => 'Angi endringslogg',
+    'pages_edit_enter_changelog_desc' => 'Gi en kort beskrivelse av endringene dine',
+    'pages_edit_enter_changelog' => 'Se endringslogg',
+    'pages_save' => 'Lagre side',
+    'pages_title' => 'Sidetittel',
+    'pages_name' => 'Sidenavn',
+    'pages_md_editor' => 'Tekstbehandler',
+    'pages_md_preview' => 'Forhåndsvisning',
+    'pages_md_insert_image' => 'Lim inn bilde',
+    'pages_md_insert_link' => 'Lim in lenke',
+    'pages_md_insert_drawing' => 'Lim inn tegning',
+    'pages_not_in_chapter' => 'Siden tilhører ingen kapittel',
+    'pages_move' => 'Flytt side',
+    'pages_move_success' => 'Siden ble flyttet til ":parentName"',
+    'pages_copy' => 'Kopier side',
+    'pages_copy_desination' => 'Destinasjon',
+    'pages_copy_success' => 'Siden ble flyttet',
+    'pages_permissions' => 'Sidetilganger',
+    'pages_permissions_success' => 'Sidens tilganger ble endret',
+    'pages_revision' => 'Revisjon',
+    'pages_revisions' => 'Sidens revisjoner',
+    'pages_revisions_named' => 'Revisjoner for :pageName',
+    'pages_revision_named' => 'Revisjoner for :pageName',
+    'pages_revisions_created_by' => 'Skrevet av',
+    'pages_revisions_date' => 'Revideringsdato',
+    'pages_revisions_number' => '#',
+    'pages_revisions_numbered' => 'Revisjon #:id',
+    'pages_revisions_numbered_changes' => 'Endringer på revisjon #:id',
+    'pages_revisions_changelog' => 'Endringslogg',
+    'pages_revisions_changes' => 'Endringer',
+    'pages_revisions_current' => 'Siste versjon',
+    'pages_revisions_preview' => 'Forhåndsvisning',
+    'pages_revisions_restore' => 'Gjenopprett',
+    'pages_revisions_none' => 'Denne siden har ingen revisjoner',
+    'pages_copy_link' => 'Kopier lenke',
+    'pages_edit_content_link' => 'Endre innhold',
+    'pages_permissions_active' => 'Sidetilganger er aktive',
+    'pages_initial_revision' => 'Første publisering',
+    'pages_initial_name' => 'Ny side',
+    'pages_editing_draft_notification' => 'Du skriver på et utkast som sist ble lagret :timeDiff.',
+    'pages_draft_edited_notification' => 'Siden har blitt endret siden du startet. Det anbefales at du forkaster dine endringer.',
+    'pages_draft_edit_active' => [
+        'start_a' => ':count forfattere har begynt å endre denne siden.',
+        'start_b' => ':userName skriver på siden for øyeblikket',
+        'time_a' => 'siden sist siden ble oppdatert',
+        'time_b' => 'i løpet av de siste :minCount minuttene',
+        'message' => ':start :time. Prøv å ikke overskriv hverandres endringer!',
+    ],
+    'pages_draft_discarded' => 'Forkastet, viser nå siste endringer fra siden slik den er lagret.',
+    'pages_specific' => 'Bestemt side',
+    'pages_is_template' => 'Sidemal',
+
+    // Editor Sidebar
+    'page_tags' => 'Sidemerker',
+    'chapter_tags' => 'Kapittelmerker',
+    'book_tags' => 'Bokmerker',
+    'shelf_tags' => 'Hyllemerker',
+    'tag' => 'Merke',
+    'tags' =>  'Merker',
+    'tag_name' =>  'Merketittel',
+    'tag_value' => 'Merkeverdi (Valgfritt)',
+    'tags_explain' => "Legg til merker for å kategorisere innholdet ditt. \n Du kan legge til merkeverdier for å beskrive dem ytterligere.",
+    'tags_add' => 'Legg til flere merker',
+    'tags_remove' => 'Fjern merke',
+    'attachments' => 'Vedlegg',
+    'attachments_explain' => 'Last opp vedlegg eller legg til lenker for å berike innholdet. Disse vil vises i sidestolpen på siden.',
+    'attachments_explain_instant_save' => 'Endringer her blir lagret med en gang.',
+    'attachments_items' => 'Vedlegg',
+    'attachments_upload' => 'Last opp vedlegg',
+    'attachments_link' => 'Fest lenke',
+    'attachments_set_link' => 'Angi lenke',
+    'attachments_delete' => 'Er du sikker på at du vil fjerne vedlegget?',
+    'attachments_dropzone' => 'Dra og slipp eller trykk her for å feste vedlegg',
+    'attachments_no_files' => 'Ingen vedlegg er lastet opp',
+    'attachments_explain_link' => 'Du kan feste lenker til denne. Det kan være henvisning til andre sider, bøker etc. eller lenker fra nettet.',
+    'attachments_link_name' => 'Lenkenavn',
+    'attachment_link' => 'Vedleggslenke',
+    'attachments_link_url' => 'Lenke til vedlegg',
+    'attachments_link_url_hint' => 'Adresse til lenke eller vedlegg',
+    'attach' => 'Fest',
+    'attachments_insert_link' => 'Fest vedleggslenke',
+    'attachments_edit_file' => 'Endre vedlegg',
+    'attachments_edit_file_name' => 'Vedleggsnavn',
+    'attachments_edit_drop_upload' => 'Dra og slipp eller trykk her for å oppdatere eller overskrive',
+    'attachments_order_updated' => 'Vedleggssortering endret',
+    'attachments_updated_success' => 'Vedleggsdetaljer endret',
+    'attachments_deleted' => 'Vedlegg fjernet',
+    'attachments_file_uploaded' => 'Vedlegg ble lastet opp',
+    'attachments_file_updated' => 'Vedlegget ble oppdatert',
+    'attachments_link_attached' => 'Lenken ble festet til siden',
+    'templates' => 'Maler',
+    'templates_set_as_template' => 'Siden er en mal',
+    'templates_explain_set_as_template' => 'Du kan angi denne siden som en mal slik at innholdet kan brukes når du oppretter andre sider. Andre brukere vil kunne bruke denne malen hvis de har visningstillatelser for denne siden.',
+    'templates_replace_content' => 'Bytt sideinnhold',
+    'templates_append_content' => 'Legg til neders på siden',
+    'templates_prepend_content' => 'Legg til øverst på siden',
+
+    // Profile View
+    'profile_user_for_x' => 'Medlem i :time',
+    'profile_created_content' => 'Har skrevet',
+    'profile_not_created_pages' => ':userName har ikke forfattet noen sider',
+    'profile_not_created_chapters' => ':userName har ikke opprettet noen kapitler',
+    'profile_not_created_books' => ':userName har ikke laget noen bøker',
+    'profile_not_created_shelves' => ':userName har ikke hengt opp noen hyller',
+
+    // Comments
+    'comment' => 'Kommentar',
+    'comments' => 'Kommentarer',
+    'comment_add' => 'Skriv kommentar',
+    'comment_placeholder' => 'Skriv en kommentar her',
+    'comment_count' => '{0} Ingen kommentarer|{1} 1 kommentar|[2,*] :count kommentarer',
+    'comment_save' => 'Publiser kommentar',
+    'comment_saving' => 'Publiserer ...',
+    'comment_deleting' => 'Fjerner...',
+    'comment_new' => 'Ny kommentar',
+    'comment_created' => 'kommenterte :createDiff',
+    'comment_updated' => 'Oppdatert :updateDiff av :username',
+    'comment_deleted_success' => 'Kommentar fjernet',
+    'comment_created_success' => 'Kommentar skrevet',
+    'comment_updated_success' => 'Kommentar endret',
+    'comment_delete_confirm' => 'Er du sikker på at du vil fjerne kommentaren?',
+    'comment_in_reply_to' => 'Som svar til :commentId',
+
+    // Revision
+    'revision_delete_confirm' => 'Vil du slette revisjonen?',
+    'revision_restore_confirm' => 'Vil du gjenopprette revisjonen? Innholdet på siden vil bli overskrevet med denne revisjonen.',
+    'revision_delete_success' => 'Revisjonen ble slettet',
+    'revision_cannot_delete_latest' => 'CKan ikke slette siste revisjon.'
+];
\ No newline at end of file
diff --git a/resources/lang/nb/errors.php b/resources/lang/nb/errors.php
new file mode 100644 (file)
index 0000000..4e5c07f
--- /dev/null
@@ -0,0 +1,102 @@
+<?php
+/**
+ * Text shown in error messaging.
+ */
+return [
+
+    // Permissions
+    'permission' => 'Du har ikke tilgang til å se denne siden.',
+    'permissionJson' => 'Du har ikke tilgang til å utføre denne handlingen.',
+
+    // Auth
+    'error_user_exists_different_creds' => 'En konto med :email finnes allerede, men har andre detaljer.',
+    'email_already_confirmed' => 'E-posten er allerede bekreftet, du kan forsøke å logge inn.',
+    'email_confirmation_invalid' => 'Denne bekreftelseskoden er allerede benyttet eller utgått. Prøv å registrere på nytt.',
+    'email_confirmation_expired' => 'Bekreftelseskoden er allerede utgått, en ny e-post er sendt.',
+    'email_confirmation_awaiting' => 'Du må bekrefte e-posten for denne kontoen.',
+    'ldap_fail_anonymous' => 'LDAP kan ikke benyttes med anonym tilgang for denne tjeneren.',
+    'ldap_fail_authed' => 'LDAP tilgang feilet med angitt DN',
+    'ldap_extension_not_installed' => 'LDAP PHP modulen er ikke installert.',
+    'ldap_cannot_connect' => 'Klarer ikke koble til LDAP på denne adressen',
+    'saml_already_logged_in' => 'Allerede logget inn',
+    'saml_user_not_registered' => 'Kontoen med navn :name er ikke registert, registrering er også deaktivert.',
+    'saml_no_email_address' => 'Denne kontoinformasjonen finnes ikke i det eksterne autentiseringssystemet.',
+    'saml_invalid_response_id' => 'Forespørselen fra det eksterne autentiseringssystemet gjenkjennes ikke av en prosess som startes av dette programmet. Å navigere tilbake etter pålogging kan forårsake dette problemet.',
+    'saml_fail_authed' => 'Innlogging gjennom :system feilet. Fikk ikke kontakt med autentiseringstjeneren.',
+    'social_no_action_defined' => 'Ingen handlinger er definert',
+    'social_login_bad_response' => "Feilmelding mottat fra :socialAccount innloggingstjeneste: \n:error",
+    'social_account_in_use' => 'Denne :socialAccount kontoen er allerede registrert, Prøv å logge inn med :socialAccount alternativet.',
+    'social_account_email_in_use' => 'E-posten :email er allerede i bruk. Har du allerede en konto hos :socialAccount kan dette angis fra profilsiden din.',
+    'social_account_existing' => 'Denne :socialAccount er allerede koblet til din konto.',
+    'social_account_already_used_existing' => 'This :socialAccount account is already used by another user.',
+    'social_account_not_used' => 'Denne :socialAccount konten er ikke koblet til noen konto, angi denne i profilinnstillingene dine. ',
+    'social_account_register_instructions' => 'Har du ikke en konto her ennå, kan du benytte :socialAccount alternativet for å registrere deg.',
+    'social_driver_not_found' => 'Autentiseringstjeneste fra sosiale medier er ikke installert',
+    'social_driver_not_configured' => 'Dine :socialAccount innstilliner er ikke angitt.',
+    'invite_token_expired' => 'Invitasjonslenken har utgått, du kan forsøke å be om nytt passord istede.',
+
+    // System
+    'path_not_writable' => 'Filstien :filePath aksepterer ikke filer, du må sjekke filstitilganger i systemet.',
+    'cannot_get_image_from_url' => 'Kan ikke hente bilde fra :url',
+    'cannot_create_thumbs' => 'Kan ikke opprette miniatyrbilder. GD PHP er ikke installert.',
+    'server_upload_limit' => 'Vedlegget er for stort, forsøk med et mindre vedlegg.',
+    'uploaded'  => 'Tjenesten aksepterer ikke vedlegg som er så stor.',
+    'image_upload_error' => 'Bildet kunne ikke lastes opp, forsøk igjen.',
+    'image_upload_type_error' => 'Bildeformatet støttes ikke, forsøk med et annet format.',
+    'file_upload_timeout' => 'Opplastingen gikk ut på tid.',
+
+    // Attachments
+    'attachment_not_found' => 'Vedlegget ble ikke funnet',
+
+    // Pages
+    'page_draft_autosave_fail' => 'Kunne ikke lagre utkastet, forsikre deg om at du er tilkoblet tjeneren (Har du nettilgang?)',
+    'page_custom_home_deletion' => 'Kan ikke slette en side som er satt som forside.',
+
+    // Entities
+    'entity_not_found' => 'Entitet ble ikke funnet',
+    'bookshelf_not_found' => 'Bokhyllen ble ikke funnet',
+    'book_not_found' => 'Boken ble ikke funnet',
+    'page_not_found' => 'Siden ble ikke funnet',
+    'chapter_not_found' => 'Kapittel ble ikke funnet',
+    'selected_book_not_found' => 'Den valgte boken eksisterer ikke',
+    'selected_book_chapter_not_found' => 'Den valgte boken eller kapittelet eksisterer ikke',
+    'guests_cannot_save_drafts' => 'Gjester kan ikke lagre utkast',
+
+    // Users
+    'users_cannot_delete_only_admin' => 'Du kan ikke kaste ut den eneste administratoren',
+    'users_cannot_delete_guest' => 'Du kan ikke slette gjestebrukeren (Du kan deaktivere offentlig visning istede)',
+
+    // Roles
+    'role_cannot_be_edited' => 'Denne rollen kan ikke endres',
+    'role_system_cannot_be_deleted' => 'Denne systemrollen kan ikke slettes',
+    'role_registration_default_cannot_delete' => 'Du kan ikke slette en rolle som er satt som registreringsrolle (rollen nye kontoer får når de registrerer seg)',
+    'role_cannot_remove_only_admin' => 'Denne brukeren er den eneste brukeren som er tildelt administratorrollen. Tilordne administratorrollen til en annen bruker før du prøver å fjerne den her.',
+
+    // Comments
+    'comment_list' => 'Det oppstod en feil under henting av kommentarene.',
+    'cannot_add_comment_to_draft' => 'Du kan ikke legge til kommentarer i et utkast.',
+    'comment_add' => 'Det oppsto en feil da kommentaren skulle legges til / oppdateres.',
+    'comment_delete' => 'Det oppstod en feil under sletting av kommentaren.',
+    'empty_comment' => 'Kan ikke legge til en tom kommentar.',
+
+    // Error pages
+    '404_page_not_found' => 'Siden finnes ikke',
+    'sorry_page_not_found' => 'Beklager, siden du leter etter ble ikke funnet.',
+    'sorry_page_not_found_permission_warning' => 'Hvis du forventet at denne siden skulle eksistere, har du kanskje ikke tillatelse til å se den.',
+    'return_home' => 'Gå til hovedside',
+    'error_occurred' => 'En feil oppsto',
+    'app_down' => ':appName er nede for øyeblikket',
+    'back_soon' => 'Den vil snart komme tilbake.',
+
+    // API errors
+    'api_no_authorization_found' => 'Ingen autorisasjonstoken ble funnet på forespørselen',
+    'api_bad_authorization_format' => 'Det ble funnet et autorisasjonstoken på forespørselen, men formatet virket feil',
+    'api_user_token_not_found' => 'Ingen samsvarende API-token ble funnet for det angitte autorisasjonstokenet',
+    'api_incorrect_token_secret' => 'Hemmeligheten som er gitt for det gitte brukte API-tokenet er feil',
+    'api_user_no_api_permission' => 'Eieren av det brukte API-tokenet har ikke tillatelse til å ringe API-samtaler',
+    'api_user_token_expired' => 'Autorisasjonstokenet som er brukt, har utløpt',
+
+    // Settings & Maintenance
+    'maintenance_test_email_failure' => 'Feil kastet når du sendte en test-e-post:',
+
+];
diff --git a/resources/lang/nb/pagination.php b/resources/lang/nb/pagination.php
new file mode 100644 (file)
index 0000000..d910da1
--- /dev/null
@@ -0,0 +1,12 @@
+<?php
+/**
+ * Pagination Language Lines
+ * The following language lines are used by the paginator library to build
+ * the simple pagination links.
+ */
+return [
+
+    'previous' => '&laquo; Forrige',
+    'next'     => 'Neste &raquo;',
+
+];
diff --git a/resources/lang/nb/passwords.php b/resources/lang/nb/passwords.php
new file mode 100644 (file)
index 0000000..8c3215b
--- /dev/null
@@ -0,0 +1,15 @@
+<?php
+/**
+ * Password Reminder Language Lines
+ * The following language lines are the default lines which match reasons
+ * that are given by the password broker for a password update attempt has failed.
+ */
+return [
+
+    'password' => 'Passord må inneholde minst åtte tegn og samsvarer med bekreftelsen.',
+    'user' => "Vi finner ikke en bruker med den e-postadressen.",
+    'token' => 'Passordet for tilbakestilling av passord er ugyldig for denne e-postadressen.',
+    'sent' => 'Vi har sendt e-postadressen til tilbakestilling av passordet ditt!',
+    'reset' => 'Passordet ditt har blitt tilbakestilt!',
+
+];
diff --git a/resources/lang/nb/settings.php b/resources/lang/nb/settings.php
new file mode 100644 (file)
index 0000000..873de5c
--- /dev/null
@@ -0,0 +1,230 @@
+<?php
+/**
+ * Settings text strings
+ * Contains all text strings used in the general settings sections of BookStack
+ * including users and roles.
+ */
+return [
+
+    // Common Messages
+    'settings' => 'Innstillinger',
+    'settings_save' => 'Lagre innstillinger',
+    'settings_save_success' => 'Innstillinger lagret',
+
+    // App Settings
+    'app_customization' => 'Tilpassing',
+    'app_features_security' => 'Funksjoner og sikkerhet',
+    'app_name' => 'Applikasjonsnavn',
+    'app_name_desc' => 'Dette navnet vises i overskriften og i alle e-postmeldinger som sendes av systemet.',
+    'app_name_header' => 'Vis navn i topptekst',
+    'app_public_access' => 'Offentlig tilgang',
+    'app_public_access_desc' => 'Hvis du aktiverer dette alternativet, kan besøkende, som ikke er logget på, få tilgang til innhold i din BookStack-forekomst.',
+    'app_public_access_desc_guest' => 'Tilgang for offentlige besøkende kan kontrolleres gjennom "Gjest" -brukeren.',
+    'app_public_access_toggle' => 'Tillat offentlig tilgang',
+    'app_public_viewing' => 'Tillat offentlig visning?',
+    'app_secure_images' => 'Høyere sikkerhet på bildeopplastinger',
+    'app_secure_images_toggle' => 'Enable høyere sikkerhet på bildeopplastinger',
+    'app_secure_images_desc' => 'Av ytelsesgrunner er alle bilder offentlige. Dette alternativet legger til en tilfeldig streng som er vanskelig å gjette foran bildets nettadresser. Forsikre deg om at katalogindekser ikke er aktivert for å forhindre enkel tilgang.',
+    'app_editor' => 'Tekstbehandler',
+    'app_editor_desc' => 'Velg hvilken tekstbehandler som skal brukes av alle brukere til å redigere sider.',
+    'app_custom_html' => 'Tilpasset HTML-hodeinnhold',
+    'app_custom_html_desc' => 'Alt innhold som legges til her, blir satt inn i bunnen av <head> -delen på hver side. Dette er praktisk for å overstyre stiler eller legge til analysekode.',
+    'app_custom_html_disabled_notice' => 'Tilpasset HTML-hodeinnhold er deaktivert på denne innstillingssiden for å sikre at eventuelle endringer ødelegger noe, kan tilbakestilles.',
+    'app_logo' => 'Applikasjonslogo',
+    'app_logo_desc' => 'Dette bildet skal være 43 px høyt. <br> Store bilder blir nedskalert.',
+    'app_primary_color' => 'Applikasjonens primærfarge',
+    'app_primary_color_desc' => 'Angir primærfargen for applikasjonen inkludert banner, knapper og lenker.',
+    'app_homepage' => 'Applikasjonens hjemmeside',
+    'app_homepage_desc' => 'Velg en visning som skal vises på hjemmesiden i stedet for standardvisningen. Sidetillatelser ignoreres for utvalgte sider.',
+    'app_homepage_select' => 'Velg en side',
+    'app_disable_comments' => 'Deaktiver kommentarer',
+    'app_disable_comments_toggle' => 'Deaktiver kommentarer',
+    'app_disable_comments_desc' => 'Deaktiver kommentarer på tvers av alle sidene i applikasjonen. <br> Eksisterende kommentarer vises ikke.',
+
+    // Color settings
+    'content_colors' => 'Innholdsfarger',
+    'content_colors_desc' => 'Angir farger for alle elementene i sideorganisasjonshierarkiet. Det anbefales å lese farger med en lignende lysstyrke som standardfargene for lesbarhet.',
+    'bookshelf_color' => 'Hyllefarge',
+    'book_color' => 'Bokfarge',
+    'chapter_color' => 'Kapittelfarge',
+    'page_color' => 'Sidefarge',
+    'page_draft_color' => 'Sideutkastsfarge',
+
+    // Registration Settings
+    'reg_settings' => 'Registrering',
+    'reg_enable' => 'Tillat registrering',
+    'reg_enable_toggle' => 'Tillat registrering',
+    'reg_enable_desc' => 'Når registrering er aktivert vil brukeren kunne registrere seg som applikasjonsbruker. Ved registrering får de en standard brukerrolle.',
+    'reg_default_role' => 'Standard brukerrolle etter registrering',
+    'reg_enable_external_warning' => 'Alternativet ovenfor ignoreres mens ekstern LDAP- eller SAML-autentisering er aktiv. Brukerkontoer for ikke-eksisterende medlemmer blir automatisk opprettet hvis autentisering mot det eksterne systemet i bruk lykkes.',
+    'reg_email_confirmation' => 'E-postbekreftelse',
+    'reg_email_confirmation_toggle' => 'Krev e-postbekreftelse',
+    'reg_confirm_email_desc' => 'Hvis domenebegrensning brukes, vil e-postbekreftelse være nødvendig, og dette alternativet vil bli ignorert.',
+    'reg_confirm_restrict_domain' => 'Domenebegrensning',
+    'reg_confirm_restrict_domain_desc' => 'Skriv inn en kommaseparert liste over e-postdomener du vil begrense registreringen til. Brukerne vil bli sendt en e-post for å bekrefte adressen deres før de får lov til å kommunisere med applikasjonen. <br> Vær oppmerksom på at brukere vil kunne endre e-postadressene sine etter vellykket registrering.',
+    'reg_confirm_restrict_domain_placeholder' => 'Ingen begrensninger er satt',
+
+    // Maintenance settings
+    'maint' => 'Maintenance',
+    'maint_image_cleanup' => 'Bildeopprydding',
+    'maint_image_cleanup_desc' => "Skanner side og revisjonsinnhold for å sjekke hvilke bilder og tegninger som for øyeblikket er i bruk, og hvilke bilder som er overflødige. Forsikre deg om at du lager en full database og sikkerhetskopiering av bilder før du kjører denne.",
+    'maint_image_cleanup_ignore_revisions' => 'Ignorer bilder i revisjoner',
+    'maint_image_cleanup_run' => 'Kjør opprydding',
+    'maint_image_cleanup_warning' => ':count potensielt ubrukte bilder ble funnet. Er du sikker på at du vil slette disse bildene?',
+    'maint_image_cleanup_success' => ':count potensielt ubrukte bilder funnet og slettet!',
+    'maint_image_cleanup_nothing_found' => 'Ingen ubrukte bilder funnet, ingenting slettet!',
+    'maint_send_test_email' => 'Send en test-e-post',
+    'maint_send_test_email_desc' => 'Dette sender en test-e-post til din e-postadresse som er angitt i profilen din.',
+    'maint_send_test_email_run' => 'Send en test-e-post',
+    'maint_send_test_email_success' => 'Send en test-e-post til :address',
+    'maint_send_test_email_mail_subject' => 'Test-e-post',
+    'maint_send_test_email_mail_greeting' => 'E-postsending ser ut til å fungere!',
+    'maint_send_test_email_mail_text' => 'Gratulerer! Da du mottok dette e-postvarselet, ser det ut til at e-postinnstillingene dine er konfigurert riktig.',
+
+    // Audit Log
+    'audit' => 'Revisjonslogg',
+    'audit_desc' => 'Denne revisjonsloggen viser en liste over aktiviteter som spores i systemet. Denne listen er ufiltrert i motsetning til lignende aktivitetslister i systemet der tillatelsesfiltre brukes.',
+    'audit_event_filter' => 'Hendelsesfilter',
+    'audit_event_filter_no_filter' => 'Ingen filter',
+    'audit_deleted_item' => 'Slettet ting',
+    'audit_deleted_item_name' => 'Navn: :name',
+    'audit_table_user' => 'Kontoholder',
+    'audit_table_event' => 'Hendelse',
+    'audit_table_item' => 'Relatert ting',
+    'audit_table_date' => 'Aktivitetsdato',
+    'audit_date_from' => 'Datoperiode fra',
+    'audit_date_to' => 'Datoperiode til',
+
+    // Role Settings
+    'roles' => 'Roller',
+    'role_user_roles' => 'Kontoroller',
+    'role_create' => 'Opprett ny rolle',
+    'role_create_success' => 'Rolle opprettet',
+    'role_delete' => 'Rolle slettet',
+    'role_delete_confirm' => 'Dette vil slette rollen «:roleName».',
+    'role_delete_users_assigned' => 'Denne rollen har :userCount kontoer koblet opp mot seg. Velg hvilke rolle du vil flytte disse til.',
+    'role_delete_no_migration' => "Ikke flytt kontoer",
+    'role_delete_sure' => 'Er du sikker på at du vil slette rollen?',
+    'role_delete_success' => 'Rollen ble slettet',
+    'role_edit' => 'Endre rolle',
+    'role_details' => 'Rolledetaljer',
+    'role_name' => 'Rollenavn',
+    'role_desc' => 'Kort beskrivelse av rolle',
+    'role_external_auth_id' => 'Ekstern godkjennings-ID',
+    'role_system' => 'Systemtilganger',
+    'role_manage_users' => 'Behandle kontoer',
+    'role_manage_roles' => 'Behandle roller og rolletilganger',
+    'role_manage_entity_permissions' => 'Behandle bok-, kapittel- og sidetilganger',
+    'role_manage_own_entity_permissions' => 'Behandle tilganger på egne verk',
+    'role_manage_page_templates' => 'Behandle sidemaler',
+    'role_access_api' => 'Systemtilgang API',
+    'role_manage_settings' => 'Behandle applikasjonsinnstillinger',
+    'role_asset' => 'Eiendomstillatelser',
+    'roles_system_warning' => 'Vær oppmerksom på at tilgang til noen av de ovennevnte tre tillatelsene kan tillate en bruker å endre sine egne rettigheter eller rettighetene til andre i systemet. Bare tildel roller med disse tillatelsene til pålitelige brukere.',
+    'role_asset_desc' => 'Disse tillatelsene kontrollerer standard tilgang til eiendelene i systemet. Tillatelser til bøker, kapitler og sider overstyrer disse tillatelsene.',
+    'role_asset_admins' => 'Administratorer får automatisk tilgang til alt innhold, men disse alternativene kan vise eller skjule UI-alternativer.',
+    'role_all' => 'Alle',
+    'role_own' => 'Egne',
+    'role_controlled_by_asset' => 'Kontrollert av eiendelen de er lastet opp til',
+    'role_save' => 'Lagre rolle',
+    'role_update_success' => 'Rollen ble oppdatert',
+    'role_users' => 'Kontoholdere med denne rollen',
+    'role_users_none' => 'Ingen kontoholdere er gitt denne rollen',
+
+    // Users
+    'users' => 'Users',
+    'user_profile' => 'Profil',
+    'users_add_new' => 'Register ny konto',
+    'users_search' => 'Søk i kontoer',
+    'users_details' => 'Kontodetaljer',
+    'users_details_desc' => 'Angi et visningsnavn og en e-postadresse for denne kontoholderen. E-postadressen vil bli brukt til å logge på applikasjonen.',
+    'users_details_desc_no_email' => 'Angi et visningsnavn for denne kontoholderen slik at andre kan gjenkjenne dem.',
+    'users_role' => 'Roller',
+    'users_role_desc' => 'Velg hvilke roller denne kontoholderen vil bli tildelt. Hvis en kontoholderen er tildelt flere roller, vil tillatelsene fra disse rollene stable seg, og de vil motta alle evnene til de tildelte rollene.',
+    'users_password' => 'Passord',
+    'users_password_desc' => 'Angi et passord som brukes til å logge på applikasjonen. Dette må bestå av minst 6 tegn.',
+    'users_send_invite_text' => 'Du kan velge å sende denne kontoholderen en invitasjons-e-post som lar dem angi sitt eget passord, ellers kan du selv angi passordet.',
+    'users_send_invite_option' => 'Send invitasjonsmelding',
+    'users_external_auth_id' => 'Ekstern godkjennings-ID',
+    'users_external_auth_id_desc' => 'Dette er ID-en som brukes til å matche denne kontoholderen når de kommuniserer med det eksterne autentiseringssystemet.',
+    'users_password_warning' => 'Fyll bare ut nedenfor hvis du vil endre passordet ditt.',
+    'users_system_public' => 'Denne brukeren representerer alle gjester som besøker appliaksjonen din. Den kan ikke brukes til å logge på, men tildeles automatisk.',
+    'users_delete' => 'Slett konto',
+    'users_delete_named' => 'Slett kontoen :userName',
+    'users_delete_warning' => 'Dette vil fullstendig slette denne brukeren med navnet «:userName» fra systemet.',
+    'users_delete_confirm' => 'Er du sikker på at du vil slette denne kontoen?',
+    'users_delete_success' => 'Konto slettet',
+    'users_edit' => 'Rediger konto',
+    'users_edit_profile' => 'Rediger profil',
+    'users_edit_success' => 'Kontoen ble oppdatert',
+    'users_avatar' => 'Kontobilde',
+    'users_avatar_desc' => 'Velg et bilde for å representere denne kontoholderen. Dette skal være omtrent 256px kvadrat.',
+    'users_preferred_language' => 'Foretrukket språk',
+    'users_preferred_language_desc' => 'Dette alternativet vil endre språket som brukes til brukergrensesnittet til applikasjonen. Dette påvirker ikke noe brukeropprettet innhold.',
+    'users_social_accounts' => 'Sosiale kontoer',
+    'users_social_accounts_info' => 'Her kan du koble andre kontoer for raskere og enklere pålogging. Hvis du frakobler en konto her, tilbakekaller ikke dette tidligere autorisert tilgang. Tilbakekall tilgang fra profilinnstillingene dine på den tilkoblede sosiale kontoen.',
+    'users_social_connect' => 'Koble til konto',
+    'users_social_disconnect' => 'Koble fra konto',
+    'users_social_connected' => ':socialAccount ble lagt til din konto.',
+    'users_social_disconnected' => ':socialAccount ble koblet fra din konto.',
+    'users_api_tokens' => 'API-nøkler',
+    'users_api_tokens_none' => 'Ingen API-nøkler finnes for denne kontoen',
+    'users_api_tokens_create' => 'Opprett nøkkel',
+    'users_api_tokens_expires' => 'Utløper',
+    'users_api_tokens_docs' => 'API-dokumentasjon',
+
+    // API Tokens
+    'user_api_token_create' => 'Opprett API-nøkkel',
+    'user_api_token_name' => 'Navn',
+    'user_api_token_name_desc' => 'Gi nøkkelen et lesbart navn som en fremtidig påminnelse om det tiltenkte formålet.',
+    'user_api_token_expiry' => 'Utløpsdato',
+    'user_api_token_expiry_desc' => 'Angi en dato da denne nøkkelen utløper. Etter denne datoen vil forespørsler som er gjort med denne nøkkelen ikke lenger fungere. Å la dette feltet stå tomt vil sette utløpsdato 100 år inn i fremtiden.',
+    'user_api_token_create_secret_message' => 'Umiddelbart etter å ha opprettet denne nøkkelen vil en identifikator og hemmelighet bli generert og vist. Hemmeligheten vil bare vises en gang, så husk å kopiere verdien til et trygt sted før du fortsetter.',
+    'user_api_token_create_success' => 'API-nøkkel ble opprettet',
+    'user_api_token_update_success' => 'API-nøkkel ble oppdatert',
+    'user_api_token' => 'API-nøkkel',
+    'user_api_token_id' => 'Identifikator',
+    'user_api_token_id_desc' => 'Dette er en ikke-redigerbar systemgenerert identifikator for denne nøkkelen som må oppgis i API-forespørsler.',
+    'user_api_token_secret' => 'Hemmelighet',
+    'user_api_token_secret_desc' => 'Dette er en systemgenerert hemmelighet for denne nøkkelen som må leveres i API-forespørsler. Dette vises bare denne gangen, så kopier denne verdien til et trygt sted.',
+    'user_api_token_created' => 'Nøkkel opprettet :timeAgo',
+    'user_api_token_updated' => 'Nøkkel oppdatert :timeAgo',
+    'user_api_token_delete' => 'Slett nøkkel',
+    'user_api_token_delete_warning' => 'Dette vil slette API-nøkkelen \':tokenName\' fra systemet.',
+    'user_api_token_delete_confirm' => 'Sikker på at du vil slette nøkkelen?',
+    'user_api_token_delete_success' => 'API-nøkkelen ble slettet',
+
+    //! If editing translations files directly please ignore this in all
+    //! languages apart from en. Content will be auto-copied from en.
+    //!////////////////////////////////
+    'language_select' => [
+        'en' => 'English',
+        'ar' => 'العربية',
+        'bg' => 'Bǎlgarski',
+        'cs' => 'Česky',
+        'da' => 'Dansk',
+        'de' => 'Deutsch (Sie)',
+        'de_informal' => 'Deutsch (Du)',
+        'es' => 'Español',
+        'es_AR' => 'Español Argentina',
+        'fr' => 'Français',
+        'he' => 'עברית',
+        'hu' => 'Magyar',
+        'it' => 'Italian',
+        'ja' => '日本語',
+        'ko' => '한국어',
+        'nl' => 'Nederlands',
+        'pl' => 'Polski',
+        'pt_BR' => 'Português do Brasil',
+        'ru' => 'Русский',
+        'sk' => 'Slovensky',
+        'sl' => 'Slovenščina',
+        'sv' => 'Svenska',
+        'tr' => 'Türkçe',
+        'uk' => 'Українська',
+        'vi' => 'Tiếng Việt',
+        'zh_CN' => '简体中文',
+        'zh_TW' => '繁體中文',
+    ]
+    //!////////////////////////////////
+];
diff --git a/resources/lang/nb/validation.php b/resources/lang/nb/validation.php
new file mode 100644 (file)
index 0000000..8060931
--- /dev/null
@@ -0,0 +1,114 @@
+<?php
+/**
+ * Validation Lines
+ * following language lines contain default error messages used by
+ * validator class. Some of these rules have multiple versions such
+ * as size rules. Feel free to tweak each of these messages here.
+ */
+return [
+
+    // Standard laravel validation lines
+    'accepted'             => ':attribute må aksepteres.',
+    'active_url'           => ':attribute er ikke en godkjent URL.',
+    'after'                => ':attribute må være en dato etter :date.',
+    'alpha'                => ':attribute kan kun inneholde bokstaver.',
+    'alpha_dash'           => ':attribute kan kunne inneholde bokstaver, tall, bindestreker eller understreker.',
+    'alpha_num'            => ':attribute kan kun inneholde bokstaver og tall.',
+    'array'                => ':attribute må være en liste.',
+    'before'               => ':attribute må være en dato før :date.',
+    'between'              => [
+        'numeric' => ':attribute må være mellom :min og :max.',
+        'file'    => ':attribute må være mellom :min og :max kilobytes.',
+        'string'  => ':attribute må være mellom :min og :max tegn.',
+        'array'   => ':attribute må være mellom :min og :max ting.',
+    ],
+    'boolean'              => ':attribute feltet kan bare være sann eller falsk.',
+    'confirmed'            => ':attribute bekreftelsen samsvarer ikke.',
+    'date'                 => ':attribute er ikke en gyldig dato.',
+    'date_format'          => ':attribute samsvarer ikke med :format.',
+    'different'            => ':attribute og :other må være forskjellige.',
+    'digits'               => ':attribute må være :digits tall.',
+    'digits_between'       => ':attribute må være mellomg :min og :max tall.',
+    'email'                => ':attribute må være en gyldig e-post.',
+    'ends_with' => ':attribute må slutte med en av verdiene: :values',
+    'filled'               => ':attribute feltet er påkrevd.',
+    'gt'                   => [
+        'numeric' => ':attribute må være større enn :value.',
+        'file'    => ':attribute må være større enn :value kilobytes.',
+        'string'  => ':attribute må være større enn :value tegn.',
+        'array'   => ':attribute må ha mer en :value ting.',
+    ],
+    'gte'                  => [
+        'numeric' => ':attribute må være større enn eller lik :value.',
+        'file'    => ':attribute må være større enn eller lik :value kilobytes.',
+        'string'  => ':attribute må være større enn eller lik :value tegn.',
+        'array'   => ':attribute må ha :value eller flere ting.',
+    ],
+    'exists'               => 'Den valgte :attribute er ugyldig.',
+    'image'                => ':attribute må være et bilde.',
+    'image_extension'      => ':attribute må ha støttet formattype.',
+    'in'                   => 'Den valgte :attribute er ugyldig.',
+    'integer'              => ':attribute må være et heltall',
+    'ip'                   => ':attribute må være en gyldig IP adresse.',
+    'ipv4'                 => ':attribute må være en gyldig IPv4 adresse.',
+    'ipv6'                 => ':attribute må være en gyldig IPv6 adresse.',
+    'json'                 => ':attribute må være en gyldig JSON tekststreng.',
+    'lt'                   => [
+        'numeric' => ':attribute må være mindre enn :value.',
+        'file'    => ':attribute må være mindre enn :value kilobytes.',
+        'string'  => ':attribute må være mindre enn :value tegn.',
+        'array'   => ':attribute må ha mindre enn :value ting.',
+    ],
+    'lte'                  => [
+        'numeric' => ':attribute må være mindre enn eller lik :value.',
+        'file'    => ':attribute må være mindre enn eller lik :value kilobytes.',
+        'string'  => ':attribute må være mindre enn eller lik :value characters.',
+        'array'   => ':attribute må ha mindre enn eller lik :value ting.',
+    ],
+    'max'                  => [
+        'numeric' => ':attribute kan ikke være større enn :max.',
+        'file'    => ':attribute kan ikke være større enn :max kilobytes.',
+        'string'  => ':attribute kan ikke være større enn :max tegn.',
+        'array'   => ':attribute kan ikke inneholde mer enn :max ting.',
+    ],
+    'mimes'                => ':attribute må være en fil av typen: :values.',
+    'min'                  => [
+        'numeric' => ':attribute må være på minst :min.',
+        'file'    => ':attribute må være på minst :min kilobytes.',
+        'string'  => ':attribute må være på minst :min tegn.',
+        'array'   => ':attribute må minst ha :min ting.',
+    ],
+    'no_double_extension'  => ':attribute kan bare ha en formattype spesifisert.',
+    'not_in'               => 'Den valgte :attribute er ugyldig.',
+    'not_regex'            => ':attribute format er ugyldig.',
+    'numeric'              => ':attribute må være et nummer.',
+    'regex'                => ':attribute format er ugyldig.',
+    'required'             => ':attribute feltet er påkrevt.',
+    'required_if'          => ':attribute feltet er påkrevt når :other er :value.',
+    'required_with'        => ':attribute feltet er påkrevt når :values er tilgjengelig.',
+    'required_with_all'    => ':attribute feltet er påkrevt når :values er tilgjengelig',
+    'required_without'     => ':attribute feltet er påkrevt når :values ikke er tilgjengelig.',
+    'required_without_all' => ':attribute feltet er påkrevt når ingen av :values er tilgjengelig.',
+    'same'                 => ':attribute og :other må samsvare.',
+    'size'                 => [
+        'numeric' => ':attribute må være :size.',
+        'file'    => ':attribute må være :size kilobytes.',
+        'string'  => ':attribute må være :size tegn.',
+        'array'   => ':attribute må inneholde :size ting.',
+    ],
+    'string'               => ':attribute må være en tekststreng.',
+    'timezone'             => ':attribute må være en tidssone.',
+    'unique'               => ':attribute har allerede blitt tatt.',
+    'url'                  => ':attribute format er ugyldig.',
+    'uploaded'             => 'kunne ikke lastes opp, tjeneren støtter ikke filer av denne størrelsen.',
+
+    // Custom validation lines
+    'custom' => [
+        'password-confirm' => [
+            'required_with' => 'passordbekreftelse er påkrevd',
+        ],
+    ],
+
+    // Custom validation attributes
+    'attributes' => [],
+];
index 5b219b9ae33ae77ad24e73d4ddd676421316608a..75adf12aacde34a99b795c7278320f6ebdfee62b 100644 (file)
 .sticky-sidebar {
   position: sticky;
   top: $-m;
+  max-height: calc(100vh - #{$-m});
+  overflow-y: auto;
 }
index e01ecebc949c2e7154ae4186d13d3171b67bcd79..c51f0165922b3c274e200fa79585a2ca246dc158 100644 (file)
@@ -92,6 +92,6 @@
 .bg-chapter {
   background-color: var(--color-chapter);
 }
-.bg-shelf {
+.bg-bookshelf {
   background-color: var(--color-bookshelf);
 }
index eb40741d14f9932c4fbd9d20bf02b1d2380db1cf..ede26c51ca3d16bd68530105bfab92b4482c5aaa 100644 (file)
@@ -724,4 +724,65 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
   .template-item-actions button:first-child {
     border-top: 0;
   }
+}
+
+.dropdown-search-dropdown {
+  box-shadow: $bs-med;
+  overflow: hidden;
+  min-height: 100px;
+  width: 240px;
+  display: none;
+  position: absolute;
+  z-index: 80;
+  right: -$-m;
+  @include rtl {
+    right: auto;
+    left: -$-m;
+  }
+  .dropdown-search-search .svg-icon {
+    position: absolute;
+    left: $-s;
+    @include rtl {
+      right: $-s;
+      left: auto;
+    }
+    top: 11px;
+    fill: #888;
+    pointer-events: none;
+  }
+  .dropdown-search-list {
+    max-height: 400px;
+    overflow-y: scroll;
+    text-align: start;
+  }
+  .dropdown-search-item {
+    padding: $-s $-m;
+    &:hover,&:focus {
+      background-color: #F2F2F2;
+      text-decoration: none;
+    }
+  }
+  input {
+    padding-inline-start: $-xl;
+    border-radius: 0;
+    border: 0;
+    border-bottom: 1px solid #DDD;
+  }
+}
+
+@include smaller-than($m) {
+  .dropdown-search-dropdown {
+    position: fixed;
+    right: auto;
+    left: $-m;
+  }
+  .dropdown-search-dropdown .dropdown-search-list {
+    max-height: 240px;
+  }
+}
+
+.custom-select-input {
+  max-width: 280px;
+  border: 1px solid #DDD;
+  border-radius: 4px;
 }
\ No newline at end of file
index e19bb4f612f373461545ff97c4b7078bc5fd31ae..246ef4b5bdabdfba587b93d96b363edd67407808 100644 (file)
@@ -269,9 +269,9 @@ header .search-box {
   }
 }
 
-.breadcrumb-listing {
+.dropdown-search {
   position: relative;
-  .breadcrumb-listing-toggle {
+  .dropdown-search-toggle {
     padding: 6px;
     border: 1px solid transparent;
     border-radius: 4px;
@@ -284,54 +284,6 @@ header .search-box {
   }
 }
 
-.breadcrumb-listing-dropdown {
-  box-shadow: $bs-med;
-  overflow: hidden;
-  min-height: 100px;
-  width: 240px;
-  display: none;
-  position: absolute;
-  z-index: 80;
-  right: -$-m;
-  @include rtl {
-    right: auto;
-    left: -$-m;
-  }
-  .breadcrumb-listing-search .svg-icon {
-    position: absolute;
-    left: $-s;
-    @include rtl {
-      right: $-s;
-      left: auto;
-    }
-    top: 11px;
-    fill: #888;
-    pointer-events: none;
-  }
-  .breadcrumb-listing-entity-list {
-    max-height: 400px;
-    overflow-y: scroll;
-    text-align: start;
-  }
-  input {
-    padding-inline-start: $-xl;
-    border-radius: 0;
-    border: 0;
-    border-bottom: 1px solid #DDD;
-  }
-}
-
-@include smaller-than($m) {
-  .breadcrumb-listing-dropdown {
-    position: fixed;
-    right: auto;
-    left: $-m;
-  }
-  .breadcrumb-listing-dropdown .breadcrumb-listing-entity-list {
-    max-height: 240px;
-  }
-}
-
 .faded {
   a, button, span, span > div {
     color: #666;
index 519cb27ad7787f31199276fbe821d87334a76603..4873ff2da651ee1575a048a4973f95069fa0a343 100644 (file)
@@ -150,28 +150,38 @@ body.flexbox {
 .justify-flex-end {
   justify-content: flex-end;
 }
+.justify-center {
+  justify-content: center;
+}
+.items-center {
+  align-items: center;
+}
 
 
 /**
  * Display and float utilities
  */
 .block {
-  display: block;
+  display: block !important;
   position: relative;
 }
 
 .inline {
-  display: inline;
+  display: inline !important;
 }
 
 .block.inline {
-  display: inline-block;
+  display: inline-block !important;
 }
 
 .hidden {
   display: none !important;
 }
 
+.fill-height {
+  height: 100%;
+}
+
 .float {
   float: left;
   &.right {
@@ -269,6 +279,7 @@ body.flexbox {
     min-height: 50vh;
     overflow-y: scroll;
     overflow-x: hidden;
+    height: 100%;
     scrollbar-width: none;
     -ms-overflow-style: none;
     &::-webkit-scrollbar {
@@ -347,4 +358,4 @@ body.flexbox {
     margin-inline-start: 0;
     margin-inline-end: 0;
   }
-}
\ No newline at end of file
+}
index 4ada3472592e4089c30c4c1ddb3e3fec5ad39985..4322cb5a606da731f00a692ed97f1c8ee325a723 100644 (file)
@@ -352,12 +352,21 @@ li > ol, li > ul {
   overflow-wrap: break-word;
 }
 
-.limit-text {
+.text-limit-lines-1 {
   white-space: nowrap;
   overflow: hidden;
   text-overflow: ellipsis;
 }
 
+.text-limit-lines-2 {
+  // -webkit use here is actually standardised cross-browser:
+  // https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-line-clamp
+  display: -webkit-box;
+  -webkit-box-orient: vertical;
+  -webkit-line-clamp: 2;
+  overflow: hidden;
+}
+
 /**
  * Grouping
  */
index 376541b5dcba5d67b476f269b1c60ab8aab61131..78d94f977f8d0043679557148fc00af97a8cd276 100644 (file)
@@ -290,12 +290,12 @@ $btt-size: 40px;
   }
 }
 
-table a.audit-log-user {
+table.table .table-user-item {
   display: grid;
   grid-template-columns: 42px 1fr;
   align-items: center;
 }
-table a.icon-list-item {
+table.table .table-entity-item {
   display: grid;
   grid-template-columns: 36px 1fr;
   align-items: center;
index e86a24e816a339e0a443bffef08bbf28b7ab6685..f62b895827b7c87b0a3f47ed82d66037fd1d5ad1 100644 (file)
@@ -41,9 +41,9 @@
         <ul class="contents">
             @foreach($bookChildren as $bookChild)
                 <li><a href="#{{$bookChild->getType()}}-{{$bookChild->id}}">{{ $bookChild->name }}</a></li>
-                @if($bookChild->isA('chapter') && count($bookChild->pages) > 0)
+                @if($bookChild->isA('chapter') && count($bookChild->visible_pages) > 0)
                     <ul>
-                        @foreach($bookChild->pages as $page)
+                        @foreach($bookChild->visible_pages as $page)
                             <li><a href="#page-{{$page->id}}">{{ $page->name }}</a></li>
                         @endforeach
                     </ul>
@@ -59,8 +59,8 @@
         @if($bookChild->isA('chapter'))
             <p>{{ $bookChild->description }}</p>
 
-            @if(count($bookChild->pages) > 0)
-                @foreach($bookChild->pages as $page)
+            @if(count($bookChild->visible_pages) > 0)
+                @foreach($bookChild->visible_pages as $page)
                     <div class="page-break"></div>
                     <div class="chapter-hint">{{$bookChild->name}}</div>
                     <h1 id="page-{{$page->id}}">{{ $page->name }}</h1>
diff --git a/resources/views/books/grid-item.blade.php b/resources/views/books/grid-item.blade.php
deleted file mode 100644 (file)
index e1d3775..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-<a href="{{$book->getUrl()}}" class="grid-card"  data-entity-type="book" data-entity-id="{{$book->id}}">
-    <div class="bg-book featured-image-container-wrap">
-        <div class="featured-image-container" @if($book->cover) style="background-image: url('{{ $book->getBookCover() }}')"@endif>
-        </div>
-        @icon('book')
-    </div>
-    <div class="grid-card-content">
-        <h2>{{$book->getShortName(35)}}</h2>
-        @if(isset($book->searchSnippet))
-            <p class="text-muted">{!! $book->searchSnippet !!}</p>
-        @else
-            <p class="text-muted">{{ $book->getExcerpt(130) }}</p>
-        @endif
-    </div>
-    <div class="grid-card-footer text-muted ">
-        <p>@icon('star')<span title="{{$book->created_at->toDayDateTimeString()}}">{{ trans('entities.meta_created', ['timeLength' => $book->created_at->diffForHumans()]) }}</span></p>
-        <p>@icon('edit')<span title="{{ $book->updated_at->toDayDateTimeString() }}">{{ trans('entities.meta_updated', ['timeLength' => $book->updated_at->diffForHumans()]) }}</span></p>
-    </div>
-</a>
\ No newline at end of file
index 42a2757f94e0e949d536791f6dd79f4daa5ea38a..52cd935d1182a7babb4b2926e56f8586ecf08cf8 100644 (file)
@@ -1,4 +1,3 @@
-
 <main class="content-wrap mt-m card">
     <div class="grid half v-center no-row-gap">
         <h1 class="list-heading">{{ trans('entities.books') }}</h1>
@@ -22,7 +21,7 @@
         @else
              <div class="grid third">
                 @foreach($books as $key => $book)
-                    @include('books.grid-item', ['book' => $book])
+                    @include('partials.entity-grid-item', ['entity' => $book])
                 @endforeach
              </div>
         @endif
index 98f0af87eeeaa711408a6c010956bb68d9c273a3..f043735bbf4c9c0df001d5f40fc565756b467550 100644 (file)
@@ -13,8 +13,8 @@
     <ul class="sortable-page-list sort-list">
 
         @foreach($bookChildren as $bookChild)
-            <li class="text-{{ $bookChild->getClassName() }}"
-                data-id="{{$bookChild->id}}" data-type="{{ $bookChild->getClassName() }}"
+            <li class="text-{{ $bookChild->getType() }}"
+                data-id="{{$bookChild->id}}" data-type="{{ $bookChild->getType() }}"
                 data-name="{{ $bookChild->name }}" data-created="{{ $bookChild->created_at->timestamp }}"
                 data-updated="{{ $bookChild->updated_at->timestamp }}">
                 <div class="entity-list-item">
@@ -28,7 +28,7 @@
                 </div>
                 @if($bookChild->isA('chapter'))
                     <ul>
-                        @foreach($bookChild->pages as $page)
+                        @foreach($bookChild->visible_pages as $page)
                             <li class="text-page"
                                 data-id="{{$page->id}}" data-type="page"
                                 data-name="{{ $page->name }}" data-created="{{ $page->created_at->timestamp }}"
index 6137c34e8fce357653db2583e3a21cb413f01aba..a1358e1db4e0398cc8d339a79213df190a2ab3f5 100644 (file)
@@ -1,10 +1,10 @@
 <div class="chapter-child-menu">
     <button chapter-toggle type="button" aria-expanded="{{ $isOpen ? 'true' : 'false' }}"
             class="text-muted @if($isOpen) open @endif">
-        @icon('caret-right') @icon('page') <span>{{ trans_choice('entities.x_pages', $bookChild->pages->count()) }}</span>
+        @icon('caret-right') @icon('page') <span>{{ trans_choice('entities.x_pages', $bookChild->visible_pages->count()) }}</span>
     </button>
     <ul class="sub-menu inset-list @if($isOpen) open @endif" @if($isOpen) style="display: block;" @endif role="menu">
-        @foreach($bookChild->pages as $childPage)
+        @foreach($bookChild->visible_pages as $childPage)
             <li class="list-item-page {{ $childPage->isA('page') && $childPage->draft ? 'draft' : '' }}" role="presentation">
                 @include('partials.entity-list-item-basic', ['entity' => $childPage, 'classes' => $current->matches($childPage)? 'selected' : '' ])
             </li>
index 7e2e0e1c539c9dca666540a245c5cf2342ea9384..9186983332eaae842d8af07aff34abbfcfd6f994 100644 (file)
@@ -1,4 +1,6 @@
-<a href="{{ $chapter->getUrl() }}" class="chapter entity-list-item @if($chapter->hasChildren()) has-children @endif" data-entity-type="chapter" data-entity-id="{{$chapter->id}}">
+{{--This view display child pages in a list if pre-loaded onto a 'visible_pages' property,--}}
+{{--To ensure that the pages have been loaded efficiently with permissions taken into account.--}}
+<a href="{{ $chapter->getUrl() }}" class="chapter entity-list-item @if($chapter->visible_pages->count() > 0) has-children @endif" data-entity-type="chapter" data-entity-id="{{$chapter->id}}">
     <span class="icon text-chapter">@icon('chapter')</span>
     <div class="content">
         <h4 class="entity-list-item-name break-text">{{ $chapter->name }}</h4>
@@ -7,16 +9,16 @@
         </div>
     </div>
 </a>
-@if ($chapter->hasChildren())
+@if ($chapter->visible_pages->count() > 0)
     <div class="chapter chapter-expansion">
         <span class="icon text-chapter">@icon('page')</span>
         <div class="content">
             <button type="button" chapter-toggle
                     aria-expanded="false"
-                    class="text-muted chapter-expansion-toggle">@icon('caret-right') <span>{{ trans_choice('entities.x_pages', $chapter->pages->count()) }}</span></button>
+                    class="text-muted chapter-expansion-toggle">@icon('caret-right') <span>{{ trans_choice('entities.x_pages', $chapter->visible_pages->count()) }}</span></button>
             <div class="inset-list">
                 <div class="entity-list-item-children">
-                    @include('partials.entity-list', ['entities' => $chapter->pages])
+                    @include('partials.entity-list', ['entities' => $chapter->visible_pages])
                 </div>
             </div>
         </div>
index 827abcac601d8d6e4276880085a1a27aadb7a960..80e79410a04ff3aeee9c4324ca3978e6b4eabf35 100644 (file)
@@ -29,7 +29,7 @@
                 <div class="links text-center">
                     @if (hasAppAccess())
                         <a class="hide-over-l" href="{{ url('/search') }}">@icon('search'){{ trans('common.search') }}</a>
-                        @if(userCanOnAny('view', \BookStack\Entities\Bookshelf::class) || userCan('bookshelf-view-all') || userCan('bookshelf-view-own'))
+                        @if(userCanOnAny('view', \BookStack\Entities\Models\Bookshelf::class) || userCan('bookshelf-view-all') || userCan('bookshelf-view-own'))
                             <a href="{{ url('/shelves') }}">@icon('bookshelf'){{ trans('entities.shelves') }}</a>
                         @endif
                         <a href="{{ url('/books') }}">@icon('books'){{ trans('entities.books') }}</a>
index e24ea49f1c82a7a374f8c8cf0c51392a40cac943..c59615d92a30a38fbb0aa66feba92d5a56c0ad1f 100644 (file)
@@ -3,7 +3,7 @@
 <div page-picker>
     <div class="input-base">
         <span @if($value) style="display: none" @endif page-picker-default class="text-muted italic">{{ $placeholder }}</span>
-        <a @if(!$value) style="display: none" @endif href="{{ url('/link/' . $value) }}" target="_blank" class="text-page" page-picker-display>#{{$value}}, {{$value ? \BookStack\Entities\Page::find($value)->name : '' }}</a>
+        <a @if(!$value) style="display: none" @endif href="{{ url('/link/' . $value) }}" target="_blank" class="text-page" page-picker-display>#{{$value}}, {{$value ? \BookStack\Entities\Models\Page::find($value)->name : '' }}</a>
     </div>
     <br>
     <input type="hidden" value="{{$value}}" name="{{$name}}" id="{{$name}}">
diff --git a/resources/views/components/user-select-list.blade.php b/resources/views/components/user-select-list.blade.php
new file mode 100644 (file)
index 0000000..2c49e96
--- /dev/null
@@ -0,0 +1,6 @@
+@foreach($users as $user)
+    <a href="#" class="flex-container-row items-center dropdown-search-item" data-id="{{ $user->id }}">
+        <img class="avatar mr-m" src="{{ $user->getAvatar(30) }}" alt="{{ $user->name }}">
+        <span>{{ $user->name }}</span>
+    </a>
+@endforeach
\ No newline at end of file
diff --git a/resources/views/components/user-select.blade.php b/resources/views/components/user-select.blade.php
new file mode 100644 (file)
index 0000000..2a07f0b
--- /dev/null
@@ -0,0 +1,34 @@
+<div class="dropdown-search custom-select-input" components="dropdown dropdown-search user-select"
+     option:dropdown-search:url="/search/users/select"
+>
+    <input refs="user-select@input" type="hidden" name="{{ $name }}" value="{{ $user->id ?? '' }}">
+    <div refs="dropdown@toggle"
+         class="dropdown-search-toggle flex-container-row items-center"
+         aria-haspopup="true" aria-expanded="false" tabindex="0">
+        <div refs="user-select@user-info" class="flex-container-row items-center px-s">
+            @if($user)
+                <img class="avatar mr-m" src="{{ $user->getAvatar(30) }}" alt="{{ $user->name }}">
+                <span>{{ $user->name }}</span>
+            @else
+                <span>{{ trans('settings.users_none_selected') }}</span>
+            @endif
+        </div>
+        <span style="font-size: 1.5rem; margin-left: auto;">
+            @icon('caret-down')
+        </span>
+    </div>
+    <div refs="dropdown@menu" class="dropdown-search-dropdown card" role="menu">
+        <div class="dropdown-search-search">
+            @icon('search')
+            <input refs="dropdown-search@searchInput"
+                   aria-label="{{ trans('common.search') }}"
+                   autocomplete="off"
+                   placeholder="{{ trans('common.search') }}"
+                   type="text">
+        </div>
+        <div refs="dropdown-search@loading" class="text-center">
+            @include('partials.loading-icon')
+        </div>
+        <div refs="dropdown-search@listContainer" class="dropdown-search-list"></div>
+    </div>
+</div>
\ No newline at end of file
index 3581a545b1c96f6b082b11c3a379747251f13ad1..35490bed9a89e74f3eeb071e708e5618fa719807 100644 (file)
@@ -2,15 +2,26 @@
     {!! csrf_field() !!}
     <input type="hidden" name="_method" value="PUT">
 
-    <p class="mb-none">{{ trans('entities.permissions_intro') }}</p>
-
-    <div class="form-group">
-        @include('form.checkbox', [
-            'name' => 'restricted',
-            'label' => trans('entities.permissions_enable'),
-        ])
+    <div class="grid half left-focus v-center">
+        <div>
+            <p class="mb-none mt-m">{{ trans('entities.permissions_intro') }}</p>
+            <div>
+                @include('form.checkbox', [
+                    'name' => 'restricted',
+                    'label' => trans('entities.permissions_enable'),
+                ])
+            </div>
+        </div>
+        <div>
+            <div class="form-group">
+                <label for="owner">{{ trans('entities.permissions_owner') }}</label>
+                @include('components.user-select', ['user' => $model->ownedBy, 'name' => 'owned_by'])
+            </div>
+        </div>
     </div>
 
+    <hr>
+
     <table permissions-table class="table permissions-table toggle-switch-list" style="{{ !$model->restricted ? 'display: none' : '' }}">
         <tr>
             <th>{{ trans('common.role') }}</th>
index 5acd11af491c4c61ecb434cb08966378ddbd2b97..f580b06cf7cd1caeee8a0bb624e5ba20a8411d10 100644 (file)
@@ -8,7 +8,7 @@
 
 @section('content')
 
-    <div class="flex-fill flex">
+    <div class="flex-fill flex fill-height">
         <form action="{{ $page->getUrl() }}" autocomplete="off" data-page-id="{{ $page->id }}" method="POST" class="flex flex-fill">
             {{ csrf_field() }}
 
index 6c1437ec1ac37b81da7e2b78ddeeceee4aad8a22..a9d1f1174bbbabf9f3266f09a6e59a4694beed33 100644 (file)
@@ -1,10 +1,8 @@
 <div id="markdown-editor" component="markdown-editor"
      option:markdown-editor:page-id="{{ $model->id ?? 0 }}"
      option:markdown-editor:text-direction="{{ config('app.rtl') ? 'rtl' : 'ltr' }}"
+     option:markdown-editor:image-upload-error-text="{{ trans('errors.image_upload_error') }}"
      class="flex-fill flex code-fill">
-    @exposeTranslations([
-        'errors.image_upload_error',
-    ])
 
     <div class="markdown-editor-wrap active">
         <div class="editor-toolbar">
index 48c88434e00bafd25cff7befb7e39e95f0f475e1..13125464a7114396012f3685fa56e38ae23b81b9 100644 (file)
@@ -49,7 +49,7 @@
                 <div class="sidebar-page-nav menu">
                     @foreach($pageNav as $navItem)
                         <li class="page-nav-item h{{ $navItem['level'] }}">
-                            <a href="{{ $navItem['link'] }}" class="limit-text block">{{ $navItem['text'] }}</a>
+                            <a href="{{ $navItem['link'] }}" class="text-limit-lines-1 block">{{ $navItem['text'] }}</a>
                             <div class="primary-background sidebar-page-nav-bullet"></div>
                         </li>
                     @endforeach
index 3bc85caa9d57a749128fad5f3cb9de02f57035d9..d8b8b1c353c73f53c10b639635345fe78c86ee14 100644 (file)
@@ -1,12 +1,9 @@
 <div component="wysiwyg-editor"
      option:wysiwyg-editor:page-id="{{ $model->id ?? 0 }}"
      option:wysiwyg-editor:text-direction="{{ config('app.rtl') ? 'rtl' : 'ltr' }}"
+     option:wysiwyg-editor:image-upload-error-text="{{ trans('errors.image_upload_error') }}"
      class="flex-fill flex">
 
-    @exposeTranslations([
-        'errors.image_upload_error',
-    ])
-
     <textarea id="html-editor"  name="html" rows="5"
           @if($errors->has('html')) class="text-neg" @endif>@if(isset($model) || old('html')){{ old('html') ? old('html') : $model->html }}@endif</textarea>
 </div>
index 4fd8dde1b58550fbf1f0f6c59c84ead73f88c11e..eebfb591a4af40713816963b5c978d5a6b3ba171 100644 (file)
 
     {{ $activity->getText() }}
 
-    @if($activity->entity)
+    @if($activity->entity && is_null($activity->entity->deleted_at))
         <a href="{{ $activity->entity->getUrl() }}">{{ $activity->entity->name }}</a>
     @endif
 
+    @if($activity->entity && !is_null($activity->entity->deleted_at))
+        "{{ $activity->entity->name }}"
+    @endif
+
     @if($activity->extra) "{{ $activity->extra }}" @endif
 
     <br>
index 5131af1aa36ae5728f12cd6c52e429e53aeaadd2..15b5832897d01756c7fc59b22a89805bdf7a8bbe 100644 (file)
         @endif
 
         @foreach($sidebarTree as $bookChild)
-            <li class="list-item-{{ $bookChild->getClassName() }} {{ $bookChild->getClassName() }} {{ $bookChild->isA('page') && $bookChild->draft ? 'draft' : '' }}">
+            <li class="list-item-{{ $bookChild->getType() }} {{ $bookChild->getType() }} {{ $bookChild->isA('page') && $bookChild->draft ? 'draft' : '' }}">
                 @include('partials.entity-list-item-basic', ['entity' => $bookChild, 'classes' => $current->matches($bookChild)? 'selected' : ''])
 
-                @if($bookChild->isA('chapter') && count($bookChild->pages) > 0)
+                @if($bookChild->isA('chapter') && count($bookChild->visible_pages) > 0)
                     <div class="entity-list-item no-hover">
                         <span role="presentation" class="icon text-chapter"></span>
                         <div class="content">
index a1a33ae1c7764e932c10912ccfb70af3d6c276d4..2a559aa7d5f7f28b39b253698da83c4ab3664051 100644 (file)
@@ -1,14 +1,23 @@
-<div class="breadcrumb-listing" component="dropdown" breadcrumb-listing="{{ $entity->getType() }}:{{ $entity->id }}">
-    <div class="breadcrumb-listing-toggle" refs="dropdown@toggle"
+<div class="dropdown-search" components="dropdown dropdown-search"
+     option:dropdown-search:url="/search/entity/siblings?entity_type={{$entity->getType()}}&entity_id={{ $entity->id }}"
+     option:dropdown-search:local-search-selector=".entity-list-item"
+>
+    <div class="dropdown-search-toggle" refs="dropdown@toggle"
          aria-haspopup="true" aria-expanded="false" tabindex="0">
         <div class="separator">@icon('chevron-right')</div>
     </div>
-    <div refs="dropdown@menu" class="breadcrumb-listing-dropdown card" role="menu">
-        <div class="breadcrumb-listing-search">
+    <div refs="dropdown@menu" class="dropdown-search-dropdown card" role="menu">
+        <div class="dropdown-search-search">
             @icon('search')
-            <input autocomplete="off" type="text" name="entity-search" placeholder="{{ trans('common.search') }}" aria-label="{{ trans('common.search') }}">
+            <input refs="dropdown-search@searchInput"
+                   aria-label="{{ trans('common.search') }}"
+                   autocomplete="off"
+                   placeholder="{{ trans('common.search') }}"
+                   type="text">
         </div>
-        @include('partials.loading-icon')
-        <div class="breadcrumb-listing-entity-list px-m"></div>
+        <div refs="dropdown-search@loading">
+            @include('partials.loading-icon')
+        </div>
+        <div refs="dropdown-search@listContainer" class="dropdown-search-list px-m"></div>
     </div>
 </div>
\ No newline at end of file
index 58ccd51257e0338686e746762781d36b318b5d70..065aa842026e91ca481dae18a5a606a5bcfe341d 100644 (file)
@@ -2,7 +2,7 @@
     <?php $breadcrumbCount = 0; ?>
 
     {{-- Show top level books item --}}
-    @if (count($crumbs) > 0 && ($crumbs[0] ?? null) instanceof  \BookStack\Entities\Book)
+    @if (count($crumbs) > 0 && ($crumbs[0] ?? null) instanceof  \BookStack\Entities\Models\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 && ($crumbs[0] ?? null) instanceof  \BookStack\Entities\Bookshelf)
+    @if (count($crumbs) > 0 && ($crumbs[0] ?? null) instanceof  \BookStack\Entities\Models\Bookshelf)
         <a href="{{  url('/shelves')  }}" class="text-bookshelf icon-list-item outline-hover">
             <span>@icon('bookshelf')</span>
             <span>{{ trans('entities.shelves') }}</span>
@@ -20,7 +20,7 @@
     @endif
 
     @foreach($crumbs as $key => $crumb)
-        <?php $isEntity = ($crumb instanceof \BookStack\Entities\Entity); ?>
+        <?php $isEntity = ($crumb instanceof \BookStack\Entities\Models\Entity); ?>
 
         @if (is_null($crumb))
             <?php continue; ?>
diff --git a/resources/views/partials/entity-display-item.blade.php b/resources/views/partials/entity-display-item.blade.php
new file mode 100644 (file)
index 0000000..d6633ed
--- /dev/null
@@ -0,0 +1,7 @@
+<?php $type = $entity->getType(); ?>
+<div class="{{$type}} {{$type === 'page' && $entity->draft ? 'draft' : ''}} {{$classes ?? ''}} entity-list-item no-hover">
+    <span role="presentation" class="icon text-{{$type}} {{$type === 'page' && $entity->draft ? 'draft' : ''}}">@icon($type)</span>
+    <div class="content">
+        <div class="entity-list-item-name break-text">{{ $entity->name }}</div>
+    </div>
+</div>
\ No newline at end of file
diff --git a/resources/views/partials/entity-grid-item.blade.php b/resources/views/partials/entity-grid-item.blade.php
new file mode 100644 (file)
index 0000000..ee31b53
--- /dev/null
@@ -0,0 +1,16 @@
+<a href="{{ $entity->getUrl() }}" class="grid-card"
+   data-entity-type="{{ $entity->getType() }}" data-entity-id="{{ $entity->id }}">
+    <div class="bg-{{ $entity->getType() }} featured-image-container-wrap">
+        <div class="featured-image-container" @if($entity->cover) style="background-image: url('{{ $entity->getBookCover() }}')"@endif>
+        </div>
+        @icon($entity->getType())
+    </div>
+    <div class="grid-card-content">
+        <h2 class="text-limit-lines-2">{{ $entity->name }}</h2>
+        <p class="text-muted">{{ $entity->getExcerpt(130) }}</p>
+    </div>
+    <div class="grid-card-footer text-muted ">
+        <p>@icon('star')<span title="{{ $entity->created_at->toDayDateTimeString() }}">{{ trans('entities.meta_created', ['timeLength' => $entity->created_at->diffForHumans()]) }}</span></p>
+        <p>@icon('edit')<span title="{{ $entity->updated_at->toDayDateTimeString() }}">{{ trans('entities.meta_updated', ['timeLength' => $entity->updated_at->diffForHumans()]) }}</span></p>
+    </div>
+</a>
\ No newline at end of file
index f759ea25b174628de3a3af0d411f1f6a076b5dda..8996df9bb67d0f491d9a8de5e5d18b56560883fb 100644 (file)
@@ -1,34 +1,50 @@
 <div class="entity-meta">
     @if($entity->isA('revision'))
-        @icon('history'){{ trans('entities.pages_revision') }}
-        {{ trans('entities.pages_revisions_number') }}{{ $entity->revision_number == 0 ? '' : $entity->revision_number }}
-        <br>
+        <div>
+            @icon('history'){{ trans('entities.pages_revision') }}
+            {{ trans('entities.pages_revisions_number') }}{{ $entity->revision_number == 0 ? '' : $entity->revision_number }}
+        </div>
     @endif
 
     @if ($entity->isA('page'))
-        @if (userCan('page-update', $entity)) <a href="{{ $entity->getUrl('/revisions') }}"> @endif
-            @icon('history'){{ trans('entities.meta_revision', ['revisionCount' => $entity->revision_count]) }} <br>
+        <div>
+            @if (userCan('page-update', $entity)) <a href="{{ $entity->getUrl('/revisions') }}"> @endif
+            @icon('history'){{ trans('entities.meta_revision', ['revisionCount' => $entity->revision_count]) }}
             @if (userCan('page-update', $entity))</a>@endif
+        </div>
     @endif
 
+    @if ($entity->ownedBy && $entity->ownedBy->id !== $entity->createdBy->id)
+        <div>
+            @icon('user'){!! trans('entities.meta_owned_name', [
+            'user' => "<a href='{$entity->ownedBy->getProfileUrl()}'>".e($entity->ownedBy->name). "</a>"
+        ]) !!}
+        </div>
+    @endif
 
     @if ($entity->createdBy)
-        @icon('star'){!! trans('entities.meta_created_name', [
+        <div>
+            @icon('star'){!! trans('entities.meta_created_name', [
             'timeLength' => '<span title="'.$entity->created_at->toDayDateTimeString().'">'.$entity->created_at->diffForHumans() . '</span>',
-            'user' => "<a href='{$entity->createdBy->getProfileUrl()}'>".htmlentities($entity->createdBy->name). "</a>"
+            'user' => "<a href='{$entity->createdBy->getProfileUrl()}'>".e($entity->createdBy->name). "</a>"
             ]) !!}
+        </div>
     @else
-        @icon('star')<span title="{{$entity->created_at->toDayDateTimeString()}}">{{ trans('entities.meta_created', ['timeLength' => $entity->created_at->diffForHumans()]) }}</span>
+        <div>
+            @icon('star')<span title="{{$entity->created_at->toDayDateTimeString()}}">{{ trans('entities.meta_created', ['timeLength' => $entity->created_at->diffForHumans()]) }}</span>
+        </div>
     @endif
 
-    <br>
-
     @if ($entity->updatedBy)
-        @icon('edit'){!! trans('entities.meta_updated_name', [
+        <div>
+            @icon('edit'){!! trans('entities.meta_updated_name', [
                 'timeLength' => '<span title="' . $entity->updated_at->toDayDateTimeString() .'">' . $entity->updated_at->diffForHumans() .'</span>',
-                'user' => "<a href='{$entity->updatedBy->getProfileUrl()}'>".htmlentities($entity->updatedBy->name). "</a>"
+                'user' => "<a href='{$entity->updatedBy->getProfileUrl()}'>".e($entity->updatedBy->name). "</a>"
             ]) !!}
+        </div>
     @elseif (!$entity->isA('revision'))
-        @icon('edit')<span title="{{ $entity->updated_at->toDayDateTimeString() }}">{{ trans('entities.meta_updated', ['timeLength' => $entity->updated_at->diffForHumans()]) }}</span>
+        <div>
+            @icon('edit')<span title="{{ $entity->updated_at->toDayDateTimeString() }}">{{ trans('entities.meta_updated', ['timeLength' => $entity->updated_at->diffForHumans()]) }}</span>
+        </div>
     @endif
 </div>
\ No newline at end of file
diff --git a/resources/views/partials/table-user.blade.php b/resources/views/partials/table-user.blade.php
new file mode 100644 (file)
index 0000000..a8f2777
--- /dev/null
@@ -0,0 +1,12 @@
+{{--
+$user - User mode to display, Can be null.
+$user_id - Id of user to show. Must be provided.
+--}}
+@if($user)
+    <a href="{{ $user->getEditUrl() }}" class="table-user-item">
+        <div><img class="avatar block" src="{{ $user->getAvatar(40)}}" alt="{{ $user->name }}"></div>
+        <div>{{ $user->name }}</div>
+    </a>
+@else
+    [ID: {{ $user_id }}] {{ trans('common.deleted_user') }}
+@endif
\ No newline at end of file
diff --git a/resources/views/search/book.blade.php b/resources/views/search/book.blade.php
deleted file mode 100644 (file)
index 36732c2..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-<div class="page-list">
-    @if(count($pages) > 0)
-        @foreach($pages as $pageIndex => $page)
-            <div class="anim searchResult" style="animation-delay: {{$pageIndex*50 . 'ms'}};">
-                @include('pages.list-item', ['page' => $page])
-                <hr>
-            </div>
-        @endforeach
-    @else
-        <p class="text-muted">{{ trans('entities.search_no_pages') }}</p>
-    @endif
-</div>
-
-@if(count($chapters) > 0)
-    <div class="page-list">
-        @foreach($chapters as $chapterIndex => $chapter)
-            <div class="anim searchResult" style="animation-delay: {{($chapterIndex+count($pages))*50 . 'ms'}};">
-                @include('chapters.list-item', ['chapter' => $chapter, 'hidePages' => true])
-                <hr>
-            </div>
-        @endforeach
-    </div>
-@endif
-
index 9b97f060da7ca9ce70b32c10819156cff5c6e823..1996e1c2144e59119d00b0f34cbfaf39d3b44626 100644 (file)
@@ -19,8 +19,8 @@
                 <button refs="dropdown@toggle" aria-haspopup="true" aria-expanded="false" aria-label="{{ trans('common.sort_options') }}" class="input-base text-left">{{ $listDetails['event'] ?: trans('settings.audit_event_filter_no_filter') }}</button>
                 <ul refs="dropdown@menu" class="dropdown-menu">
                     <li @if($listDetails['event'] === '') class="active" @endif><a href="{{ sortUrl('/settings/audit', $listDetails, ['event' => '']) }}">{{ trans('settings.audit_event_filter_no_filter') }}</a></li>
-                    @foreach($activityKeys as $key)
-                        <li @if($key === $listDetails['event']) class="active" @endif><a href="{{ sortUrl('/settings/audit', $listDetails, ['event' => $key]) }}">{{ $key }}</a></li>
+                    @foreach($activityTypes as $type)
+                        <li @if($type === $listDetails['event']) class="active" @endif><a href="{{ sortUrl('/settings/audit', $listDetails, ['event' => $type]) }}">{{ $type }}</a></li>
                     @endforeach
                 </ul>
             </div>
                 <th>
                     <a href="{{ sortUrl('/settings/audit', $listDetails, ['sort' => 'key']) }}">{{ trans('settings.audit_table_event') }}</a>
                 </th>
-                <th>{{ trans('settings.audit_table_item') }}</th>
+                <th>{{ trans('settings.audit_table_related') }}</th>
                 <th>
                     <a href="{{ sortUrl('/settings/audit', $listDetails, ['sort' => 'created_at']) }}">{{ trans('settings.audit_table_date') }}</a></th>
             </tr>
             @foreach($activities as $activity)
                 <tr>
                     <td>
-                        @if($activity->user)
-                            <a href="{{ $activity->user->getEditUrl() }}" class="audit-log-user">
-                                <div><img class="avatar block" src="{{ $activity->user->getAvatar(40)}}" alt="{{ $activity->user->name }}"></div>
-                                <div>{{ $activity->user->name }}</div>
-                            </a>
-                        @else
-                            [ID: {{ $activity->user_id }}] {{ trans('common.deleted_user') }}
-                        @endif
+                        @include('partials.table-user', ['user' => $activity->user, 'user_id' => $activity->user_id])
                     </td>
-                    <td>{{ $activity->key }}</td>
+                    <td>{{ $activity->type }}</td>
                     <td>
                         @if($activity->entity)
-                            <a href="{{ $activity->entity->getUrl() }}" class="icon-list-item">
+                            <a href="{{ $activity->entity->getUrl() }}" class="table-entity-item">
                                 <span role="presentation" class="icon text-{{$activity->entity->getType()}}">@icon($activity->entity->getType())</span>
                                 <div class="text-{{ $activity->entity->getType() }}">
                                     {{ $activity->entity->name }}
                                 </div>
                             </a>
-                        @elseif($activity->extra)
+                        @elseif($activity->detail && $activity->isForEntity())
                             <div class="px-m">
                                 {{ trans('settings.audit_deleted_item') }} <br>
-                                {{ trans('settings.audit_deleted_item_name', ['name' => $activity->extra]) }}
+                                {{ trans('settings.audit_deleted_item_name', ['name' => $activity->detail]) }}
                             </div>
+                        @elseif($activity->detail)
+                            <div class="px-m">{{ $activity->detail }}</div>
                         @endif
                     </td>
                     <td>{{ $activity->created_at }}</td>
index 35686ca3307e0f27a268987cc2fbad708d411071..941a258d84942e32a1c004e4b0b7013f954984f0 100644 (file)
@@ -5,6 +5,24 @@
 
     @include('settings.navbar-with-version', ['selected' => 'maintenance'])
 
+    <div class="card content-wrap auto-height pb-xl">
+        <h2 class="list-heading">{{ trans('settings.recycle_bin') }}</h2>
+        <div class="grid half gap-xl">
+            <div>
+                <p class="small text-muted">{{ trans('settings.maint_recycle_bin_desc') }}</p>
+            </div>
+            <div>
+                <div class="grid half no-gap mb-m">
+                    <p class="mb-xs text-bookshelf">@icon('bookshelf'){{ trans('entities.shelves') }}: {{ $recycleStats['bookshelf'] }}</p>
+                    <p class="mb-xs text-book">@icon('book'){{ trans('entities.books') }}: {{ $recycleStats['book'] }}</p>
+                    <p class="mb-xs text-chapter">@icon('chapter'){{ trans('entities.chapters') }}: {{ $recycleStats['chapter'] }}</p>
+                    <p class="mb-xs text-page">@icon('page'){{ trans('entities.pages') }}: {{ $recycleStats['page'] }}</p>
+                </div>
+                <a href="{{ url('/settings/recycle-bin') }}" class="button outline">{{ trans('settings.maint_recycle_bin_open') }}</a>
+            </div>
+        </div>
+    </div>
+
     <div id="image-cleanup" class="card content-wrap auto-height">
         <h2 class="list-heading">{{ trans('settings.maint_image_cleanup') }}</h2>
         <div class="grid half gap-xl">
@@ -15,7 +33,7 @@
                 <form method="POST" action="{{ url('/settings/maintenance/cleanup-images') }}">
                     {!! csrf_field()  !!}
                     <input type="hidden" name="_method" value="DELETE">
-                    <div>
+                    <div class="mb-s">
                         @if(session()->has('cleanup-images-warning'))
                             <p class="text-neg">
                                 {{ session()->get('cleanup-images-warning') }}
@@ -23,9 +41,9 @@
                             <input type="hidden" name="ignore_revisions" value="{{ session()->getOldInput('ignore_revisions', 'false') }}">
                             <input type="hidden" name="confirm" value="true">
                         @else
-                            <label>
-                                <input type="checkbox" name="ignore_revisions" value="true">
-                                {{ trans('settings.maint_image_cleanup_ignore_revisions') }}
+                            <label class="flex-container-row">
+                                <div class="mr-s"><input type="checkbox" name="ignore_revisions" value="true"></div>
+                                <div>{{ trans('settings.maint_delete_images_only_in_revisions') }}</div>
                             </label>
                         @endif
                     </div>
diff --git a/resources/views/settings/recycle-bin/deletable-entity-list.blade.php b/resources/views/settings/recycle-bin/deletable-entity-list.blade.php
new file mode 100644 (file)
index 0000000..07ad94f
--- /dev/null
@@ -0,0 +1,11 @@
+@include('partials.entity-display-item', ['entity' => $entity])
+@if($entity->isA('book'))
+    @foreach($entity->chapters()->withTrashed()->get() as $chapter)
+        @include('partials.entity-display-item', ['entity' => $chapter])
+    @endforeach
+@endif
+@if($entity->isA('book') || $entity->isA('chapter'))
+    @foreach($entity->pages()->withTrashed()->get() as $page)
+        @include('partials.entity-display-item', ['entity' => $page])
+    @endforeach
+@endif
\ No newline at end of file
diff --git a/resources/views/settings/recycle-bin/destroy.blade.php b/resources/views/settings/recycle-bin/destroy.blade.php
new file mode 100644 (file)
index 0000000..d027199
--- /dev/null
@@ -0,0 +1,31 @@
+@extends('simple-layout')
+
+@section('body')
+    <div class="container small">
+
+        <div class="grid left-focus v-center no-row-gap">
+            <div class="py-m">
+                @include('settings.navbar', ['selected' => 'maintenance'])
+            </div>
+        </div>
+
+        <div class="card content-wrap auto-height">
+            <h2 class="list-heading">{{ trans('settings.recycle_bin_permanently_delete') }}</h2>
+            <p class="text-muted">{{ trans('settings.recycle_bin_destroy_confirm') }}</p>
+            <form action="{{ url('/settings/recycle-bin/' . $deletion->id) }}" method="post">
+                {!! method_field('DELETE') !!}
+                {!! csrf_field() !!}
+                <a href="{{ url('/settings/recycle-bin') }}" class="button outline">{{ trans('common.cancel') }}</a>
+                <button type="submit" class="button">{{ trans('common.delete_confirm') }}</button>
+            </form>
+
+            @if($deletion->deletable instanceof \BookStack\Entities\Models\Entity)
+                <hr class="mt-m">
+                <h5>{{ trans('settings.recycle_bin_destroy_list') }}</h5>
+                @include('settings.recycle-bin.deletable-entity-list', ['entity' => $deletion->deletable])
+            @endif
+
+        </div>
+
+    </div>
+@stop
diff --git a/resources/views/settings/recycle-bin/index.blade.php b/resources/views/settings/recycle-bin/index.blade.php
new file mode 100644 (file)
index 0000000..657b45a
--- /dev/null
@@ -0,0 +1,103 @@
+@extends('simple-layout')
+
+@section('body')
+    <div class="container">
+
+        <div class="grid left-focus v-center no-row-gap">
+            <div class="py-m">
+                @include('settings.navbar', ['selected' => 'maintenance'])
+            </div>
+        </div>
+
+        <div class="card content-wrap auto-height">
+            <h2 class="list-heading">{{ trans('settings.recycle_bin') }}</h2>
+
+            <div class="grid half left-focus">
+                <div>
+                    <p class="text-muted">{{ trans('settings.recycle_bin_desc') }}</p>
+                </div>
+                <div class="text-right">
+                    <div component="dropdown" class="dropdown-container">
+                        <button refs="dropdown@toggle"
+                                type="button"
+                                class="button outline">{{ trans('settings.recycle_bin_empty') }} </button>
+                        <div refs="dropdown@menu" class="dropdown-menu">
+                            <p class="text-neg small px-m mb-xs">{{ trans('settings.recycle_bin_empty_confirm') }}</p>
+
+                            <form action="{{ url('/settings/recycle-bin/empty') }}" method="POST">
+                                {!! csrf_field() !!}
+                                <button type="submit" class="text-primary small delete">{{ trans('common.confirm') }}</button>
+                            </form>
+                        </div>
+                    </div>
+
+                </div>
+            </div>
+
+
+            <hr class="mt-l mb-s">
+
+            {!! $deletions->links() !!}
+
+            <table class="table">
+                <tr>
+                    <th>{{ trans('settings.recycle_bin_deleted_item') }}</th>
+                    <th>{{ trans('settings.recycle_bin_deleted_by') }}</th>
+                    <th>{{ trans('settings.recycle_bin_deleted_at') }}</th>
+                    <th></th>
+                </tr>
+                @if(count($deletions) === 0)
+                    <tr>
+                        <td colspan="4">
+                            <p class="text-muted"><em>{{ trans('settings.recycle_bin_contents_empty') }}</em></p>
+                        </td>
+                    </tr>
+                @endif
+                @foreach($deletions as $deletion)
+                <tr>
+                    <td>
+                        <div class="table-entity-item">
+                            <span role="presentation" class="icon text-{{$deletion->deletable->getType()}}">@icon($deletion->deletable->getType())</span>
+                            <div class="text-{{ $deletion->deletable->getType() }}">
+                                {{ $deletion->deletable->name }}
+                            </div>
+                        </div>
+                        @if($deletion->deletable instanceof \BookStack\Entities\Models\Book || $deletion->deletable instanceof \BookStack\Entities\Models\Chapter)
+                            <div class="mb-m"></div>
+                        @endif
+                        @if($deletion->deletable instanceof \BookStack\Entities\Models\Book)
+                            <div class="pl-xl block inline">
+                                <div class="text-chapter">
+                                    @icon('chapter') {{ trans_choice('entities.x_chapters', $deletion->deletable->chapters()->withTrashed()->count()) }}
+                                </div>
+                            </div>
+                        @endif
+                        @if($deletion->deletable instanceof \BookStack\Entities\Models\Book || $deletion->deletable instanceof \BookStack\Entities\Models\Chapter)
+                        <div class="pl-xl block inline">
+                            <div class="text-page">
+                                @icon('page') {{ trans_choice('entities.x_pages', $deletion->deletable->pages()->withTrashed()->count()) }}
+                            </div>
+                        </div>
+                        @endif
+                    </td>
+                    <td>@include('partials.table-user', ['user' => $deletion->deleter, 'user_id' => $deletion->deleted_by])</td>
+                    <td width="200">{{ $deletion->created_at }}</td>
+                    <td width="150" class="text-right">
+                        <div component="dropdown" class="dropdown-container">
+                            <button type="button" refs="dropdown@toggle" class="button outline">{{ trans('common.actions') }}</button>
+                            <ul refs="dropdown@menu" class="dropdown-menu">
+                                <li><a class="block" href="{{ url('/settings/recycle-bin/'.$deletion->id.'/restore') }}">{{ trans('settings.recycle_bin_restore') }}</a></li>
+                                <li><a class="block" href="{{ url('/settings/recycle-bin/'.$deletion->id.'/destroy') }}">{{ trans('settings.recycle_bin_permanently_delete') }}</a></li>
+                            </ul>
+                        </div>
+                    </td>
+                </tr>
+                @endforeach
+            </table>
+
+            {!! $deletions->links() !!}
+
+        </div>
+
+    </div>
+@stop
diff --git a/resources/views/settings/recycle-bin/restore.blade.php b/resources/views/settings/recycle-bin/restore.blade.php
new file mode 100644 (file)
index 0000000..62a31e5
--- /dev/null
@@ -0,0 +1,33 @@
+@extends('simple-layout')
+
+@section('body')
+    <div class="container small">
+
+        <div class="grid left-focus v-center no-row-gap">
+            <div class="py-m">
+                @include('settings.navbar', ['selected' => 'maintenance'])
+            </div>
+        </div>
+
+        <div class="card content-wrap auto-height">
+            <h2 class="list-heading">{{ trans('settings.recycle_bin_restore') }}</h2>
+            <p class="text-muted">{{ trans('settings.recycle_bin_restore_confirm') }}</p>
+            <form action="{{ url('/settings/recycle-bin/' . $deletion->id . '/restore') }}" method="post">
+                {!! csrf_field() !!}
+                <a href="{{ url('/settings/recycle-bin') }}" class="button outline">{{ trans('common.cancel') }}</a>
+                <button type="submit" class="button">{{ trans('settings.recycle_bin_restore') }}</button>
+            </form>
+
+            @if($deletion->deletable instanceof \BookStack\Entities\Models\Entity)
+                <hr class="mt-m">
+                <h5>{{ trans('settings.recycle_bin_restore_list') }}</h5>
+                @if($deletion->deletable->getParent() && $deletion->deletable->getParent()->trashed())
+                    <p class="text-neg">{{ trans('settings.recycle_bin_restore_deleted_parent') }}</p>
+                @endif
+                @include('settings.recycle-bin.deletable-entity-list', ['entity' => $deletion->deletable])
+            @endif
+
+        </div>
+
+    </div>
+@stop
diff --git a/resources/views/shelves/grid-item.blade.php b/resources/views/shelves/grid-item.blade.php
deleted file mode 100644 (file)
index 25b35b9..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-<a href="{{$shelf->getUrl()}}" class="bookshelf-grid-item grid-card"
-   data-entity-type="bookshelf" data-entity-id="{{$shelf->id}}">
-    <div class="bg-shelf featured-image-container-wrap">
-        <div class="featured-image-container" @if($shelf->cover) style="background-image: url('{{ $shelf->getBookCover() }}')"@endif>
-        </div>
-        @icon('bookshelf')
-    </div>
-    <div class="grid-card-content">
-        <h2>{{$shelf->getShortName(35)}}</h2>
-        @if(isset($shelf->searchSnippet))
-            <p class="text-muted">{!! $shelf->searchSnippet !!}</p>
-        @else
-            <p class="text-muted">{{ $shelf->getExcerpt(130) }}</p>
-        @endif
-    </div>
-    <div class="grid-card-footer text-muted text-small">
-        @icon('star')<span title="{{$shelf->created_at->toDayDateTimeString()}}">{{ trans('entities.meta_created', ['timeLength' => $shelf->created_at->diffForHumans()]) }}</span>
-        <br>
-        @icon('edit')<span title="{{ $shelf->updated_at->toDayDateTimeString() }}">{{ trans('entities.meta_updated', ['timeLength' => $shelf->updated_at->diffForHumans()]) }}</span>
-    </div>
-</a>
\ No newline at end of file
index 6e5ed29a5fa41ef09156e58456b842e77b4039de..00cacfa707c3c28f927695e7cf7fc71c3f629ba1 100644 (file)
@@ -1,5 +1,5 @@
 <a href="{{ $shelf->getUrl() }}" class="shelf entity-list-item" data-entity-type="bookshelf" data-entity-id="{{$shelf->id}}">
-    <div class="entity-list-item-image bg-shelf @if($shelf->image_id) has-image @endif" style="background-image: url('{{ $shelf->getBookCover() }}')">
+    <div class="entity-list-item-image bg-bookshelf @if($shelf->image_id) has-image @endif" style="background-image: url('{{ $shelf->getBookCover() }}')">
         @icon('bookshelf')
     </div>
     <div class="content py-xs">
index b20b08a2c59e40f8a110394404f63ac198c91f8f..3600a8c795f70303dc464eb73b6fe503ec1bef90 100644 (file)
@@ -21,7 +21,7 @@
         @else
             <div class="grid third">
                 @foreach($shelves as $key => $shelf)
-                    @include('shelves.grid-item', ['shelf' => $shelf])
+                    @include('partials.entity-grid-item', ['entity' => $shelf])
                 @endforeach
             </div>
         @endif
index 6fee6f45d522718ac0829dd7b9cf33e4a9b2874d..46432c1b92594372b196fdb35fec9951833fffa9 100644 (file)
@@ -22,7 +22,7 @@
                 @else
                     <div class="grid third">
                         @foreach($shelf->visibleBooks as $key => $book)
-                            @include('books.grid-item', ['book' => $book])
+                            @include('partials.entity-grid-item', ['entity' => $book])
                         @endforeach
                     </div>
                 @endif
index d3349c2f3fc29b93e6b10319e95d0cefc97df7e5..aba6f5cc1f6de6cee8da0beec9b64a51fc563eab 100644 (file)
 
             <p>{{ trans('settings.users_delete_warning', ['userName' => $user->name]) }}</p>
 
+            <hr class="my-l">
+
+            <div class="grid half gap-xl v-center">
+                <div>
+                    <label class="setting-list-label">{{ trans('settings.users_migrate_ownership') }}</label>
+                    <p class="small">{{ trans('settings.users_migrate_ownership_desc') }}</p>
+                </div>
+                <div>
+                    @include('components.user-select', ['name' => 'new_owner_id', 'user' => null])
+                </div>
+            </div>
+
+            <hr class="my-l">
+
             <div class="grid half">
                 <p class="text-neg"><strong>{{ trans('settings.users_delete_confirm') }}</strong></p>
                 <div>
index da373c1618b563fddcc9768644b7d1ac608e29cf..4b5bad0fd5bb3db369d47dfe2250ed00f5e40c58 100644 (file)
@@ -27,7 +27,6 @@
                 </div>
             </div>
 
-            {{--TODO - Add last login--}}
             <table class="table">
                 <tr>
                     <th></th>
@@ -37,6 +36,9 @@
                         <a href="{{ sortUrl('/settings/users', $listDetails, ['sort' => 'email']) }}">{{ trans('auth.email') }}</a>
                     </th>
                     <th>{{ trans('settings.role_user_roles') }}</th>
+                    <th class="text-right">
+                        <a href="{{ sortUrl('/settings/users', $listDetails, ['sort' => 'latest_activity']) }}">{{ trans('settings.users_latest_activity') }}</a>
+                    </th>
                 </tr>
                 @foreach($users as $user)
                     <tr>
                                 <small><a href="{{ url("/settings/roles/{$role->id}") }}">{{$role->display_name}}</a>@if($index !== count($user->roles) -1),@endif</small>
                             @endforeach
                         </td>
+                        <td class="text-right text-muted">
+                            @if($user->latestActivity)
+                                <small title="{{ $user->latestActivity->created_at->format('Y-m-d H:i:s') }}">{{ $user->latestActivity->created_at->diffForHumans() }}</small>
+                            @endif
+                        </td>
                     </tr>
                 @endforeach
             </table>
index 1b90d9b8fd12d591cf87b6e34fba2191b6711ff9..44643d6d4cc7182a436a7f5ed0d6605d83df637f 100644 (file)
@@ -29,6 +29,16 @@ Route::get('chapters/{id}/export/html', 'ChapterExportApiController@exportHtml')
 Route::get('chapters/{id}/export/pdf', 'ChapterExportApiController@exportPdf');
 Route::get('chapters/{id}/export/plaintext', 'ChapterExportApiController@exportPlainText');
 
+Route::get('pages', 'PageApiController@list');
+Route::post('pages', 'PageApiController@create');
+Route::get('pages/{id}', 'PageApiController@read');
+Route::put('pages/{id}', 'PageApiController@update');
+Route::delete('pages/{id}', 'PageApiController@delete');
+
+Route::get('pages/{id}/export/html', 'PageExportApiController@exportHtml');
+Route::get('pages/{id}/export/pdf', 'PageExportApiController@exportPdf');
+Route::get('pages/{id}/export/plaintext', 'PageExportApiController@exportPlainText');
+
 Route::get('shelves', 'BookshelfApiController@list');
 Route::post('shelves', 'BookshelfApiController@create');
 Route::get('shelves/{id}', 'BookshelfApiController@read');
index acbcb4e8fd4eb8f8e78002adc51584ec79306f02..e8f217862281a3622b94bede6593e77d975e318c 100644 (file)
@@ -148,6 +148,9 @@ Route::group(['middleware' => 'auth'], function () {
     Route::get('/search/chapter/{bookId}', 'SearchController@searchChapter');
     Route::get('/search/entity/siblings', 'SearchController@searchSiblings');
 
+    // User Search
+    Route::get('/search/users/select', 'UserSearchController@forSelect');
+
     Route::get('/templates', 'PageTemplateController@list');
     Route::get('/templates/{templateId}', 'PageTemplateController@get');
 
@@ -166,6 +169,14 @@ Route::group(['middleware' => 'auth'], function () {
         Route::delete('/maintenance/cleanup-images', 'MaintenanceController@cleanupImages');
         Route::post('/maintenance/send-test-email', 'MaintenanceController@sendTestEmail');
 
+        // Recycle Bin
+        Route::get('/recycle-bin', 'RecycleBinController@index');
+        Route::post('/recycle-bin/empty', 'RecycleBinController@empty');
+        Route::get('/recycle-bin/{id}/destroy', 'RecycleBinController@showDestroy');
+        Route::delete('/recycle-bin/{id}', 'RecycleBinController@destroy');
+        Route::get('/recycle-bin/{id}/restore', 'RecycleBinController@showRestore');
+        Route::post('/recycle-bin/{id}/restore', 'RecycleBinController@restore');
+
         // Audit Log
         Route::get('/audit', 'AuditLogController@index');
 
@@ -193,13 +204,13 @@ Route::group(['middleware' => 'auth'], function () {
         Route::delete('/users/{userId}/api-tokens/{tokenId}', 'UserApiTokenController@destroy');
 
         // Roles
-        Route::get('/roles', 'PermissionController@listRoles');
-        Route::get('/roles/new', 'PermissionController@createRole');
-        Route::post('/roles/new', 'PermissionController@storeRole');
-        Route::get('/roles/delete/{id}', 'PermissionController@showDeleteRole');
-        Route::delete('/roles/delete/{id}', 'PermissionController@deleteRole');
-        Route::get('/roles/{id}', 'PermissionController@editRole');
-        Route::put('/roles/{id}', 'PermissionController@updateRole');
+        Route::get('/roles', 'RoleController@list');
+        Route::get('/roles/new', 'RoleController@create');
+        Route::post('/roles/new', 'RoleController@store');
+        Route::get('/roles/delete/{id}', 'RoleController@showDelete');
+        Route::delete('/roles/delete/{id}', 'RoleController@delete');
+        Route::get('/roles/{id}', 'RoleController@edit');
+        Route::put('/roles/{id}', 'RoleController@update');
     });
 
 });
index f47bc44a30d7b01d419178dc0e9f83cc414887b6..9c3fe273c1cbf034ab8a7246f943cbabbe0d1e2c 100644 (file)
@@ -1,7 +1,7 @@
 <?php namespace Tests;
 
 
-use BookStack\Entities\Book;
+use BookStack\Entities\Models\Book;
 
 class ActivityTrackingTest extends BrowserKitTest
 {
index bb4920cc3667d35d5c52ec3df904515a817cd602..c3d9bc10888d51e91bc1a584cadabf6ff27b3642 100644 (file)
@@ -1,6 +1,6 @@
 <?php namespace Tests\Api;
 
-use BookStack\Entities\Book;
+use BookStack\Entities\Models\Book;
 use Tests\TestCase;
 
 class ApiListingTest extends TestCase
index 3fd763ec625969872d9fa5a5f27f27e6002ad80e..de4db1469acade17af9d9f1c1f9481bf4be81ff3 100644 (file)
@@ -1,6 +1,6 @@
 <?php namespace Tests\Api;
 
-use BookStack\Entities\Book;
+use BookStack\Entities\Models\Book;
 use Tests\TestCase;
 
 class BooksApiTest extends TestCase
index 15a44459ee3b4750a9e59e615224574fc7866299..422631c3af9e9bb674da98333d74a94a0c63bca2 100644 (file)
@@ -1,7 +1,7 @@
 <?php namespace Tests\Api;
 
-use BookStack\Entities\Book;
-use BookStack\Entities\Chapter;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Chapter;
 use Tests\TestCase;
 
 class ChaptersApiTest extends TestCase
diff --git a/tests/Api/PagesApiTest.php b/tests/Api/PagesApiTest.php
new file mode 100644 (file)
index 0000000..44fbf5e
--- /dev/null
@@ -0,0 +1,258 @@
+<?php namespace Tests\Api;
+
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Models\Page;
+use Tests\TestCase;
+
+class PagesApiTest extends TestCase
+{
+    use TestsApi;
+
+    protected $baseEndpoint = '/api/pages';
+
+    public function test_index_endpoint_returns_expected_page()
+    {
+        $this->actingAsApiEditor();
+        $firstPage = Page::query()->orderBy('id', 'asc')->first();
+
+        $resp = $this->getJson($this->baseEndpoint . '?count=1&sort=+id');
+        $resp->assertJson(['data' => [
+            [
+                'id' => $firstPage->id,
+                'name' => $firstPage->name,
+                'slug' => $firstPage->slug,
+                'book_id' => $firstPage->book->id,
+                'priority' => $firstPage->priority,
+            ]
+        ]]);
+    }
+
+    public function test_create_endpoint()
+    {
+        $this->actingAsApiEditor();
+        $book = Book::query()->first();
+        $details = [
+            'name' => 'My API page',
+            'book_id' => $book->id,
+            'html' => '<p>My new page content</p>',
+            'tags' => [
+                [
+                    'name' => 'tagname',
+                    'value' => 'tagvalue',
+                ]
+            ]
+        ];
+
+        $resp = $this->postJson($this->baseEndpoint, $details);
+        unset($details['html']);
+        $resp->assertStatus(200);
+        $newItem = Page::query()->orderByDesc('id')->where('name', '=', $details['name'])->first();
+        $resp->assertJson(array_merge($details, ['id' => $newItem->id, 'slug' => $newItem->slug]));
+        $this->assertDatabaseHas('tags', [
+            'entity_id' => $newItem->id,
+            'entity_type' => $newItem->getMorphClass(),
+            'name' => 'tagname',
+            'value' => 'tagvalue',
+        ]);
+        $resp->assertSeeText('My new page content');
+        $resp->assertJsonMissing(['book' => []]);
+        $this->assertActivityExists('page_create', $newItem);
+    }
+
+    public function test_page_name_needed_to_create()
+    {
+        $this->actingAsApiEditor();
+        $book = Book::query()->first();
+        $details = [
+            'book_id' => $book->id,
+            'html' => '<p>A page created via the API</p>',
+        ];
+
+        $resp = $this->postJson($this->baseEndpoint, $details);
+        $resp->assertStatus(422);
+        $resp->assertJson($this->validationResponse([
+            "name" => ["The name field is required."]
+        ]));
+    }
+
+    public function test_book_id_or_chapter_id_needed_to_create()
+    {
+        $this->actingAsApiEditor();
+        $details = [
+            'name' => 'My api page',
+            'html' => '<p>A page created via the API</p>',
+        ];
+
+        $resp = $this->postJson($this->baseEndpoint, $details);
+        $resp->assertStatus(422);
+        $resp->assertJson($this->validationResponse([
+            "book_id" => ["The book id field is required when chapter id is not present."],
+            "chapter_id" => ["The chapter id field is required when book id is not present."]
+        ]));
+
+        $chapter = Chapter::visible()->first();
+        $resp = $this->postJson($this->baseEndpoint, array_merge($details, ['chapter_id' => $chapter->id]));
+        $resp->assertStatus(200);
+
+        $book = Book::visible()->first();
+        $resp = $this->postJson($this->baseEndpoint, array_merge($details, ['book_id' => $book->id]));
+        $resp->assertStatus(200);
+    }
+
+    public function test_markdown_can_be_provided_for_create()
+    {
+        $this->actingAsApiEditor();
+        $book = Book::visible()->first();
+        $details = [
+            'book_id' => $book->id,
+            'name' => 'My api page',
+            'markdown' => "# A new API page \n[link](https://example.com)",
+        ];
+
+        $resp = $this->postJson($this->baseEndpoint, $details);
+        $resp->assertJson(['markdown' => $details['markdown']]);
+
+        $respHtml = $resp->json('html');
+        $this->assertStringContainsString('new API page</h1>', $respHtml);
+        $this->assertStringContainsString('link</a>', $respHtml);
+        $this->assertStringContainsString('href="https://example.com"', $respHtml);
+    }
+
+    public function test_read_endpoint()
+    {
+        $this->actingAsApiEditor();
+        $page = Page::visible()->first();
+
+        $resp = $this->getJson($this->baseEndpoint . "/{$page->id}");
+        $resp->assertStatus(200);
+        $resp->assertJson([
+            'id' => $page->id,
+            'slug' => $page->slug,
+            'created_by' => [
+                'name' => $page->createdBy->name,
+            ],
+            'book_id' => $page->book_id,
+            'updated_by' => [
+                'name' => $page->createdBy->name,
+            ],
+        ]);
+    }
+
+    public function test_read_endpoint_provides_rendered_html()
+    {
+        $this->actingAsApiEditor();
+        $page = Page::visible()->first();
+        $page->html = "<p>testing</p><script>alert('danger')</script><h1>Hello</h1>";
+        $page->save();
+
+        $resp = $this->getJson($this->baseEndpoint . "/{$page->id}");
+        $html = $resp->json('html');
+        $this->assertStringNotContainsString('script', $html);
+        $this->assertStringContainsString('Hello', $html);
+        $this->assertStringContainsString('testing', $html);
+    }
+
+    public function test_update_endpoint()
+    {
+        $this->actingAsApiEditor();
+        $page = Page::visible()->first();
+        $details = [
+            'name' => 'My updated API page',
+            'html' => '<p>A page created via the API</p>',
+            'tags' => [
+                [
+                    'name' => 'freshtag',
+                    'value' => 'freshtagval',
+                ]
+            ],
+        ];
+
+        $resp = $this->putJson($this->baseEndpoint . "/{$page->id}", $details);
+        $page->refresh();
+
+        $resp->assertStatus(200);
+        unset($details['html']);
+        $resp->assertJson(array_merge($details, [
+            'id' => $page->id, 'slug' => $page->slug, 'book_id' => $page->book_id
+        ]));
+        $this->assertActivityExists('page_update', $page);
+    }
+
+    public function test_providing_new_chapter_id_on_update_will_move_page()
+    {
+        $this->actingAsApiEditor();
+        $page = Page::visible()->first();
+        $chapter = Chapter::visible()->where('book_id', '!=', $page->book_id)->first();
+        $details = [
+            'name' => 'My updated API page',
+            'chapter_id' => $chapter->id,
+            'html' => '<p>A page created via the API</p>',
+        ];
+
+        $resp = $this->putJson($this->baseEndpoint . "/{$page->id}", $details);
+        $resp->assertStatus(200);
+        $resp->assertJson([
+            'chapter_id' => $chapter->id,
+            'book_id' => $chapter->book_id,
+        ]);
+    }
+
+    public function test_providing_move_via_update_requires_page_create_permission_on_new_parent()
+    {
+        $this->actingAsApiEditor();
+        $page = Page::visible()->first();
+        $chapter = Chapter::visible()->where('book_id', '!=', $page->book_id)->first();
+        $this->setEntityRestrictions($chapter, ['view'], [$this->getEditor()->roles()->first()]);
+        $details = [
+            'name' => 'My updated API page',
+            'chapter_id' => $chapter->id,
+            'html' => '<p>A page created via the API</p>',
+        ];
+
+        $resp = $this->putJson($this->baseEndpoint . "/{$page->id}", $details);
+        $resp->assertStatus(403);
+    }
+
+    public function test_delete_endpoint()
+    {
+        $this->actingAsApiEditor();
+        $page = Page::visible()->first();
+        $resp = $this->deleteJson($this->baseEndpoint . "/{$page->id}");
+
+        $resp->assertStatus(204);
+        $this->assertActivityExists('page_delete', $page);
+    }
+
+    public function test_export_html_endpoint()
+    {
+        $this->actingAsApiEditor();
+        $page = Page::visible()->first();
+
+        $resp = $this->get($this->baseEndpoint . "/{$page->id}/export/html");
+        $resp->assertStatus(200);
+        $resp->assertSee($page->name);
+        $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.html"');
+    }
+
+    public function test_export_plain_text_endpoint()
+    {
+        $this->actingAsApiEditor();
+        $page = Page::visible()->first();
+
+        $resp = $this->get($this->baseEndpoint . "/{$page->id}/export/plaintext");
+        $resp->assertStatus(200);
+        $resp->assertSee($page->name);
+        $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.txt"');
+    }
+
+    public function test_export_pdf_endpoint()
+    {
+        $this->actingAsApiEditor();
+        $page = Page::visible()->first();
+
+        $resp = $this->get($this->baseEndpoint . "/{$page->id}/export/pdf");
+        $resp->assertStatus(200);
+        $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.pdf"');
+    }
+}
\ No newline at end of file
index 13e44d97de7d002317d2c598e107e584838b3088..4c5600d159935e17f313f6dd2fb75080378b62cc 100644 (file)
@@ -1,7 +1,7 @@
 <?php namespace Tests\Api;
 
-use BookStack\Entities\Book;
-use BookStack\Entities\Bookshelf;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Bookshelf;
 use Tests\TestCase;
 
 class ShelvesApiTest extends TestCase
index a2cdc33ffda04aa1895ce0566e123167b984b9ce..3dc6fd7c2ecfd46b19cde94ba81f04da4a8f5d9a 100644 (file)
@@ -2,13 +2,23 @@
 
 use BookStack\Actions\Activity;
 use BookStack\Actions\ActivityService;
+use BookStack\Actions\ActivityType;
 use BookStack\Auth\UserRepo;
-use BookStack\Entities\Page;
+use BookStack\Entities\Tools\TrashCan;
+use BookStack\Entities\Models\Page;
 use BookStack\Entities\Repos\PageRepo;
 use Carbon\Carbon;
 
 class AuditLogTest extends TestCase
 {
+    /** @var ActivityService  */
+    protected $activityService;
+
+    public function setUp(): void
+    {
+        parent::setUp();
+        $this->activityService = app(ActivityService::class);
+    }
 
     public function test_only_accessible_with_right_permissions()
     {
@@ -33,14 +43,14 @@ class AuditLogTest extends TestCase
         $admin = $this->getAdmin();
         $this->actingAs($admin);
         $page = Page::query()->first();
-        app(ActivityService::class)->add($page, 'page_create', $page->book->id);
+        $this->activityService->addForEntity($page, ActivityType::PAGE_CREATE);
         $activity = Activity::query()->orderBy('id', 'desc')->first();
 
         $resp = $this->get('settings/audit');
         $resp->assertSeeText($page->name);
         $resp->assertSeeText('page_create');
         $resp->assertSeeText($activity->created_at->toDateTimeString());
-        $resp->assertElementContains('.audit-log-user', $admin->name);
+        $resp->assertElementContains('.table-user-item', $admin->name);
     }
 
     public function test_shows_name_for_deleted_items()
@@ -48,9 +58,10 @@ class AuditLogTest extends TestCase
         $this->actingAs( $this->getAdmin());
         $page = Page::query()->first();
         $pageName = $page->name;
-        app(ActivityService::class)->add($page, 'page_create', $page->book->id);
+        $this->activityService->addForEntity($page, ActivityType::PAGE_CREATE);
 
         app(PageRepo::class)->destroy($page);
+        app(TrashCan::class)->empty();
 
         $resp = $this->get('settings/audit');
         $resp->assertSeeText('Deleted Item');
@@ -62,7 +73,7 @@ class AuditLogTest extends TestCase
         $viewer = $this->getViewer();
         $this->actingAs($viewer);
         $page = Page::query()->first();
-        app(ActivityService::class)->add($page, 'page_create', $page->book->id);
+        $this->activityService->addForEntity($page, ActivityType::PAGE_CREATE);
 
         $this->actingAs($this->getAdmin());
         app(UserRepo::class)->destroy($viewer);
@@ -75,7 +86,7 @@ class AuditLogTest extends TestCase
     {
         $this->actingAs($this->getAdmin());
         $page = Page::query()->first();
-        app(ActivityService::class)->add($page, 'page_create', $page->book->id);
+        $this->activityService->addForEntity($page, ActivityType::PAGE_CREATE);
 
         $resp = $this->get('settings/audit');
         $resp->assertSeeText($page->name);
@@ -88,7 +99,7 @@ class AuditLogTest extends TestCase
     {
         $this->actingAs($this->getAdmin());
         $page = Page::query()->first();
-        app(ActivityService::class)->add($page, 'page_create', $page->book->id);
+        $this->activityService->addForEntity($page, ActivityType::PAGE_CREATE);
 
         $yesterday = (Carbon::now()->subDay()->format('Y-m-d'));
         $tomorrow = (Carbon::now()->addDay()->format('Y-m-d'));
index e2b1e0cd66edcbae814bec9f055da290c8a0375d..a0de7f803505860647964c68a917c2d49833cf0d 100644 (file)
@@ -2,7 +2,7 @@
 
 use BookStack\Auth\Role;
 use BookStack\Auth\User;
-use BookStack\Entities\Page;
+use BookStack\Entities\Models\Page;
 use BookStack\Notifications\ConfirmEmail;
 use BookStack\Notifications\ResetPassword;
 use BookStack\Settings\SettingService;
index b81afe31106025352a3d5c50a23b2162b1ae6d5f..6c332a98469c4f5141972ee0b7e562b2f74b4ab2 100644 (file)
@@ -1,10 +1,16 @@
 <?php namespace Tests;
 
-use BookStack\Entities\Entity;
+use BookStack\Auth\User;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Models\Entity;
 use BookStack\Auth\Role;
 use BookStack\Auth\Permissions\PermissionService;
+use BookStack\Entities\Models\Page;
 use BookStack\Settings\SettingService;
+use DB;
 use Illuminate\Contracts\Console\Kernel;
+use Illuminate\Foundation\Application;
 use Illuminate\Foundation\Testing\DatabaseTransactions;
 use Laravel\BrowserKitTesting\TestCase;
 use Symfony\Component\DomCrawler\Crawler;
@@ -23,14 +29,14 @@ abstract class BrowserKitTest extends TestCase
 
     public function tearDown() : void
     {
-        \DB::disconnect();
+        DB::disconnect();
         parent::tearDown();
     }
 
     /**
      * Creates the application.
      *
-     * @return \Illuminate\Foundation\Application
+     * @return Application
      */
     public function createApplication()
     {
@@ -47,7 +53,7 @@ abstract class BrowserKitTest extends TestCase
      */
     public function getNormalUser()
     {
-        return \BookStack\Auth\User::where('system_name', '=', null)->get()->last();
+        return User::where('system_name', '=', null)->get()->last();
     }
 
     /**
@@ -64,23 +70,21 @@ abstract class BrowserKitTest extends TestCase
 
     /**
      * Create a group of entities that belong to a specific user.
-     * @param $creatorUser
-     * @param $updaterUser
-     * @return array
      */
-    protected function createEntityChainBelongingToUser($creatorUser, $updaterUser = false)
+    protected function createEntityChainBelongingToUser(User $creatorUser, ?User $updaterUser = null): array
     {
-        if ($updaterUser === false) $updaterUser = $creatorUser;
-        $book = factory(\BookStack\Entities\Book::class)->create(['created_by' => $creatorUser->id, 'updated_by' => $updaterUser->id]);
-        $chapter = factory(\BookStack\Entities\Chapter::class)->create(['created_by' => $creatorUser->id, 'updated_by' => $updaterUser->id, 'book_id' => $book->id]);
-        $page = factory(\BookStack\Entities\Page::class)->create(['created_by' => $creatorUser->id, 'updated_by' => $updaterUser->id, 'book_id' => $book->id, 'chapter_id' => $chapter->id]);
+        if (empty($updaterUser)) {
+            $updaterUser = $creatorUser;
+        }
+
+        $userAttrs = ['created_by' => $creatorUser->id, 'owned_by' => $creatorUser->id, 'updated_by' => $updaterUser->id];
+        $book = factory(Book::class)->create($userAttrs);
+        $chapter = factory(Chapter::class)->create(array_merge(['book_id' => $book->id], $userAttrs));
+        $page = factory(Page::class)->create(array_merge(['book_id' => $book->id, 'chapter_id' => $chapter->id], $userAttrs));
         $restrictionService = $this->app[PermissionService::class];
         $restrictionService->buildJointPermissionsForEntity($book);
-        return [
-            'book' => $book,
-            'chapter' => $chapter,
-            'page' => $page
-        ];
+
+        return compact('book', 'chapter', 'page');
     }
 
     /**
@@ -101,7 +105,7 @@ abstract class BrowserKitTest extends TestCase
      */
     protected function getNewBlankUser($attributes = [])
     {
-        $user = factory(\BookStack\Auth\User::class)->create($attributes);
+        $user = factory(User::class)->create($attributes);
         return $user;
     }
 
index bfc0ac0eb4bb1b8ab6ea9d0c1aae4b8cebcffd05..8c6ea84bf8524e2ec61d1b2284a560147ab760aa 100644 (file)
@@ -1,10 +1,11 @@
 <?php namespace Tests;
 
+use BookStack\Actions\ActivityType;
 use BookStack\Actions\Comment;
 use BookStack\Actions\CommentRepo;
 use BookStack\Auth\Permissions\JointPermission;
-use BookStack\Entities\Bookshelf;
-use BookStack\Entities\Page;
+use BookStack\Entities\Models\Bookshelf;
+use BookStack\Entities\Models\Page;
 use BookStack\Auth\User;
 use BookStack\Entities\Repos\PageRepo;
 use Symfony\Component\Console\Exception\RuntimeException;
@@ -37,10 +38,10 @@ class CommandsTest extends TestCase
     {
         $this->asEditor();
         $page = Page::first();
-        \Activity::add($page, 'page_update', $page->book->id);
+        \Activity::addForEntity($page, ActivityType::PAGE_UPDATE);
 
         $this->assertDatabaseHas('activities', [
-            'key' => 'page_update',
+            'type' => 'page_update',
             'entity_id' => $page->id,
             'user_id' => $this->getEditor()->id
         ]);
@@ -50,7 +51,7 @@ class CommandsTest extends TestCase
 
 
         $this->assertDatabaseMissing('activities', [
-            'key' => 'page_update'
+            'type' => 'page_update'
         ]);
     }
 
index cb3acfb1e8eb8724d3a927e0b4c8c1a5a11d9823..9b3290370c197a14bd1d558a9728e1c96cae89e6 100644 (file)
@@ -1,8 +1,8 @@
 <?php namespace Tests\Entity;
 
 use BookStack\Auth\User;
-use BookStack\Entities\Book;
-use BookStack\Entities\Bookshelf;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Bookshelf;
 use BookStack\Uploads\Image;
 use Illuminate\Support\Str;
 use Tests\TestCase;
@@ -222,16 +222,25 @@ class BookShelfTest extends TestCase
 
     public function test_shelf_delete()
     {
-        $shelf = Bookshelf::first();
-        $resp = $this->asEditor()->get($shelf->getUrl('/delete'));
-        $resp->assertSeeText('Delete Bookshelf');
-        $resp->assertSee("action=\"{$shelf->getUrl()}\"");
-
-        $resp = $this->delete($shelf->getUrl());
-        $resp->assertRedirect('/shelves');
-        $this->assertDatabaseMissing('bookshelves', ['id' => $shelf->id]);
-        $this->assertDatabaseMissing('bookshelves_books', ['bookshelf_id' => $shelf->id]);
-        $this->assertSessionHas('success');
+        $shelf = Bookshelf::query()->whereHas('books')->first();
+        $this->assertNull($shelf->deleted_at);
+        $bookCount = $shelf->books()->count();
+
+        $deleteViewReq = $this->asEditor()->get($shelf->getUrl('/delete'));
+        $deleteViewReq->assertSeeText('Are you sure you want to delete this bookshelf?');
+
+        $deleteReq = $this->delete($shelf->getUrl());
+        $deleteReq->assertRedirect(url('/shelves'));
+        $this->assertActivityExists('bookshelf_delete', $shelf);
+
+        $shelf->refresh();
+        $this->assertNotNull($shelf->deleted_at);
+
+        $this->assertTrue($shelf->books()->count() === $bookCount);
+        $this->assertTrue($shelf->deletions()->count() === 1);
+
+        $redirectReq = $this->get($deleteReq->baseResponse->headers->get('location'));
+        $redirectReq->assertNotificationContains('Bookshelf Successfully Deleted');
     }
 
     public function test_shelf_copy_permissions()
diff --git a/tests/Entity/BookTest.php b/tests/Entity/BookTest.php
new file mode 100644 (file)
index 0000000..6c2cf30
--- /dev/null
@@ -0,0 +1,34 @@
+<?php namespace Tests\Entity;
+
+use BookStack\Entities\Models\Book;
+use Tests\TestCase;
+
+class BookTest extends TestCase
+{
+    public function test_book_delete()
+    {
+        $book = Book::query()->whereHas('pages')->whereHas('chapters')->first();
+        $this->assertNull($book->deleted_at);
+        $pageCount = $book->pages()->count();
+        $chapterCount = $book->chapters()->count();
+
+        $deleteViewReq = $this->asEditor()->get($book->getUrl('/delete'));
+        $deleteViewReq->assertSeeText('Are you sure you want to delete this book?');
+
+        $deleteReq = $this->delete($book->getUrl());
+        $deleteReq->assertRedirect(url('/books'));
+        $this->assertActivityExists('book_delete', $book);
+
+        $book->refresh();
+        $this->assertNotNull($book->deleted_at);
+
+        $this->assertTrue($book->pages()->count() === 0);
+        $this->assertTrue($book->chapters()->count() === 0);
+        $this->assertTrue($book->pages()->withTrashed()->count() === $pageCount);
+        $this->assertTrue($book->chapters()->withTrashed()->count() === $chapterCount);
+        $this->assertTrue($book->deletions()->count() === 1);
+
+        $redirectReq = $this->get($deleteReq->baseResponse->headers->get('location'));
+        $redirectReq->assertNotificationContains('Book Successfully Deleted');
+    }
+}
\ No newline at end of file
diff --git a/tests/Entity/ChapterTest.php b/tests/Entity/ChapterTest.php
new file mode 100644 (file)
index 0000000..e9350a3
--- /dev/null
@@ -0,0 +1,31 @@
+<?php namespace Tests\Entity;
+
+use BookStack\Entities\Models\Chapter;
+use Tests\TestCase;
+
+class ChapterTest extends TestCase
+{
+    public function test_chapter_delete()
+    {
+        $chapter = Chapter::query()->whereHas('pages')->first();
+        $this->assertNull($chapter->deleted_at);
+        $pageCount = $chapter->pages()->count();
+
+        $deleteViewReq = $this->asEditor()->get($chapter->getUrl('/delete'));
+        $deleteViewReq->assertSeeText('Are you sure you want to delete this chapter?');
+
+        $deleteReq = $this->delete($chapter->getUrl());
+        $deleteReq->assertRedirect($chapter->getParent()->getUrl());
+        $this->assertActivityExists('chapter_delete', $chapter);
+
+        $chapter->refresh();
+        $this->assertNotNull($chapter->deleted_at);
+
+        $this->assertTrue($chapter->pages()->count() === 0);
+        $this->assertTrue($chapter->pages()->withTrashed()->count() === $pageCount);
+        $this->assertTrue($chapter->deletions()->count() === 1);
+
+        $redirectReq = $this->get($deleteReq->baseResponse->headers->get('location'));
+        $redirectReq->assertNotificationContains('Chapter Successfully Deleted');
+    }
+}
\ No newline at end of file
index 3c8cae68ccefefd1e28f0aca63884c28dfdf0b33..49ceede9f3edd23207b5f62906c14fe677fcd043 100644 (file)
@@ -1,6 +1,6 @@
 <?php namespace Tests\Entity;
 
-use BookStack\Entities\Page;
+use BookStack\Entities\Models\Page;
 use Tests\BrowserKitTest;
 
 class CommentSettingTest extends BrowserKitTest
index 2198b2dd2c72decb348bd6421d8f664b52c3e793..63d1a29a29ac656bf5d53ec38a00fad8102af35c 100644 (file)
@@ -1,6 +1,6 @@
 <?php namespace Tests\Entity;
 
-use BookStack\Entities\Page;
+use BookStack\Entities\Models\Page;
 use BookStack\Actions\Comment;
 use Tests\TestCase;
 
index 956e46c3713d4785cd7b8dcd11589cc6988dcfb0..2b5dc6d749cdbf40e24ae9f93c5f5d446c0fc051 100644 (file)
@@ -1,10 +1,10 @@
 <?php namespace Tests\Entity;
 
 use BookStack\Actions\Tag;
-use BookStack\Entities\Book;
-use BookStack\Entities\Bookshelf;
-use BookStack\Entities\Chapter;
-use BookStack\Entities\Page;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Bookshelf;
+use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Models\Page;
 use Tests\TestCase;
 
 class EntitySearchTest extends TestCase
index de1e025ade6a1e832cad59834f0c0774abf8ad1e..3a363e2b87bfeaaa7424946f49acff2556554c7d 100644 (file)
@@ -1,13 +1,12 @@
 <?php namespace Tests\Entity;
 
-use BookStack\Entities\Bookshelf;
-use BookStack\Entities\Book;
-use BookStack\Entities\Chapter;
-use BookStack\Entities\Page;
+use BookStack\Entities\Models\Bookshelf;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Models\Page;
 use BookStack\Auth\UserRepo;
 use BookStack\Entities\Repos\PageRepo;
 use Carbon\Carbon;
-use Illuminate\Support\Facades\DB;
 use Tests\BrowserKitTest;
 
 class EntityTest extends BrowserKitTest
@@ -18,27 +17,10 @@ class EntityTest extends BrowserKitTest
         // Test Creation
         $book = $this->bookCreation();
         $chapter = $this->chapterCreation($book);
-        $page = $this->pageCreation($chapter);
+        $this->pageCreation($chapter);
 
         // Test Updating
-        $book = $this->bookUpdate($book);
-
-        // Test Deletion
-        $this->bookDelete($book);
-    }
-
-    public function bookDelete(Book $book)
-    {
-        $this->asAdmin()
-            ->visit($book->getUrl())
-            // Check link works correctly
-            ->click('Delete')
-            ->seePageIs($book->getUrl() . '/delete')
-            // Ensure the book name is show to user
-            ->see($book->name)
-            ->press('Confirm')
-            ->seePageIs('/books')
-            ->notSeeInDatabase('books', ['id' => $book->id]);
+        $this->bookUpdate($book);
     }
 
     public function bookUpdate(Book $book)
@@ -332,34 +314,4 @@ class EntityTest extends BrowserKitTest
             ->seePageIs($chapter->getUrl());
     }
 
-    public function test_page_delete_removes_entity_from_its_activity()
-    {
-        $page = Page::query()->first();
-
-        $this->asEditor()->put($page->getUrl(), [
-            'name' => 'My updated page',
-            'html' => '<p>updated content</p>',
-        ]);
-        $page->refresh();
-
-        $this->seeInDatabase('activities', [
-            'entity_id' => $page->id,
-            'entity_type' => $page->getMorphClass(),
-        ]);
-
-        $resp = $this->delete($page->getUrl());
-        $resp->assertResponseStatus(302);
-
-        $this->dontSeeInDatabase('activities', [
-            'entity_id' => $page->id,
-            'entity_type' => $page->getMorphClass(),
-        ]);
-
-        $this->seeInDatabase('activities', [
-            'extra' => 'My updated page',
-            'entity_id' => 0,
-            'entity_type' => '',
-        ]);
-    }
-
 }
index 5a94adac91c4b8d8dc46866f897e45f7057c3808..1e44f015a5a0b69f8520c9227b971e79f17c0b63 100644 (file)
@@ -1,9 +1,9 @@
 <?php namespace Tests\Entity;
 
 
-use BookStack\Entities\Chapter;
-use BookStack\Entities\Page;
-use BookStack\Uploads\HttpFetcher;
+use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Models\Page;
+use Illuminate\Support\Facades\Storage;
 use Illuminate\Support\Str;
 use Tests\TestCase;
 
@@ -154,14 +154,55 @@ class ExportTest extends TestCase
     public function test_page_export_sets_right_data_type_for_svg_embeds()
     {
         $page = Page::first();
-        $page->html = '<img src="http://example.com/image.svg">';
+        Storage::disk('local')->makeDirectory('uploads/images/gallery');
+        Storage::disk('local')->put('uploads/images/gallery/svg_test.svg', '<svg></svg>');
+        $page->html = '<img src="http://localhost/uploads/images/gallery/svg_test.svg">';
         $page->save();
 
         $this->asEditor();
-        $this->mockHttpFetch('<svg></svg>');
         $resp = $this->get($page->getUrl('/export/html'));
+        Storage::disk('local')->delete('uploads/images/gallery/svg_test.svg');
+
         $resp->assertStatus(200);
         $resp->assertSee('<img src="data:image/svg+xml;base64');
     }
 
-}
\ No newline at end of file
+    public function test_page_image_containment_works_on_multiple_images_within_a_single_line()
+    {
+        $page = Page::first();
+        Storage::disk('local')->makeDirectory('uploads/images/gallery');
+        Storage::disk('local')->put('uploads/images/gallery/svg_test.svg', '<svg></svg>');
+        Storage::disk('local')->put('uploads/images/gallery/svg_test2.svg', '<svg></svg>');
+        $page->html = '<img src="http://localhost/uploads/images/gallery/svg_test.svg" class="a"><img src="http://localhost/uploads/images/gallery/svg_test2.svg" class="b">';
+        $page->save();
+
+        $resp = $this->asEditor()->get($page->getUrl('/export/html'));
+        Storage::disk('local')->delete('uploads/images/gallery/svg_test.svg');
+        Storage::disk('local')->delete('uploads/images/gallery/svg_test2.svg');
+
+        $resp->assertDontSee('http://localhost/uploads/images/gallery/svg_test');
+    }
+
+    public function test_page_export_contained_html_image_fetches_only_run_when_url_points_to_image_upload_folder()
+    {
+        $page = Page::first();
+        $page->html = '<img src="http://localhost/uploads/images/gallery/svg_test.svg"/>'
+            .'<img src="http://localhost/uploads/svg_test.svg"/>'
+            .'<img src="/uploads/svg_test.svg"/>';
+        $storageDisk = Storage::disk('local');
+        $storageDisk->makeDirectory('uploads/images/gallery');
+        $storageDisk->put('uploads/images/gallery/svg_test.svg', '<svg>good</svg>');
+        $storageDisk->put('uploads/svg_test.svg', '<svg>bad</svg>');
+        $page->save();
+
+        $resp = $this->asEditor()->get($page->getUrl('/export/html'));
+
+        $storageDisk->delete('uploads/images/gallery/svg_test.svg');
+        $storageDisk->delete('uploads/svg_test.svg');
+
+        $resp->assertDontSee('http://localhost/uploads/images/gallery/svg_test.svg');
+        $resp->assertSee('http://localhost/uploads/svg_test.svg');
+        $resp->assertSee('src="/uploads/svg_test.svg"');
+    }
+
+}
index 452b4c07f379ed1a1ed4e7e1c747192ca541700c..5e5fa8a0c2f7b52427784e295bef3b935e250294 100644 (file)
@@ -9,7 +9,7 @@ class MarkdownTest extends BrowserKitTest
     public function setUp(): void
     {
         parent::setUp();
-        $this->page = \BookStack\Entities\Page::first();
+        $this->page = \BookStack\Entities\Models\Page::first();
     }
 
     protected function setMarkdownEditor()
index e97df2c7edd80725bb9e830f9cebf8146def5c69..51a8568bfa9a7e8a32e45b844da214b7f4592aa1 100644 (file)
@@ -1,7 +1,7 @@
 <?php namespace Tests\Entity;
 
-use BookStack\Entities\Managers\PageContent;
-use BookStack\Entities\Page;
+use BookStack\Entities\Tools\PageContent;
+use BookStack\Entities\Models\Page;
 use Tests\TestCase;
 
 class PageContentTest extends TestCase
index a0cf9e5fca9267aa6a8866f00f9e940e87b27ece..0e3980c6702217cd10043748a9d8ad89dbb18bcc 100644 (file)
@@ -1,6 +1,6 @@
 <?php namespace Tests\Entity;
 
-use BookStack\Entities\Page;
+use BookStack\Entities\Models\Page;
 use BookStack\Entities\Repos\PageRepo;
 use Tests\BrowserKitTest;
 
@@ -16,7 +16,7 @@ class PageDraftTest extends BrowserKitTest
     public function setUp(): void
     {
         parent::setUp();
-        $this->page = \BookStack\Entities\Page::first();
+        $this->page = \BookStack\Entities\Models\Page::first();
         $this->pageRepo = app(PageRepo::class);
     }
 
@@ -56,7 +56,7 @@ class PageDraftTest extends BrowserKitTest
 
     public function test_alert_message_shows_if_someone_else_editing()
     {
-        $nonEditedPage = \BookStack\Entities\Page::take(10)->get()->last();
+        $nonEditedPage = \BookStack\Entities\Models\Page::take(10)->get()->last();
         $addedContent = '<p>test message content</p>';
         $this->asAdmin()->visit($this->page->getUrl('/edit'))
             ->dontSeeInField('html', $addedContent);
@@ -75,7 +75,7 @@ class PageDraftTest extends BrowserKitTest
 
     public function test_draft_pages_show_on_homepage()
     {
-        $book = \BookStack\Entities\Book::first();
+        $book = \BookStack\Entities\Models\Book::first();
         $this->asAdmin()->visit('/')
             ->dontSeeInElement('#recent-drafts', 'New Page')
             ->visit($book->getUrl() . '/create-page')
@@ -85,7 +85,7 @@ class PageDraftTest extends BrowserKitTest
 
     public function test_draft_pages_not_visible_by_others()
     {
-        $book = \BookStack\Entities\Book::first();
+        $book = \BookStack\Entities\Models\Book::first();
         $chapter = $book->chapters->first();
         $newUser = $this->getEditor();
 
index 1e9dbd626b78fd011184e3549cbf239578365b6a..6eaea129c0ace0a333393bc853b09dc2fb9934f2 100644 (file)
@@ -1,6 +1,6 @@
 <?php namespace Tests\Entity;
 
-use BookStack\Entities\Page;
+use BookStack\Entities\Models\Page;
 use BookStack\Entities\Repos\PageRepo;
 use Tests\TestCase;
 
index 8eba1355792f593be11a7431c6f6866eaa3732cc..a5594e8b8df06ececf95ae1df6d8256590738e91 100644 (file)
@@ -1,6 +1,6 @@
 <?php namespace Tests\Entity;
 
-use BookStack\Entities\Page;
+use BookStack\Entities\Models\Page;
 use Tests\TestCase;
 
 class PageTemplateTest extends TestCase
diff --git a/tests/Entity/PageTest.php b/tests/Entity/PageTest.php
new file mode 100644 (file)
index 0000000..887dfe8
--- /dev/null
@@ -0,0 +1,27 @@
+<?php namespace Tests\Entity;
+
+use BookStack\Entities\Models\Page;
+use Tests\TestCase;
+
+class PageTest extends TestCase
+{
+    public function test_page_delete()
+    {
+        $page = Page::query()->first();
+        $this->assertNull($page->deleted_at);
+
+        $deleteViewReq = $this->asEditor()->get($page->getUrl('/delete'));
+        $deleteViewReq->assertSeeText('Are you sure you want to delete this page?');
+
+        $deleteReq = $this->delete($page->getUrl());
+        $deleteReq->assertRedirect($page->getParent()->getUrl());
+        $this->assertActivityExists('page_delete', $page);
+
+        $page->refresh();
+        $this->assertNotNull($page->deleted_at);
+        $this->assertTrue($page->deletions()->count() === 1);
+
+        $redirectReq = $this->get($deleteReq->baseResponse->headers->get('location'));
+        $redirectReq->assertNotificationContains('Page Successfully Deleted');
+    }
+}
\ No newline at end of file
index 727db553367fe647462a9c880a4c9bbb16998dc8..c9e116523ed0fdf943622fc4c6a77a7d07ceb689 100644 (file)
@@ -1,6 +1,6 @@
 <?php namespace Tests\Entity;
 
-use BookStack\Entities\SearchOptions;
+use BookStack\Entities\Tools\SearchOptions;
 use Tests\TestCase;
 
 class SearchOptionsTest extends TestCase
index 28c3adf312682fa84783a78634eb946681c98e70..01f764b7b16e05d0bad2f99610963fd3f39900cf 100644 (file)
@@ -1,8 +1,8 @@
 <?php namespace Tests\Entity;
 
-use BookStack\Entities\Book;
-use BookStack\Entities\Chapter;
-use BookStack\Entities\Page;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Models\Page;
 use BookStack\Entities\Repos\PageRepo;
 use Tests\TestCase;
 
@@ -79,7 +79,7 @@ class SortTest extends TestCase
         $movePageResp = $this->actingAs($this->getEditor())->put($page->getUrl('/move'), [
             'entity_selection' => 'book:' . $newBook->id
         ]);
-        $page = Page::find($page->id);
+        $page->refresh();
 
         $movePageResp->assertRedirect($page->getUrl());
         $this->assertTrue($page->book->id == $newBook->id, 'Page parent is now the new book');
@@ -287,7 +287,7 @@ class SortTest extends TestCase
         $resp = $this->actingAs($viewer)->get($page->getUrl());
         $resp->assertDontSee($page->getUrl('/copy'));
 
-        $newBook->created_by = $viewer->id;
+        $newBook->owned_by = $viewer->id;
         $newBook->save();
         $this->giveUserPermissions($viewer, ['page-create-own']);
         $this->regenEntityPermissions($newBook);
index e8a99cf781b6bd972ee43708025da51d081c1c51..3ad10641ef3d0196ce15800d18d0ed149e3ed50a 100644 (file)
@@ -1,10 +1,10 @@
 <?php namespace Tests\Entity;
 
-use BookStack\Entities\Book;
-use BookStack\Entities\Chapter;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Chapter;
 use BookStack\Actions\Tag;
-use BookStack\Entities\Entity;
-use BookStack\Entities\Page;
+use BookStack\Entities\Models\Entity;
+use BookStack\Entities\Models\Page;
 use BookStack\Auth\Permissions\PermissionService;
 use Tests\BrowserKitTest;
 
index 8f6867cdeb0c357e16f9b7ce7df7df3813984200..1558df78d1c200d59b29d559ceef38f687b81e4f 100644 (file)
@@ -1,6 +1,6 @@
 <?php namespace Tests;
 
-use BookStack\Entities\Book;
+use BookStack\Entities\Models\Book;
 use Illuminate\Support\Facades\Log;
 
 class ErrorTest extends TestCase
index ada1f5aafde22b4929d31166be3d156a7c98e053..943a3160a1915af2a803ed88cd82411e67f2336d 100644 (file)
@@ -1,6 +1,6 @@
 <?php namespace Tests;
 
-use BookStack\Entities\Bookshelf;
+use BookStack\Entities\Models\Bookshelf;
 
 class HomepageTest extends TestCase
 {
@@ -98,16 +98,16 @@ class HomepageTest extends TestCase
     {
         $editor = $this->getEditor();
         setting()->putUser($editor, 'bookshelves_view_type', 'grid');
+        $shelf = Bookshelf::query()->firstOrFail();
 
         $this->setSettings(['app-homepage-type' => 'bookshelves']);
 
         $this->asEditor();
         $homeVisit = $this->get('/');
         $homeVisit->assertSee('Shelves');
-        $homeVisit->assertSee('bookshelf-grid-item grid-card');
         $homeVisit->assertSee('grid-card-content');
-        $homeVisit->assertSee('grid-card-footer');
         $homeVisit->assertSee('featured-image-container');
+        $homeVisit->assertElementContains('.grid-card', $shelf->name);
 
         $this->setSettings(['app-homepage-type' => false]);
         $this->test_default_homepage_visible();
diff --git a/tests/Permissions/EntityOwnerChangeTest.php b/tests/Permissions/EntityOwnerChangeTest.php
new file mode 100644 (file)
index 0000000..2f06bff
--- /dev/null
@@ -0,0 +1,50 @@
+<?php namespace Tests\Permissions;
+
+use BookStack\Auth\User;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Bookshelf;
+use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Models\Page;
+use Illuminate\Support\Str;
+use Tests\TestCase;
+
+class EntityOwnerChangeTest extends TestCase
+{
+
+    public function test_changing_page_owner()
+    {
+        $page = Page::query()->first();
+        $user = User::query()->where('id', '!=', $page->owned_by)->first();
+
+        $this->asAdmin()->put($page->getUrl('permissions'), ['owned_by' => $user->id]);
+        $this->assertDatabaseHas('pages', ['owned_by' => $user->id, 'id' => $page->id]);
+    }
+
+    public function test_changing_chapter_owner()
+    {
+        $chapter = Chapter::query()->first();
+        $user = User::query()->where('id', '!=', $chapter->owned_by)->first();
+
+        $this->asAdmin()->put($chapter->getUrl('permissions'), ['owned_by' => $user->id]);
+        $this->assertDatabaseHas('chapters', ['owned_by' => $user->id, 'id' => $chapter->id]);
+    }
+
+    public function test_changing_book_owner()
+    {
+        $book = Book::query()->first();
+        $user = User::query()->where('id', '!=', $book->owned_by)->first();
+
+        $this->asAdmin()->put($book->getUrl('permissions'), ['owned_by' => $user->id]);
+        $this->assertDatabaseHas('books', ['owned_by' => $user->id, 'id' => $book->id]);
+    }
+
+    public function test_changing_shelf_owner()
+    {
+        $shelf = Bookshelf::query()->first();
+        $user = User::query()->where('id', '!=', $shelf->owned_by)->first();
+
+        $this->asAdmin()->put($shelf->getUrl('permissions'), ['owned_by' => $user->id]);
+        $this->assertDatabaseHas('bookshelves', ['owned_by' => $user->id, 'id' => $shelf->id]);
+    }
+
+}
\ No newline at end of file
similarity index 96%
rename from tests/Permissions/RestrictionsTest.php
rename to tests/Permissions/EntityPermissionsTest.php
index a43a65e5865c16744eddf636fa208ccce054176f..1e6d1cc325e6bff83ff55322dc05e0c99e0c22d1 100644 (file)
@@ -1,14 +1,15 @@
 <?php namespace Tests\Permissions;
 
-use BookStack\Entities\Book;
-use BookStack\Entities\Bookshelf;
-use BookStack\Entities\Chapter;
-use BookStack\Entities\Entity;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Bookshelf;
+use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Models\Entity;
 use BookStack\Auth\User;
-use BookStack\Entities\Page;
+use BookStack\Entities\Models\Page;
+use Illuminate\Support\Str;
 use Tests\BrowserKitTest;
 
-class RestrictionsTest extends BrowserKitTest
+class EntityPermissionsTest extends BrowserKitTest
 {
 
     /**
@@ -490,6 +491,22 @@ class RestrictionsTest extends BrowserKitTest
             ->dontSee($page->name);
     }
 
+    public function test_restricted_chapter_pages_not_visible_on_book_page()
+    {
+        $chapter = Chapter::query()->first();
+        $this->actingAs($this->user)
+            ->visit($chapter->book->getUrl())
+            ->see($chapter->pages->first()->name);
+
+        foreach ($chapter->pages as $page) {
+            $this->setEntityRestrictions($page, []);
+        }
+
+        $this->actingAs($this->user)
+            ->visit($chapter->book->getUrl())
+            ->dontSee($chapter->pages->first()->name);
+    }
+
     public function test_bookshelf_update_restriction_override()
     {
         $shelf = Bookshelf::first();
@@ -626,11 +643,15 @@ class RestrictionsTest extends BrowserKitTest
     public function test_page_visible_if_has_permissions_when_book_not_visible()
     {
         $book = Book::first();
-
-        $this->setEntityRestrictions($book, []);
-
         $bookChapter = $book->chapters->first();
         $bookPage = $bookChapter->pages->first();
+
+        foreach ([$book, $bookChapter, $bookPage] as $entity) {
+            $entity->name = Str::random(24);
+            $entity->save();
+        }
+
+        $this->setEntityRestrictions($book, []);
         $this->setEntityRestrictions($bookPage, ['view']);
 
         $this->actingAs($this->viewer);
diff --git a/tests/Permissions/ExportPermissionsTest.php b/tests/Permissions/ExportPermissionsTest.php
new file mode 100644 (file)
index 0000000..e5a1146
--- /dev/null
@@ -0,0 +1,67 @@
+<?php namespace Tests\Permissions;
+
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Chapter;
+use Illuminate\Support\Str;
+use Tests\TestCase;
+
+class ExportPermissionsTest extends TestCase
+{
+
+    public function test_page_content_without_view_access_hidden_on_chapter_export()
+    {
+        $chapter = Chapter::query()->first();
+        $page = $chapter->pages()->firstOrFail();
+        $pageContent = Str::random(48);
+        $page->html = '<p>' . $pageContent . '</p>';
+        $page->save();
+        $viewer = $this->getViewer();
+        $this->actingAs($viewer);
+        $formats = ['html', 'plaintext'];
+
+        foreach ($formats as $format) {
+            $resp = $this->get($chapter->getUrl("export/{$format}"));
+            $resp->assertStatus(200);
+            $resp->assertSee($page->name);
+            $resp->assertSee($pageContent);
+        }
+
+        $this->setEntityRestrictions($page, []);
+
+        foreach ($formats as $format) {
+            $resp = $this->get($chapter->getUrl("export/{$format}"));
+            $resp->assertStatus(200);
+            $resp->assertDontSee($page->name);
+            $resp->assertDontSee($pageContent);
+        }
+    }
+
+    public function test_page_content_without_view_access_hidden_on_book_export()
+    {
+        $book = Book::query()->first();
+        $page = $book->pages()->firstOrFail();
+        $pageContent = Str::random(48);
+        $page->html = '<p>' . $pageContent . '</p>';
+        $page->save();
+        $viewer = $this->getViewer();
+        $this->actingAs($viewer);
+        $formats = ['html', 'plaintext'];
+
+        foreach ($formats as $format) {
+            $resp = $this->get($book->getUrl("export/{$format}"));
+            $resp->assertStatus(200);
+            $resp->assertSee($page->name);
+            $resp->assertSee($pageContent);
+        }
+
+        $this->setEntityRestrictions($page, []);
+
+        foreach ($formats as $format) {
+            $resp = $this->get($book->getUrl("export/{$format}"));
+            $resp->assertStatus(200);
+            $resp->assertDontSee($page->name);
+            $resp->assertDontSee($pageContent);
+        }
+    }
+
+}
\ No newline at end of file
index 73060c834383726a138fa8ce180980123070104a..3397ef42905bdb029108da634558a281d4482800 100644 (file)
@@ -1,8 +1,13 @@
 <?php namespace Tests\Permissions;
 
-use BookStack\Entities\Bookshelf;
-use BookStack\Entities\Page;
+use BookStack\Actions\Comment;
+use BookStack\Auth\User;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Bookshelf;
+use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Models\Page;
 use BookStack\Auth\Role;
+use BookStack\Uploads\Image;
 use Laravel\BrowserKitTesting\HttpException;
 use Tests\BrowserKitTest;
 
@@ -23,7 +28,7 @@ class RolesTest extends BrowserKitTest
 
     public function test_cannot_delete_admin_role()
     {
-        $adminRole = \BookStack\Auth\Role::getRole('admin');
+        $adminRole = Role::getRole('admin');
         $deletePageUrl = '/settings/roles/delete/' . $adminRole->id;
         $this->asAdmin()->visit($deletePageUrl)
             ->press('Confirm')
@@ -195,7 +200,7 @@ class RolesTest extends BrowserKitTest
 
     public function test_restrictions_manage_all_permission()
     {
-        $page = \BookStack\Entities\Page::take(1)->get()->first();
+        $page = Page::take(1)->get()->first();
         $this->actingAs($this->user)->visit($page->getUrl())
             ->dontSee('Permissions')
             ->visit($page->getUrl() . '/permissions')
@@ -209,7 +214,7 @@ class RolesTest extends BrowserKitTest
 
     public function test_restrictions_manage_own_permission()
     {
-        $otherUsersPage = \BookStack\Entities\Page::first();
+        $otherUsersPage = Page::first();
         $content = $this->createEntityChainBelongingToUser($this->user);
         // Check can't restrict other's content
         $this->actingAs($this->user)->visit($otherUsersPage->getUrl())
@@ -284,7 +289,7 @@ class RolesTest extends BrowserKitTest
     {
         $otherShelf = Bookshelf::first();
         $ownShelf = $this->newShelf(['name' => 'test-shelf', 'slug' => 'test-shelf']);
-        $ownShelf->forceFill(['created_by' => $this->user->id, 'updated_by' => $this->user->id])->save();
+        $ownShelf->forceFill(['owned_by' => $this->user->id, 'updated_by' => $this->user->id])->save();
         $this->regenEntityPermissions($ownShelf);
 
         $this->checkAccessPermission('bookshelf-update-own', [
@@ -301,7 +306,7 @@ class RolesTest extends BrowserKitTest
 
     public function test_bookshelves_edit_all_permission()
     {
-        $otherShelf = \BookStack\Entities\Bookshelf::first();
+        $otherShelf = Bookshelf::first();
         $this->checkAccessPermission('bookshelf-update-all', [
             $otherShelf->getUrl('/edit')
         ], [
@@ -312,9 +317,9 @@ class RolesTest extends BrowserKitTest
     public function test_bookshelves_delete_own_permission()
     {
         $this->giveUserPermissions($this->user, ['bookshelf-update-all']);
-        $otherShelf = \BookStack\Entities\Bookshelf::first();
+        $otherShelf = Bookshelf::first();
         $ownShelf = $this->newShelf(['name' => 'test-shelf', 'slug' => 'test-shelf']);
-        $ownShelf->forceFill(['created_by' => $this->user->id, 'updated_by' => $this->user->id])->save();
+        $ownShelf->forceFill(['owned_by' => $this->user->id, 'updated_by' => $this->user->id])->save();
         $this->regenEntityPermissions($ownShelf);
 
         $this->checkAccessPermission('bookshelf-delete-own', [
@@ -336,7 +341,7 @@ class RolesTest extends BrowserKitTest
     public function test_bookshelves_delete_all_permission()
     {
         $this->giveUserPermissions($this->user, ['bookshelf-update-all']);
-        $otherShelf = \BookStack\Entities\Bookshelf::first();
+        $otherShelf = Bookshelf::first();
         $this->checkAccessPermission('bookshelf-delete-all', [
             $otherShelf->getUrl('/delete')
         ], [
@@ -366,7 +371,7 @@ class RolesTest extends BrowserKitTest
 
     public function test_books_edit_own_permission()
     {
-        $otherBook = \BookStack\Entities\Book::take(1)->get()->first();
+        $otherBook = Book::take(1)->get()->first();
         $ownBook = $this->createEntityChainBelongingToUser($this->user)['book'];
         $this->checkAccessPermission('book-update-own', [
             $ownBook->getUrl() . '/edit'
@@ -382,7 +387,7 @@ class RolesTest extends BrowserKitTest
 
     public function test_books_edit_all_permission()
     {
-        $otherBook = \BookStack\Entities\Book::take(1)->get()->first();
+        $otherBook = Book::take(1)->get()->first();
         $this->checkAccessPermission('book-update-all', [
             $otherBook->getUrl() . '/edit'
         ], [
@@ -393,7 +398,7 @@ class RolesTest extends BrowserKitTest
     public function test_books_delete_own_permission()
     {
         $this->giveUserPermissions($this->user, ['book-update-all']);
-        $otherBook = \BookStack\Entities\Book::take(1)->get()->first();
+        $otherBook = Book::take(1)->get()->first();
         $ownBook = $this->createEntityChainBelongingToUser($this->user)['book'];
         $this->checkAccessPermission('book-delete-own', [
             $ownBook->getUrl() . '/delete'
@@ -414,7 +419,7 @@ class RolesTest extends BrowserKitTest
     public function test_books_delete_all_permission()
     {
         $this->giveUserPermissions($this->user, ['book-update-all']);
-        $otherBook = \BookStack\Entities\Book::take(1)->get()->first();
+        $otherBook = Book::take(1)->get()->first();
         $this->checkAccessPermission('book-delete-all', [
             $otherBook->getUrl() . '/delete'
         ], [
@@ -429,7 +434,7 @@ class RolesTest extends BrowserKitTest
 
     public function test_chapter_create_own_permissions()
     {
-        $book = \BookStack\Entities\Book::take(1)->get()->first();
+        $book = Book::take(1)->get()->first();
         $ownBook = $this->createEntityChainBelongingToUser($this->user)['book'];
         $this->checkAccessPermission('chapter-create-own', [
             $ownBook->getUrl('/create-chapter')
@@ -451,7 +456,7 @@ class RolesTest extends BrowserKitTest
 
     public function test_chapter_create_all_permissions()
     {
-        $book = \BookStack\Entities\Book::take(1)->get()->first();
+        $book = Book::take(1)->get()->first();
         $this->checkAccessPermission('chapter-create-all', [
             $book->getUrl('/create-chapter')
         ], [
@@ -467,7 +472,7 @@ class RolesTest extends BrowserKitTest
 
     public function test_chapter_edit_own_permission()
     {
-        $otherChapter = \BookStack\Entities\Chapter::take(1)->get()->first();
+        $otherChapter = Chapter::take(1)->get()->first();
         $ownChapter = $this->createEntityChainBelongingToUser($this->user)['chapter'];
         $this->checkAccessPermission('chapter-update-own', [
             $ownChapter->getUrl() . '/edit'
@@ -483,7 +488,7 @@ class RolesTest extends BrowserKitTest
 
     public function test_chapter_edit_all_permission()
     {
-        $otherChapter = \BookStack\Entities\Chapter::take(1)->get()->first();
+        $otherChapter = Chapter::take(1)->get()->first();
         $this->checkAccessPermission('chapter-update-all', [
             $otherChapter->getUrl() . '/edit'
         ], [
@@ -494,7 +499,7 @@ class RolesTest extends BrowserKitTest
     public function test_chapter_delete_own_permission()
     {
         $this->giveUserPermissions($this->user, ['chapter-update-all']);
-        $otherChapter = \BookStack\Entities\Chapter::take(1)->get()->first();
+        $otherChapter = Chapter::take(1)->get()->first();
         $ownChapter = $this->createEntityChainBelongingToUser($this->user)['chapter'];
         $this->checkAccessPermission('chapter-delete-own', [
             $ownChapter->getUrl() . '/delete'
@@ -516,7 +521,7 @@ class RolesTest extends BrowserKitTest
     public function test_chapter_delete_all_permission()
     {
         $this->giveUserPermissions($this->user, ['chapter-update-all']);
-        $otherChapter = \BookStack\Entities\Chapter::take(1)->get()->first();
+        $otherChapter = Chapter::take(1)->get()->first();
         $this->checkAccessPermission('chapter-delete-all', [
             $otherChapter->getUrl() . '/delete'
         ], [
@@ -532,8 +537,8 @@ class RolesTest extends BrowserKitTest
 
     public function test_page_create_own_permissions()
     {
-        $book = \BookStack\Entities\Book::first();
-        $chapter = \BookStack\Entities\Chapter::first();
+        $book = Book::first();
+        $chapter = Chapter::first();
 
         $entities = $this->createEntityChainBelongingToUser($this->user);
         $ownBook = $entities['book'];
@@ -557,7 +562,7 @@ class RolesTest extends BrowserKitTest
 
         foreach ($accessUrls as $index => $url) {
             $this->actingAs($this->user)->visit($url);
-            $expectedUrl = \BookStack\Entities\Page::where('draft', '=', true)->orderBy('id', 'desc')->first()->getUrl();
+            $expectedUrl = Page::where('draft', '=', true)->orderBy('id', 'desc')->first()->getUrl();
             $this->seePageIs($expectedUrl);
         }
 
@@ -579,8 +584,8 @@ class RolesTest extends BrowserKitTest
 
     public function test_page_create_all_permissions()
     {
-        $book = \BookStack\Entities\Book::take(1)->get()->first();
-        $chapter = \BookStack\Entities\Chapter::take(1)->get()->first();
+        $book = Book::take(1)->get()->first();
+        $chapter = Chapter::take(1)->get()->first();
         $baseUrl = $book->getUrl() . '/page';
         $createUrl = $book->getUrl('/create-page');
 
@@ -601,7 +606,7 @@ class RolesTest extends BrowserKitTest
 
         foreach ($accessUrls as $index => $url) {
             $this->actingAs($this->user)->visit($url);
-            $expectedUrl = \BookStack\Entities\Page::where('draft', '=', true)->orderBy('id', 'desc')->first()->getUrl();
+            $expectedUrl = Page::where('draft', '=', true)->orderBy('id', 'desc')->first()->getUrl();
             $this->seePageIs($expectedUrl);
         }
 
@@ -620,7 +625,7 @@ class RolesTest extends BrowserKitTest
 
     public function test_page_edit_own_permission()
     {
-        $otherPage = \BookStack\Entities\Page::take(1)->get()->first();
+        $otherPage = Page::take(1)->get()->first();
         $ownPage = $this->createEntityChainBelongingToUser($this->user)['page'];
         $this->checkAccessPermission('page-update-own', [
             $ownPage->getUrl() . '/edit'
@@ -636,7 +641,7 @@ class RolesTest extends BrowserKitTest
 
     public function test_page_edit_all_permission()
     {
-        $otherPage = \BookStack\Entities\Page::take(1)->get()->first();
+        $otherPage = Page::take(1)->get()->first();
         $this->checkAccessPermission('page-update-all', [
             $otherPage->getUrl() . '/edit'
         ], [
@@ -647,7 +652,7 @@ class RolesTest extends BrowserKitTest
     public function test_page_delete_own_permission()
     {
         $this->giveUserPermissions($this->user, ['page-update-all']);
-        $otherPage = \BookStack\Entities\Page::take(1)->get()->first();
+        $otherPage = Page::take(1)->get()->first();
         $ownPage = $this->createEntityChainBelongingToUser($this->user)['page'];
         $this->checkAccessPermission('page-delete-own', [
             $ownPage->getUrl() . '/delete'
@@ -669,7 +674,7 @@ class RolesTest extends BrowserKitTest
     public function test_page_delete_all_permission()
     {
         $this->giveUserPermissions($this->user, ['page-update-all']);
-        $otherPage = \BookStack\Entities\Page::take(1)->get()->first();
+        $otherPage = Page::take(1)->get()->first();
         $this->checkAccessPermission('page-delete-all', [
             $otherPage->getUrl() . '/delete'
         ], [
@@ -685,7 +690,7 @@ class RolesTest extends BrowserKitTest
 
     public function test_public_role_visible_in_user_edit_screen()
     {
-        $user = \BookStack\Auth\User::first();
+        $user = User::first();
         $adminRole = Role::getSystemRole('admin');
         $publicRole = Role::getSystemRole('public');
         $this->asAdmin()->visit('/settings/users/' . $user->id)
@@ -721,8 +726,8 @@ class RolesTest extends BrowserKitTest
     public function test_image_delete_own_permission()
     {
         $this->giveUserPermissions($this->user, ['image-update-all']);
-        $page = \BookStack\Entities\Page::first();
-        $image = factory(\BookStack\Uploads\Image::class)->create(['uploaded_to' => $page->id, 'created_by' => $this->user->id, 'updated_by' => $this->user->id]);
+        $page = Page::first();
+        $image = factory(Image::class)->create(['uploaded_to' => $page->id, 'created_by' => $this->user->id, 'updated_by' => $this->user->id]);
 
         $this->actingAs($this->user)->json('delete', '/images/' . $image->id)
             ->seeStatusCode(403);
@@ -738,8 +743,8 @@ class RolesTest extends BrowserKitTest
     {
         $this->giveUserPermissions($this->user, ['image-update-all']);
         $admin = $this->getAdmin();
-        $page = \BookStack\Entities\Page::first();
-        $image = factory(\BookStack\Uploads\Image::class)->create(['uploaded_to' => $page->id, 'created_by' => $admin->id, 'updated_by' => $admin->id]);
+        $page = Page::first();
+        $image = factory(Image::class)->create(['uploaded_to' => $page->id, 'created_by' => $admin->id, 'updated_by' => $admin->id]);
 
         $this->actingAs($this->user)->json('delete', '/images/' . $image->id)
             ->seeStatusCode(403);
@@ -760,7 +765,7 @@ class RolesTest extends BrowserKitTest
     {
         // To cover issue fixed in f99c8ff99aee9beb8c692f36d4b84dc6e651e50a.
         $page = Page::first();
-        $viewerRole = \BookStack\Auth\Role::getRole('viewer');
+        $viewerRole = Role::getRole('viewer');
         $viewer = $this->getViewer();
         $this->actingAs($viewer)->visit($page->getUrl())->assertResponseStatus(200);
 
@@ -778,14 +783,14 @@ class RolesTest extends BrowserKitTest
     {
         $admin = $this->getAdmin();
         // Book links
-        $book = factory(\BookStack\Entities\Book::class)->create(['created_by' => $admin->id, 'updated_by' => $admin->id]);
+        $book = factory(Book::class)->create(['created_by' => $admin->id, 'updated_by' => $admin->id]);
         $this->updateEntityPermissions($book);
         $this->actingAs($this->getViewer())->visit($book->getUrl())
             ->dontSee('Create a new page')
             ->dontSee('Add a chapter');
 
         // Chapter links
-        $chapter = factory(\BookStack\Entities\Chapter::class)->create(['created_by' => $admin->id, 'updated_by' => $admin->id, 'book_id' => $book->id]);
+        $chapter = factory(Chapter::class)->create(['created_by' => $admin->id, 'updated_by' => $admin->id, 'book_id' => $book->id]);
         $this->updateEntityPermissions($chapter);
         $this->actingAs($this->getViewer())->visit($chapter->getUrl())
             ->dontSee('Create a new page')
@@ -869,7 +874,7 @@ class RolesTest extends BrowserKitTest
     }
 
     private function addComment($page) {
-        $comment = factory(\BookStack\Actions\Comment::class)->make();
+        $comment = factory(Comment::class)->make();
         $url = "/comment/$page->id";
         $request = [
             'text' => $comment->text,
@@ -882,7 +887,7 @@ class RolesTest extends BrowserKitTest
     }
 
     private function updateComment($commentId) {
-        $comment = factory(\BookStack\Actions\Comment::class)->make();
+        $comment = factory(Comment::class)->make();
         $url = "/comment/$commentId";
         $request = [
             'text' => $comment->text,
index 3670df87d39df58e28ea7383955e3f9db473ab7f..1941901240b5288d628a8264c5bd83a1bef0b074 100644 (file)
@@ -5,9 +5,9 @@ use BookStack\Auth\Permissions\PermissionService;
 use BookStack\Auth\Permissions\RolePermission;
 use BookStack\Auth\Role;
 use BookStack\Auth\User;
-use BookStack\Entities\Book;
-use BookStack\Entities\Chapter;
-use BookStack\Entities\Page;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Models\Page;
 
 class PublicActionTest extends BrowserKitTest
 {
diff --git a/tests/RecycleBinTest.php b/tests/RecycleBinTest.php
new file mode 100644 (file)
index 0000000..60f06cf
--- /dev/null
@@ -0,0 +1,232 @@
+<?php namespace Tests;
+
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Deletion;
+use BookStack\Entities\Models\Page;
+use DB;
+use Illuminate\Support\Carbon;
+
+class RecycleBinTest extends TestCase
+{
+    public function test_recycle_bin_routes_permissions()
+    {
+        $page = Page::query()->first();
+        $editor = $this->getEditor();
+        $this->actingAs($editor)->delete($page->getUrl());
+        $deletion = Deletion::query()->firstOrFail();
+
+        $routes = [
+            'GET:/settings/recycle-bin',
+            'POST:/settings/recycle-bin/empty',
+            "GET:/settings/recycle-bin/{$deletion->id}/destroy",
+            "GET:/settings/recycle-bin/{$deletion->id}/restore",
+            "POST:/settings/recycle-bin/{$deletion->id}/restore",
+            "DELETE:/settings/recycle-bin/{$deletion->id}",
+        ];
+
+        foreach($routes as $route) {
+            [$method, $url] = explode(':', $route);
+            $resp = $this->call($method, $url);
+            $this->assertPermissionError($resp);
+        }
+
+        $this->giveUserPermissions($editor, ['restrictions-manage-all']);
+
+        foreach($routes as $route) {
+            [$method, $url] = explode(':', $route);
+            $resp = $this->call($method, $url);
+            $this->assertPermissionError($resp);
+        }
+
+        $this->giveUserPermissions($editor, ['settings-manage']);
+
+        foreach($routes as $route) {
+            DB::beginTransaction();
+            [$method, $url] = explode(':', $route);
+            $resp = $this->call($method, $url);
+            $this->assertNotPermissionError($resp);
+            DB::rollBack();
+        }
+
+    }
+
+    public function test_recycle_bin_view()
+    {
+        $page = Page::query()->first();
+        $book = Book::query()->whereHas('pages')->whereHas('chapters')->withCount(['pages', 'chapters'])->first();
+        $editor = $this->getEditor();
+        $this->actingAs($editor)->delete($page->getUrl());
+        $this->actingAs($editor)->delete($book->getUrl());
+
+        $viewReq = $this->asAdmin()->get('/settings/recycle-bin');
+        $viewReq->assertElementContains('table.table', $page->name);
+        $viewReq->assertElementContains('table.table', $editor->name);
+        $viewReq->assertElementContains('table.table', $book->name);
+        $viewReq->assertElementContains('table.table', $book->pages_count . ' Pages');
+        $viewReq->assertElementContains('table.table', $book->chapters_count . ' Chapters');
+    }
+
+    public function test_recycle_bin_empty()
+    {
+        $page = Page::query()->first();
+        $book = Book::query()->where('id' , '!=', $page->book_id)->whereHas('pages')->whereHas('chapters')->with(['pages', 'chapters'])->firstOrFail();
+        $editor = $this->getEditor();
+        $this->actingAs($editor)->delete($page->getUrl());
+        $this->actingAs($editor)->delete($book->getUrl());
+
+        $this->assertTrue(Deletion::query()->count() === 2);
+        $emptyReq = $this->asAdmin()->post('/settings/recycle-bin/empty');
+        $emptyReq->assertRedirect('/settings/recycle-bin');
+
+        $this->assertTrue(Deletion::query()->count() === 0);
+        $this->assertDatabaseMissing('books', ['id' => $book->id]);
+        $this->assertDatabaseMissing('pages', ['id' => $page->id]);
+        $this->assertDatabaseMissing('pages', ['id' => $book->pages->first()->id]);
+        $this->assertDatabaseMissing('chapters', ['id' => $book->chapters->first()->id]);
+
+        $itemCount = 2 + $book->pages->count() + $book->chapters->count();
+        $redirectReq = $this->get('/settings/recycle-bin');
+        $redirectReq->assertNotificationContains('Deleted '.$itemCount.' total items from the recycle bin');
+    }
+
+    public function test_entity_restore()
+    {
+        $book = Book::query()->whereHas('pages')->whereHas('chapters')->with(['pages', 'chapters'])->firstOrFail();
+        $this->asEditor()->delete($book->getUrl());
+        $deletion = Deletion::query()->firstOrFail();
+
+        $this->assertEquals($book->pages->count(), DB::table('pages')->where('book_id', '=', $book->id)->whereNotNull('deleted_at')->count());
+        $this->assertEquals($book->chapters->count(), DB::table('chapters')->where('book_id', '=', $book->id)->whereNotNull('deleted_at')->count());
+
+        $restoreReq = $this->asAdmin()->post("/settings/recycle-bin/{$deletion->id}/restore");
+        $restoreReq->assertRedirect('/settings/recycle-bin');
+        $this->assertTrue(Deletion::query()->count() === 0);
+
+        $this->assertEquals($book->pages->count(), DB::table('pages')->where('book_id', '=', $book->id)->whereNull('deleted_at')->count());
+        $this->assertEquals($book->chapters->count(), DB::table('chapters')->where('book_id', '=', $book->id)->whereNull('deleted_at')->count());
+
+        $itemCount = 1 + $book->pages->count() + $book->chapters->count();
+        $redirectReq = $this->get('/settings/recycle-bin');
+        $redirectReq->assertNotificationContains('Restored '.$itemCount.' total items from the recycle bin');
+    }
+
+    public function test_permanent_delete()
+    {
+        $book = Book::query()->whereHas('pages')->whereHas('chapters')->with(['pages', 'chapters'])->firstOrFail();
+        $this->asEditor()->delete($book->getUrl());
+        $deletion = Deletion::query()->firstOrFail();
+
+        $deleteReq = $this->asAdmin()->delete("/settings/recycle-bin/{$deletion->id}");
+        $deleteReq->assertRedirect('/settings/recycle-bin');
+        $this->assertTrue(Deletion::query()->count() === 0);
+
+        $this->assertDatabaseMissing('books', ['id' => $book->id]);
+        $this->assertDatabaseMissing('pages', ['id' => $book->pages->first()->id]);
+        $this->assertDatabaseMissing('chapters', ['id' => $book->chapters->first()->id]);
+
+        $itemCount = 1 + $book->pages->count() + $book->chapters->count();
+        $redirectReq = $this->get('/settings/recycle-bin');
+        $redirectReq->assertNotificationContains('Deleted '.$itemCount.' total items from the recycle bin');
+    }
+
+    public function test_permanent_entity_delete_updates_existing_activity_with_entity_name()
+    {
+        $page = Page::query()->firstOrFail();
+        $this->asEditor()->delete($page->getUrl());
+        $deletion = $page->deletions()->firstOrFail();
+
+        $this->assertDatabaseHas('activities', [
+            'type' => 'page_delete',
+            'entity_id' => $page->id,
+            'entity_type' => $page->getMorphClass(),
+        ]);
+
+        $this->asAdmin()->delete("/settings/recycle-bin/{$deletion->id}");
+
+        $this->assertDatabaseMissing('activities', [
+            'type' => 'page_delete',
+            'entity_id' => $page->id,
+            'entity_type' => $page->getMorphClass(),
+        ]);
+
+        $this->assertDatabaseHas('activities', [
+            'type' => 'page_delete',
+            'entity_id' => null,
+            'entity_type' => null,
+            'detail' => $page->name,
+        ]);
+    }
+
+    public function test_auto_clear_functionality_works()
+    {
+        config()->set('app.recycle_bin_lifetime', 5);
+        $page = Page::query()->firstOrFail();
+        $otherPage = Page::query()->where('id', '!=', $page->id)->firstOrFail();
+
+        $this->asEditor()->delete($page->getUrl());
+        $this->assertDatabaseHas('pages', ['id' => $page->id]);
+        $this->assertEquals(1, Deletion::query()->count());
+
+        Carbon::setTestNow(Carbon::now()->addDays(6));
+        $this->asEditor()->delete($otherPage->getUrl());
+        $this->assertEquals(1, Deletion::query()->count());
+
+        $this->assertDatabaseMissing('pages', ['id' => $page->id]);
+    }
+
+    public function test_auto_clear_functionality_with_negative_time_keeps_forever()
+    {
+        config()->set('app.recycle_bin_lifetime', -1);
+        $page = Page::query()->firstOrFail();
+        $otherPage = Page::query()->where('id', '!=', $page->id)->firstOrFail();
+
+        $this->asEditor()->delete($page->getUrl());
+        $this->assertEquals(1, Deletion::query()->count());
+
+        Carbon::setTestNow(Carbon::now()->addDays(6000));
+        $this->asEditor()->delete($otherPage->getUrl());
+        $this->assertEquals(2, Deletion::query()->count());
+
+        $this->assertDatabaseHas('pages', ['id' => $page->id]);
+    }
+
+    public function test_auto_clear_functionality_with_zero_time_deletes_instantly()
+    {
+        config()->set('app.recycle_bin_lifetime', 0);
+        $page = Page::query()->firstOrFail();
+
+        $this->asEditor()->delete($page->getUrl());
+        $this->assertDatabaseMissing('pages', ['id' => $page->id]);
+        $this->assertEquals(0, Deletion::query()->count());
+    }
+
+    public function test_restore_flow_when_restoring_nested_delete_first()
+    {
+        $book = Book::query()->whereHas('pages')->whereHas('chapters')->with(['pages', 'chapters'])->firstOrFail();
+        $chapter = $book->chapters->first();
+        $this->asEditor()->delete($chapter->getUrl());
+        $this->asEditor()->delete($book->getUrl());
+
+        $bookDeletion = $book->deletions()->first();
+        $chapterDeletion = $chapter->deletions()->first();
+
+        $chapterRestoreView = $this->asAdmin()->get("/settings/recycle-bin/{$chapterDeletion->id}/restore");
+        $chapterRestoreView->assertStatus(200);
+        $chapterRestoreView->assertSeeText($chapter->name);
+
+        $chapterRestore = $this->post("/settings/recycle-bin/{$chapterDeletion->id}/restore");
+        $chapterRestore->assertRedirect("/settings/recycle-bin");
+        $this->assertDatabaseMissing("deletions", ["id" => $chapterDeletion->id]);
+
+        $chapter->refresh();
+        $this->assertNotNull($chapter->deleted_at);
+
+        $bookRestoreView = $this->asAdmin()->get("/settings/recycle-bin/{$bookDeletion->id}/restore");
+        $bookRestoreView->assertStatus(200);
+        $bookRestoreView->assertSeeText($chapter->name);
+
+        $this->post("/settings/recycle-bin/{$bookDeletion->id}/restore");
+        $chapter->refresh();
+        $this->assertNull($chapter->deleted_at);
+    }
+}
\ No newline at end of file
diff --git a/tests/SecurityHeaderTest.php b/tests/SecurityHeaderTest.php
new file mode 100644 (file)
index 0000000..db095ff
--- /dev/null
@@ -0,0 +1,71 @@
+<?php namespace Tests;
+
+
+use Illuminate\Support\Str;
+
+class SecurityHeaderTest extends TestCase
+{
+
+    public function test_cookies_samesite_lax_by_default()
+    {
+        $resp = $this->get("/");
+        foreach ($resp->headers->getCookies() as $cookie) {
+            $this->assertEquals("lax", $cookie->getSameSite());
+        }
+    }
+
+    public function test_cookies_samesite_none_when_iframe_hosts_set()
+    {
+        $this->runWithEnv("ALLOWED_IFRAME_HOSTS", "http://example.com", function() {
+            $resp = $this->get("/");
+            foreach ($resp->headers->getCookies() as $cookie) {
+                $this->assertEquals("none", $cookie->getSameSite());
+            }
+        });
+    }
+
+    public function test_secure_cookies_controlled_by_app_url()
+    {
+        $this->runWithEnv("APP_URL", "http://example.com", function() {
+            $resp = $this->get("/");
+            foreach ($resp->headers->getCookies() as $cookie) {
+                $this->assertFalse($cookie->isSecure());
+            }
+        });
+
+        $this->runWithEnv("APP_URL", "https://example.com", function() {
+            $resp = $this->get("/");
+            foreach ($resp->headers->getCookies() as $cookie) {
+                $this->assertTrue($cookie->isSecure());
+            }
+        });
+    }
+
+    public function test_iframe_csp_self_only_by_default()
+    {
+        $resp = $this->get("/");
+        $cspHeaders = collect($resp->headers->get('Content-Security-Policy'));
+        $frameHeaders = $cspHeaders->filter(function ($val) {
+            return Str::startsWith($val, 'frame-ancestors');
+        });
+
+        $this->assertTrue($frameHeaders->count() === 1);
+        $this->assertEquals('frame-ancestors \'self\'', $frameHeaders->first());
+    }
+
+    public function test_iframe_csp_includes_extra_hosts_if_configured()
+    {
+        $this->runWithEnv("ALLOWED_IFRAME_HOSTS", "https://a.example.com https://b.example.com", function() {
+            $resp = $this->get("/");
+            $cspHeaders = collect($resp->headers->get('Content-Security-Policy'));
+            $frameHeaders = $cspHeaders->filter(function($val) {
+                return Str::startsWith($val, 'frame-ancestors');
+            });
+
+            $this->assertTrue($frameHeaders->count() === 1);
+            $this->assertEquals('frame-ancestors \'self\' https://a.example.com https://b.example.com', $frameHeaders->first());
+        });
+
+    }
+
+}
\ No newline at end of file
index c7659a02dabae0168348553d4f0efd360f5598d9..02f7caae1cb859b4c14d105eab864a78eb4ec0fc 100644 (file)
@@ -1,11 +1,11 @@
 <?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\Models\Book;
+use BookStack\Entities\Models\Bookshelf;
+use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Models\Entity;
+use BookStack\Entities\Models\Page;
 use BookStack\Entities\Repos\BookRepo;
 use BookStack\Entities\Repos\BookshelfRepo;
 use BookStack\Entities\Repos\ChapterRepo;
@@ -15,12 +15,14 @@ use BookStack\Auth\Permissions\PermissionService;
 use BookStack\Entities\Repos\PageRepo;
 use BookStack\Settings\SettingService;
 use BookStack\Uploads\HttpFetcher;
+use Illuminate\Http\Response;
 use Illuminate\Support\Env;
 use Illuminate\Support\Facades\Log;
 use Mockery;
 use Monolog\Handler\TestHandler;
 use Monolog\Logger;
 use Throwable;
+use Illuminate\Foundation\Testing\Assert as PHPUnit;
 
 trait SharedTestHelpers
 {
@@ -177,15 +179,13 @@ trait SharedTestHelpers
 
     /**
      * Give the given user some permissions.
-     * @param User $user
-     * @param array $permissions
      */
-    protected function giveUserPermissions(User $user, $permissions = [])
+    protected function giveUserPermissions(User $user, array $permissions = [])
     {
         $newRole = $this->createNewRole($permissions);
         $user->attachRole($newRole);
         $user->load('roles');
-        $user->permissions(false);
+        $user->clearPermissionCache();
     }
 
     /**
@@ -270,14 +270,25 @@ trait SharedTestHelpers
      */
     protected function assertPermissionError($response)
     {
-        if ($response instanceof BrowserKitTest) {
-            $response = \Illuminate\Foundation\Testing\TestResponse::fromBaseResponse($response->response);
-        }
+        PHPUnit::assertTrue($this->isPermissionError($response->baseResponse ?? $response->response), "Failed asserting the response contains a permission error.");
+    }
+
+    /**
+     * Assert a permission error has occurred.
+     */
+    protected function assertNotPermissionError($response)
+    {
+        PHPUnit::assertFalse($this->isPermissionError($response->baseResponse ?? $response->response), "Failed asserting the response does not contain a permission error.");
+    }
 
-        $response->assertRedirect('/');
-        $this->assertSessionHas('error');
-        $error = session()->pull('error');
-        $this->assertStringStartsWith('You do not have permission to access', $error);
+    /**
+     * Check if the given response is a permission error.
+     */
+    private function isPermissionError($response): bool
+    {
+        return $response->status() === 302
+            && $response->headers->get('Location') === url('/')
+            && strpos(session()->pull('error', ''), 'You do not have permission to access') === 0;
     }
 
     /**
index 1f1d5ece7288e88575b49975848812bac5915173..2c901981af53cb9e22909fdcc89673cb3cf5fece 100644 (file)
@@ -1,6 +1,6 @@
 <?php namespace Tests;
 
-use BookStack\Entities\Entity;
+use BookStack\Entities\Models\Entity;
 use Illuminate\Foundation\Testing\DatabaseTransactions;
 use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
 
@@ -53,9 +53,9 @@ abstract class TestCase extends BaseTestCase
      * Assert that an activity entry exists of the given key.
      * Checks the activity belongs to the given entity if provided.
      */
-    protected function assertActivityExists(string $key, Entity $entity = null)
+    protected function assertActivityExists(string $type, Entity $entity = null)
     {
-        $detailsToCheck = ['key' => $key];
+        $detailsToCheck = ['type' => $type];
 
         if ($entity) {
             $detailsToCheck['entity_type'] = $entity->getMorphClass();
index a68a5783fa044c881bfbf8fa39b66355128ae8be..9c6b78782b4c91bb8576bfb535666559a588d772 100644 (file)
@@ -15,9 +15,8 @@ class TestResponse extends BaseTestResponse {
 
     /**
      * Get the DOM Crawler for the response content.
-     * @return Crawler
      */
-    protected function crawler()
+    protected function crawler(): Crawler
     {
         if (!is_object($this->crawlerInstance)) {
             $this->crawlerInstance = new Crawler($this->getContent());
@@ -27,7 +26,6 @@ class TestResponse extends BaseTestResponse {
 
     /**
      * Assert the response contains the specified element.
-     * @param string $selector
      * @return $this
      */
     public function assertElementExists(string $selector)
@@ -45,7 +43,6 @@ class TestResponse extends BaseTestResponse {
 
     /**
      * Assert the response does not contain the specified element.
-     * @param string $selector
      * @return $this
      */
     public function assertElementNotExists(string $selector)
@@ -63,8 +60,6 @@ class TestResponse extends BaseTestResponse {
 
     /**
      * Assert the response includes a specific element containing the given text.
-     * @param string $selector
-     * @param string $text
      * @return $this
      */
     public function assertElementContains(string $selector, string $text)
@@ -95,8 +90,6 @@ class TestResponse extends BaseTestResponse {
 
     /**
      * Assert the response does not include a specific element containing the given text.
-     * @param string $selector
-     * @param string $text
      * @return $this
      */
     public function assertElementNotContains(string $selector, string $text)
@@ -125,12 +118,20 @@ class TestResponse extends BaseTestResponse {
         return $this;
     }
 
+    /**
+     * Assert there's a notification within the view containing the given text.
+     * @return $this
+     */
+    public function assertNotificationContains(string $text)
+    {
+        return $this->assertElementContains('[notification]', $text);
+    }
+
     /**
      * Get the escaped text pattern for the constraint.
-     * @param  string  $text
      * @return string
      */
-    protected function getEscapedPattern($text)
+    protected function getEscapedPattern(string $text)
     {
         $rawPattern = preg_quote($text, '/');
         $escapedPattern = preg_quote(e($text), '/');
index 5838b019e4affb526e50472bf439285f0c24c865..1ca9ea23b17d5d04101c2203173d394b3455b379 100644 (file)
@@ -1,7 +1,9 @@
 <?php namespace Tests\Uploads;
 
+use BookStack\Entities\Tools\TrashCan;
+use BookStack\Entities\Repos\PageRepo;
 use BookStack\Uploads\Attachment;
-use BookStack\Entities\Page;
+use BookStack\Entities\Models\Page;
 use BookStack\Auth\Permissions\PermissionService;
 use BookStack\Uploads\AttachmentService;
 use Illuminate\Http\UploadedFile;
@@ -213,7 +215,8 @@ class AttachmentTest extends TestCase
             'name' => $fileName
         ]);
 
-        $this->call('DELETE', $page->getUrl());
+        app(PageRepo::class)->destroy($page);
+        app(TrashCan::class)->empty();
 
         $this->assertDatabaseMissing('attachments', [
             'name' => $fileName
index 3fc009c8ab11b7fd58a447d349a74f08dbdfd0a4..d134135aa6e9aed7a6f3fceb0a07d37702252ec0 100644 (file)
@@ -1,6 +1,6 @@
 <?php namespace Tests\Uploads;
 
-use BookStack\Entities\Page;
+use BookStack\Entities\Models\Page;
 use BookStack\Uploads\Image;
 use Tests\TestCase;
 
index 08ac633268c68fd71241069ce24ebb137104c9da..1c736d672d977b8727c8ee5cf08b9f9ee5ba1538 100644 (file)
@@ -2,7 +2,7 @@
 
 use BookStack\Entities\Repos\PageRepo;
 use BookStack\Uploads\Image;
-use BookStack\Entities\Page;
+use BookStack\Entities\Models\Page;
 use BookStack\Uploads\ImageService;
 use Illuminate\Support\Str;
 use Tests\TestCase;
index f5d1032ad1f7e6844f5f379a9db85438193ccc01..64f26dea8a9be7c847909d0d192a780a2ed25f02 100644 (file)
@@ -1,6 +1,6 @@
 <?php namespace Tests\Uploads;
 
-use BookStack\Entities\Page;
+use BookStack\Entities\Models\Page;
 use Illuminate\Http\UploadedFile;
 
 trait UsesImages
index c89a590f0dafc5539fc58884657a8f4c9320a7c8..df686dd77df953423a103d8caa57313539dd832e 100644 (file)
@@ -1,5 +1,6 @@
 <?php namespace Tests\User;
 
+use BookStack\Actions\ActivityType;
 use BookStack\Api\ApiToken;
 use Carbon\Carbon;
 use Tests\TestCase;
@@ -67,6 +68,7 @@ class UserApiTokenTest extends TestCase
         $this->assertTrue(strlen($secret) === 32);
 
         $this->assertSessionHas('success');
+        $this->assertActivityExists(ActivityType::API_TOKEN_CREATE);
     }
 
     public function test_create_with_no_expiry_sets_expiry_hundred_years_away()
@@ -124,6 +126,7 @@ class UserApiTokenTest extends TestCase
 
         $this->assertDatabaseHas('api_tokens', array_merge($updateData, ['id' => $token->id]));
         $this->assertSessionHas('success');
+        $this->assertActivityExists(ActivityType::API_TOKEN_UPDATE);
     }
 
     public function test_token_update_with_blank_expiry_sets_to_hundred_years_away()
@@ -162,6 +165,7 @@ class UserApiTokenTest extends TestCase
         $resp = $this->delete($tokenUrl);
         $resp->assertRedirect($editor->getEditUrl('#api_tokens'));
         $this->assertDatabaseMissing('api_tokens', ['id' => $token->id]);
+        $this->assertActivityExists(ActivityType::API_TOKEN_DELETE);
     }
 
     public function test_user_manage_can_delete_token_without_api_permission_themselves()
diff --git a/tests/User/UserManagementTest.php b/tests/User/UserManagementTest.php
new file mode 100644 (file)
index 0000000..d99d614
--- /dev/null
@@ -0,0 +1,44 @@
+<?php namespace Tests\User;
+
+use BookStack\Actions\ActivityType;
+use BookStack\Auth\User;
+use BookStack\Entities\Models\Page;
+use Tests\TestCase;
+
+class UserManagementTest extends TestCase
+{
+
+    public function test_delete()
+    {
+        $editor = $this->getEditor();
+        $resp = $this->asAdmin()->delete("settings/users/{$editor->id}");
+        $resp->assertRedirect("/settings/users");
+        $resp = $this->followRedirects($resp);
+
+        $resp->assertSee("User successfully removed");
+        $this->assertActivityExists(ActivityType::USER_DELETE);
+
+        $this->assertDatabaseMissing('users', ['id' => $editor->id]);
+    }
+
+    public function test_delete_offers_migrate_option()
+    {
+        $editor = $this->getEditor();
+        $resp = $this->asAdmin()->get("settings/users/{$editor->id}/delete");
+        $resp->assertSee("Migrate Ownership");
+        $resp->assertSee("new_owner_id");
+    }
+
+    public function test_delete_with_new_owner_id_changes_ownership()
+    {
+        $page = Page::query()->first();
+        $owner = $page->ownedBy;
+        $newOwner = User::query()->where('id', '!=' , $owner->id)->first();
+
+        $this->asAdmin()->delete("settings/users/{$owner->id}", ['new_owner_id' => $newOwner->id]);
+        $this->assertDatabaseHas('pages', [
+            'id' => $page->id,
+            'owned_by' => $newOwner->id,
+        ]);
+    }
+}
\ No newline at end of file
index b564ed8c235a55e42d19d06cef4b8f2e199038aa..27d97381e54393a8f469fcce6f47038cd3690dae 100644 (file)
@@ -1,8 +1,9 @@
 <?php namespace Tests\User;
 
 use Activity;
+use BookStack\Actions\ActivityType;
 use BookStack\Auth\User;
-use BookStack\Entities\Bookshelf;
+use BookStack\Entities\Models\Bookshelf;
 use Tests\BrowserKitTest;
 
 class UserProfileTest extends BrowserKitTest
@@ -60,8 +61,8 @@ class UserProfileTest extends BrowserKitTest
         $newUser = $this->getNewBlankUser();
         $this->actingAs($newUser);
         $entities = $this->createEntityChainBelongingToUser($newUser, $newUser);
-        Activity::add($entities['book'], 'book_update', $entities['book']->id);
-        Activity::add($entities['page'], 'page_create', $entities['book']->id);
+        Activity::addForEntity($entities['book'], ActivityType::BOOK_UPDATE);
+        Activity::addForEntity($entities['page'], ActivityType::PAGE_CREATE);
 
         $this->asAdmin()->visit('/user/' . $newUser->id)
             ->seeInElement('#recent-user-activity', 'updated book')
@@ -74,8 +75,8 @@ class UserProfileTest extends BrowserKitTest
         $newUser = $this->getNewBlankUser();
         $this->actingAs($newUser);
         $entities = $this->createEntityChainBelongingToUser($newUser, $newUser);
-        Activity::add($entities['book'], 'book_update', $entities['book']->id);
-        Activity::add($entities['page'], 'page_create', $entities['book']->id);
+        Activity::addForEntity($entities['book'], ActivityType::BOOK_UPDATE);
+        Activity::addForEntity($entities['page'], ActivityType::PAGE_CREATE);
 
         $this->asAdmin()->visit('/')->clickInElement('#recent-activity', $newUser->name)
             ->seePageIs('/user/' . $newUser->id)