]> BookStack Code Mirror - bookstack/commitdiff
Merge branch 'master' of git://github.com/Body4/BookStack into Body4-master
authorDan Brown <redacted>
Sun, 7 Feb 2021 18:33:10 +0000 (18:33 +0000)
committerDan Brown <redacted>
Sun, 7 Feb 2021 18:33:10 +0000 (18:33 +0000)
435 files changed:
.env.example
.env.example.complete
.github/FUNDING.yml [new file with mode: 0644]
.github/ISSUE_TEMPLATE/api_request.md [new file with mode: 0644]
.github/translators.txt
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/Models/Book.php [moved from app/Entities/Book.php with 77% similarity]
app/Entities/Models/BookChild.php [moved from app/Entities/BookChild.php with 83% 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 [moved from app/Entities/Deletion.php with 71% similarity]
app/Entities/Models/Entity.php [moved from app/Entities/Entity.php with 76% 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 69% 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/Markdown/CustomStrikeThroughExtension.php [new file with mode: 0644]
app/Entities/Tools/Markdown/CustomStrikethroughRenderer.php [new file with mode: 0644]
app/Entities/Tools/PageContent.php [moved from app/Entities/Managers/PageContent.php with 89% 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 [moved from app/Entities/Managers/TrashCan.php with 92% similarity]
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
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/StatusController.php [new file with mode: 0644]
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/GlobalViewData.php [deleted file]
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/Settings/SettingService.php
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_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/migrations/2021_01_30_225441_add_settings_type_column.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-create.json
dev/api/responses/books-list.json
dev/api/responses/books-read.json
dev/api/responses/books-update.json
dev/api/responses/chapters-create.json
dev/api/responses/chapters-list.json
dev/api/responses/chapters-read.json
dev/api/responses/chapters-update.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-create.json
dev/api/responses/shelves-list.json
dev/api/responses/shelves-read.json
dev/api/responses/shelves-update.json
docker-compose.yml
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/page-display.js
resources/js/components/page-editor.js
resources/js/components/user-select.js [new file with mode: 0644]
resources/js/components/wysiwyg-editor.js
resources/js/services/code.js
resources/lang/ar/activities.php
resources/lang/ar/auth.php
resources/lang/ar/common.php
resources/lang/ar/components.php
resources/lang/ar/entities.php
resources/lang/ar/errors.php
resources/lang/ar/passwords.php
resources/lang/ar/settings.php
resources/lang/ar/validation.php
resources/lang/bg/activities.php
resources/lang/bg/entities.php
resources/lang/bg/settings.php
resources/lang/bg/validation.php
resources/lang/cs/activities.php
resources/lang/cs/entities.php
resources/lang/cs/settings.php
resources/lang/cs/validation.php
resources/lang/da/activities.php
resources/lang/da/entities.php
resources/lang/da/settings.php
resources/lang/da/validation.php
resources/lang/de/activities.php
resources/lang/de/entities.php
resources/lang/de/settings.php
resources/lang/de/validation.php
resources/lang/de_informal/activities.php
resources/lang/de_informal/entities.php
resources/lang/de_informal/settings.php
resources/lang/de_informal/validation.php
resources/lang/en/activities.php
resources/lang/en/common.php
resources/lang/en/entities.php
resources/lang/en/settings.php
resources/lang/es/activities.php
resources/lang/es/entities.php
resources/lang/es/settings.php
resources/lang/es/validation.php
resources/lang/es_AR/activities.php
resources/lang/es_AR/entities.php
resources/lang/es_AR/settings.php
resources/lang/es_AR/validation.php
resources/lang/fr/activities.php
resources/lang/fr/entities.php
resources/lang/fr/settings.php
resources/lang/fr/validation.php
resources/lang/he/activities.php
resources/lang/he/entities.php
resources/lang/he/settings.php
resources/lang/he/validation.php
resources/lang/hu/activities.php
resources/lang/hu/common.php
resources/lang/hu/components.php
resources/lang/hu/entities.php
resources/lang/hu/settings.php
resources/lang/hu/validation.php
resources/lang/it/activities.php
resources/lang/it/entities.php
resources/lang/it/settings.php
resources/lang/it/validation.php
resources/lang/ja/activities.php
resources/lang/ja/entities.php
resources/lang/ja/settings.php
resources/lang/ja/validation.php
resources/lang/ko/activities.php
resources/lang/ko/common.php
resources/lang/ko/components.php
resources/lang/ko/entities.php
resources/lang/ko/settings.php
resources/lang/ko/validation.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/lang/nl/activities.php
resources/lang/nl/entities.php
resources/lang/nl/settings.php
resources/lang/nl/validation.php
resources/lang/pl/activities.php
resources/lang/pl/entities.php
resources/lang/pl/settings.php
resources/lang/pl/validation.php
resources/lang/pt_BR/activities.php
resources/lang/pt_BR/entities.php
resources/lang/pt_BR/settings.php
resources/lang/pt_BR/validation.php
resources/lang/ru/activities.php
resources/lang/ru/entities.php
resources/lang/ru/settings.php
resources/lang/ru/validation.php
resources/lang/sk/activities.php
resources/lang/sk/auth.php
resources/lang/sk/entities.php
resources/lang/sk/settings.php
resources/lang/sk/validation.php
resources/lang/sl/activities.php
resources/lang/sl/auth.php
resources/lang/sl/common.php
resources/lang/sl/components.php
resources/lang/sl/entities.php
resources/lang/sl/errors.php
resources/lang/sl/pagination.php
resources/lang/sl/passwords.php
resources/lang/sl/settings.php
resources/lang/sl/validation.php
resources/lang/sv/activities.php
resources/lang/sv/auth.php
resources/lang/sv/common.php
resources/lang/sv/components.php
resources/lang/sv/entities.php
resources/lang/sv/passwords.php
resources/lang/sv/settings.php
resources/lang/sv/validation.php
resources/lang/tr/activities.php
resources/lang/tr/entities.php
resources/lang/tr/settings.php
resources/lang/tr/validation.php
resources/lang/uk/activities.php
resources/lang/uk/common.php
resources/lang/uk/components.php
resources/lang/uk/entities.php
resources/lang/uk/errors.php
resources/lang/uk/settings.php
resources/lang/uk/validation.php
resources/lang/vi/activities.php
resources/lang/vi/auth.php
resources/lang/vi/common.php
resources/lang/vi/components.php
resources/lang/vi/entities.php
resources/lang/vi/errors.php
resources/lang/vi/passwords.php
resources/lang/vi/settings.php
resources/lang/vi/validation.php
resources/lang/zh_CN/activities.php
resources/lang/zh_CN/entities.php
resources/lang/zh_CN/settings.php
resources/lang/zh_CN/validation.php
resources/lang/zh_TW/activities.php
resources/lang/zh_TW/entities.php
resources/lang/zh_TW/settings.php
resources/lang/zh_TW/validation.php
resources/sass/_blocks.scss
resources/sass/_colors.scss
resources/sass/_components.scss
resources/sass/_footer.scss [new file with mode: 0644]
resources/sass/_header.scss
resources/sass/_html.scss
resources/sass/_layout.scss
resources/sass/_tables.scss
resources/sass/_text.scss
resources/sass/styles.scss
resources/views/base.blade.php
resources/views/books/export.blade.php
resources/views/books/grid-item.blade.php [deleted file]
resources/views/books/index.blade.php
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/footer.blade.php [new file with mode: 0644]
resources/views/common/header.blade.php
resources/views/common/home-book.blade.php
resources/views/common/home-shelves.blade.php
resources/views/common/home-sidebar.blade.php
resources/views/common/home.blade.php
resources/views/components/expand-toggle.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/form/password.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/book-tree.blade.php
resources/views/partials/breadcrumb-listing.blade.php
resources/views/partials/breadcrumbs.blade.php
resources/views/partials/entity-grid-item.blade.php [new file with mode: 0644]
resources/views/partials/entity-meta.blade.php
resources/views/partials/sort.blade.php
resources/views/partials/view-toggle.blade.php
resources/views/search/book.blade.php [deleted file]
resources/views/settings/audit.blade.php
resources/views/settings/footer-links.blade.php [new file with mode: 0644]
resources/views/settings/index.blade.php
resources/views/settings/maintenance.blade.php
resources/views/settings/navbar.blade.php
resources/views/settings/recycle-bin/destroy.blade.php
resources/views/settings/recycle-bin/index.blade.php
resources/views/settings/recycle-bin/restore.blade.php
resources/views/settings/roles/form.blade.php
resources/views/shelves/grid-item.blade.php [deleted file]
resources/views/shelves/index.blade.php
resources/views/shelves/list-item.blade.php
resources/views/shelves/list.blade.php
resources/views/shelves/show.blade.php
resources/views/users/create.blade.php
resources/views/users/delete.blade.php
resources/views/users/edit.blade.php
resources/views/users/form.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
tests/Entity/ChapterTest.php
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
tests/Entity/SearchOptionsTest.php
tests/Entity/SortTest.php
tests/Entity/TagTest.php
tests/ErrorTest.php
tests/FooterLinksTest.php [new file with mode: 0644]
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
tests/SecurityHeaderTest.php [new file with mode: 0644]
tests/SharedTestHelpers.php
tests/StatusTest.php [new file with mode: 0644]
tests/TestCase.php
tests/Uploads/AttachmentTest.php
tests/Uploads/AvatarTest.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
tests/test-data/bad-php.base64 [new file with mode: 0644]
tests/test-data/bad-phtml-png.base64 [new file with mode: 0644]
tests/test-data/bad-phtml.base64 [new file with mode: 0644]
tests/test-data/bad.php [deleted file]
tests/test-data/bad.phtml [deleted file]
tests/test-data/bad.phtml.png [deleted file]
version

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 750896f3084f4f6fca21fc2c61e24441369c0cb1..9547b0fcdd69b6bcf59e010ee5f700a45f6adbe2 100644 (file)
@@ -274,6 +274,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]
diff --git a/.github/ISSUE_TEMPLATE/api_request.md b/.github/ISSUE_TEMPLATE/api_request.md
new file mode 100644 (file)
index 0000000..dc050ef
--- /dev/null
@@ -0,0 +1,17 @@
+---
+name: New API Endpoint or Feature
+about: Request a new endpoint or API feature be added
+labels: ":nut_and_bolt: API Request"
+---
+
+#### API Endpoint or Feature
+
+Clearly describe what you'd like to have added to the API. 
+
+#### Use-Case
+
+Explain the use-case that you're working-on that requires the above request.
+
+#### Additional Context
+
+If required, add any other context about the feature request here.
\ No newline at end of file
index c74fe394374053fb9dd9fd4e674787f592ffd7eb..9849b4cb27b6f0e4fab16750e4a877a8d14e5d7c 100644 (file)
@@ -123,3 +123,14 @@ Jakub Bouček (jakubboucek) :: Czech
 Marco (cdrfun) :: German
 10935336 :: Chinese Simplified
 孟繁阳 (FanyangMeng) :: Chinese Simplified
+Andrej Močan (andrejm) :: Slovenian
+gilane9_ :: Arabic
+Raed alnahdi (raednahdi) :: Arabic
+Xiphoseer :: German
+MerlinSVK (merlinsvk) :: Slovak
+Kauê Sena (kaue.sena.ks) :: Portuguese, Brazilian
+MatthieuParis :: French
+Douradinho :: Portuguese, Brazilian
+Gaku Yaguchi (tama11) :: Japanese
+johnroyer :: Chinese Traditional
+jackaaa :: Chinese Traditional
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 4e5b1f36575ff74c5439fe1518c372d94ae448f6..b2a35fd2a5115c7ba869a35ddbd34e69287b8e6d 100644 (file)
@@ -2,58 +2,59 @@
 
 use BookStack\Auth\Permissions\PermissionService;
 use BookStack\Auth\User;
-use BookStack\Entities\Entity;
+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\Collection;
 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,
         ]);
     }
 
@@ -62,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;
     }
 
     /**
@@ -95,16 +94,27 @@ 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')
+        $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'])
@@ -155,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 aa75abb72d3e0b8fb006d8600654bd2029c7d04f..51a60d96e20effd49c6eda2ea7f247deb828d6a6 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)
@@ -74,7 +74,12 @@ class ViewService
             $query->whereIn('viewable_type', $this->entityProvider->getMorphClasses($filterModels));
         }
 
-        return $query->with('viewable')->skip($skipCount)->take($count)->get()->pluck('viewable');
+        return $query->with('viewable')
+            ->skip($skipCount)
+            ->take($count)
+            ->get()
+            ->pluck('viewable')
+            ->filter();
     }
 
     /**
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 d9a52c1beb152e900bf89c60df90c0161b97246c..89c8a5fbb25a55fe54fd95cc8d2721c27c6b9334 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;
@@ -74,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 = [])
     {
@@ -111,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)
     {
@@ -168,7 +170,7 @@ class PermissionService
         });
 
         // Chunk through all bookshelves
-        $this->entityProvider->bookshelf->newQuery()->withTrashed()->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);
             });
@@ -181,10 +183,10 @@ class PermissionService
     protected function bookFetchQuery()
     {
         return $this->entityProvider->book->withTrashed()->newQuery()
-            ->select(['id', 'restricted', 'created_by'])->with(['chapters' => function ($query) {
-                $query->withTrashed()->select(['id', 'restricted', 'created_by', 'book_id']);
+            ->select(['id', 'restricted', 'owned_by'])->with(['chapters' => function ($query) {
+                $query->withTrashed()->select(['id', 'restricted', 'owned_by', 'book_id']);
             }, 'pages'  => function ($query) {
-                $query->withTrashed()->select(['id', 'restricted', 'created_by', 'book_id', 'chapter_id']);
+                $query->withTrashed()->select(['id', 'restricted', 'owned_by', 'book_id', 'chapter_id']);
             }]);
     }
 
@@ -230,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)
@@ -286,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);
             });
@@ -325,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)
@@ -406,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)
@@ -492,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
@@ -508,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;
 
@@ -533,7 +533,8 @@ class PermissionService
             $allPermission = $this->currentUser() && $this->currentUser()->can($permission . '-all');
             $ownPermission = $this->currentUser() && $this->currentUser()->can($permission . '-own');
             $this->currentAction = 'view';
-            $isOwner = $this->currentUser() && $this->currentUser()->id === $ownable->created_by;
+            $ownerField = ($ownable instanceof Entity) ? 'owned_by' : 'created_by';
+            $isOwner = $this->currentUser() && $this->currentUser()->id === $ownable->$ownerField;
             return ($allPermission || ($isOwner && $ownPermission));
         }
 
@@ -566,7 +567,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);
                     });
             });
 
@@ -583,7 +584,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
      */
@@ -615,7 +616,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);
                             });
                     });
             });
@@ -639,7 +640,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);
                             });
                     });
             });
@@ -656,7 +657,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);
                 });
         });
     }
@@ -664,7 +665,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
      */
@@ -676,7 +677,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);
                     });
             });
         }
@@ -710,7 +711,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);
                         });
                     });
             });
@@ -746,7 +747,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..9d7eaa72e211a2206a88baa0bcc909bf6eedc9a2 100644 (file)
@@ -1,21 +1,25 @@
 <?php namespace BookStack\Auth;
 
 use BookStack\Api\ApiToken;
+use BookStack\Interfaces\Loggable;
 use BookStack\Model;
 use BookStack\Notifications\ResetPassword;
 use BookStack\Uploads\Image;
 use Carbon\Carbon;
+use Exception;
 use Illuminate\Auth\Authenticatable;
 use Illuminate\Auth\Passwords\CanResetPassword;
 use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
 use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
+use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
 use Illuminate\Database\Eloquent\Relations\BelongsToMany;
 use Illuminate\Database\Eloquent\Relations\HasMany;
 use Illuminate\Notifications\Notifiable;
+use Illuminate\Support\Collection;
 
 /**
  * Class User
- * @package BookStack\Auth
  * @property string $id
  * @property string $name
  * @property string $email
@@ -27,7 +31,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;
 
@@ -43,6 +47,8 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
      */
     protected $fillable = ['name', 'email'];
 
+    protected $casts = ['last_activity_at' => 'datetime'];
+
     /**
      * The attributes excluded from the model's JSON form.
      * @var array
@@ -54,7 +60,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
 
     /**
      * This holds the user's permissions when loaded.
-     * @var array
+     * @var ?Collection
      */
     protected $permissions;
 
@@ -128,35 +134,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;
     }
 
     /**
@@ -169,7 +184,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
 
     /**
      * Get the social account associated with this user.
-     * @return \Illuminate\Database\Eloquent\Relations\HasMany
+     * @return HasMany
      */
     public function socialAccounts()
     {
@@ -206,7 +221,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
 
         try {
             $avatar = $this->avatar ? url($this->avatar->getThumb($size, $size, false)) : $default;
-        } catch (\Exception $err) {
+        } catch (Exception $err) {
             $avatar = $default;
         }
         return $avatar;
@@ -214,7 +229,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
 
     /**
      * Get the avatar for the user.
-     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
+     * @return BelongsTo
      */
     public function avatar()
     {
@@ -229,6 +244,19 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
         return $this->hasMany(ApiToken::class);
     }
 
+    /**
+     * Get the last activity time for this user.
+     */
+    public function scopeWithLastActivityAt(Builder $query)
+    {
+        $query->addSelect(['activities.created_at as last_activity_at'])
+            ->leftJoinSub(function (\Illuminate\Database\Query\Builder $query) {
+                $query->from('activities')->select('user_id')
+                    ->selectRaw('max(created_at) as created_at')
+                    ->groupBy('user_id');
+            }, 'activities', 'users.id', '=', 'activities.user_id');
+    }
+
     /**
      * Get the url for editing this user.
      */
@@ -274,4 +302,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..29a0ebc14aceae23c3e33fcfece504b4897d14b8 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,36 @@ 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'];
+
+        $query = User::query()->select(['*'])
+            ->withLastActivityAt()
+            ->with(['roles', 'avatar'])
+            ->orderBy($sort, $sortData['order']);
 
         if ($sortData['search']) {
             $term = '%' . $sortData['search'] . '%';
@@ -89,14 +90,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 +103,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 +134,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 +152,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 +245,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 24ed5799b062d7f9d2b8431756255d3055583678..762845e9f2bb681d47504c66a72cf8d37d49cc85 100755 (executable)
@@ -52,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'),
 
@@ -59,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',
@@ -124,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 d28afe6f2c13701e87e898726e29af1e7745bdd8..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,27 +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 [string => Entity]
+     * @return array<Entity>
      */
     public function all(): array
     {
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 83%
rename from app/Entities/BookChild.php
rename to app/Entities/Models/BookChild.php
index 6eac4375ddce6c271a06669a8eaa108b774d55e2..8b968cc8b8d8268ae755bbc79ad42bd24419031e 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
 {
 
     /**
@@ -28,11 +31,10 @@ class BookChild extends Entity
 
     /**
      * Get the book this page sits in.
-     * @return BelongsTo
      */
     public function book(): BelongsTo
     {
-        return $this->belongsTo(Book::class);
+        return $this->belongsTo(Book::class)->withTrashed();
     }
 
     /**
@@ -45,9 +47,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();
+    }
+}
similarity index 71%
rename from app/Entities/Deletion.php
rename to app/Entities/Models/Deletion.php
index 576862caa7b7ea8d8124fb724b1c75a49c0d3a5e..1be0ba4c678c17139cf1aa9ca2592f2ca0f692d7 100644 (file)
@@ -1,11 +1,13 @@
-<?php namespace BookStack\Entities;
+<?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
+class Deletion extends Model implements Loggable
 {
 
     /**
@@ -38,4 +40,9 @@ class Deletion extends Model
         return $record;
     }
 
+    public function logDescriptor(): string
+    {
+        $deletable = $this->deletable()->first();
+        return "Deletion ({$this->id}) for {$deletable->getType()} ({$deletable->id}) {$deletable->name}";
+    }
 }
similarity index 76%
rename from app/Entities/Entity.php
rename to app/Entities/Models/Entity.php
index 99922bcebe50047c18563da197091d3b9bca1215..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,8 +6,12 @@ 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;
@@ -32,12 +36,12 @@ use Illuminate\Database\Eloquent\SoftDeletes;
  * @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
@@ -52,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');
     }
@@ -94,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;
         }
 
@@ -128,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');
@@ -139,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;
@@ -166,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');
     }
@@ -176,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;
@@ -219,28 +209,12 @@ class Entity extends Ownable
     }
 
     /**
-     * Get entity type.
-     * @return mixed
+     * Get the entity type as a simple lowercase word.
      */
-    public static function getType()
+    public static function getType(): string
     {
-        return strtolower(static::getClassName());
-    }
-
-    /**
-     * Get an instance of an entity of the given type.
-     * @param $type
-     * @return Entity
-     */
-    public static function getEntityInstance($type)
-    {
-        $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);
     }
 
     /**
@@ -256,36 +230,30 @@ 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 = '/')
-    {
-        return $path;
-    }
+    abstract public function getUrl(string $path = '/'): string;
 
     /**
      * Get the parent entity if existing.
@@ -317,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);
     }
 
     /**
@@ -326,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 69%
rename from app/Entities/Page.php
rename to app/Entities/Models/Page.php
index 8ad05e7aa7393ee1fcb3dafee9f12bc71a437ffa..739927aff6dad8561672a5687d504d18848079ee 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);
@@ -86,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, '/');
-        }
+        $parts = [
+            'books',
+            urlencode($this->getAttribute('bookSlug') ?? $this->book->slug),
+            $this->draft ? 'draft' : 'page',
+            $this->draft ? $this->id : urlencode($this->slug),
+            trim($path, '/'),
+        ];
 
-        return url($url);
+        return url('/' . implode('/', $parts));
     }
 
     /**
@@ -112,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', 'ownedBy']);
+        $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 b0ea7cb87f1bd612c068f591af23ccef8906c793..27d0b407541d02857d8aec46cadbeb71cf7bc840 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)
     {
@@ -36,7 +35,7 @@ class BookRepo
      */
     public function getAllPaginated(int $count = 20, string $sort = 'name', string $order = 'asc'): LengthAwarePaginator
     {
-        return Book::visible()->orderBy($sort, $order)->paginate($count);
+        return Book::visible()->with('cover')->orderBy($sort, $order)->paginate($count);
     }
 
     /**
@@ -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,14 +114,6 @@ 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 Exception
@@ -129,6 +122,8 @@ class BookRepo
     {
         $trashCan = new TrashCan();
         $trashCan->softDestroyBook($book);
+        Activity::addForEntity($book, ActivityType::BOOK_DELETE);
+
         $trashCan->autoClearOld();
     }
 }
index 49fb75f4cfebd47d3cd70701c45a8061027ee009..649f4b0c461c0b461701bd26508af44fe0ab43c2 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)
     {
@@ -29,7 +30,7 @@ class BookshelfRepo
     public function getAllPaginated(int $count = 20, string $sort = 'name', string $order = 'asc'): LengthAwarePaginator
     {
         return Bookshelf::visible()
-            ->with('visibleBooks')
+            ->with(['visibleBooks', 'cover'])
             ->orderBy($sort, $order)
             ->paginate($count);
     }
@@ -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.
      */
@@ -175,6 +170,7 @@ class BookshelfRepo
     {
         $trashCan = new TrashCan();
         $trashCan->softDestroyShelf($shelf);
+        Activity::addForEntity($shelf, ActivityType::BOOKSHELF_DELETE);
         $trashCan->autoClearOld();
     }
 }
index 60599eac8231017fb85fb49cd2c029a6abb1bd53..d56874e0d54a9b647ce59f4a84add65fa793f302 100644 (file)
@@ -1,11 +1,13 @@
 <?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\Facades\Activity;
 use Exception;
 use Illuminate\Support\Collection;
 
@@ -46,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;
     }
 
@@ -55,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
@@ -74,6 +70,7 @@ class ChapterRepo
     {
         $trashCan = new TrashCan();
         $trashCan->softDestroyChapter($chapter);
+        Activity::addForEntity($chapter, ActivityType::CHAPTER_DELETE);
         $trashCan->autoClearOld();
     }
 
@@ -93,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');
@@ -100,6 +98,8 @@ class ChapterRepo
 
         $chapter->changeBook($parent->id);
         $chapter->rebuildPermissions();
+        Activity::addForEntity($chapter, ActivityType::CHAPTER_MOVE);
+
         return $parent;
     }
 }
index 80c8afe9813e873ac835516d9785aa3ba8fa64bc..6a4eaeb1553f834ccafdd3e6b642d160e96d81d3 100644 (file)
@@ -1,17 +1,18 @@
 <?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;
@@ -34,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'));
@@ -129,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,
         ]);
@@ -151,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);
@@ -165,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,47 +177,52 @@ class PageRepo
         // Hold the old details to compare later
         $oldHtml = $page->html;
         $oldName = $page->name;
+        $oldMarkdown = $page->markdown;
 
-        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
         $page->revision_count++;
-
-        if (setting('app-editor') !== 'markdown') {
-            $page->markdown = '';
-        }
-
         $page->save();
 
         // Remove all update drafts for this user & page.
         $this->getUserDraftQuery($page)->delete();
 
         // Save a revision after updating
-        $summary = $input['summary'] ?? null;
-        if ($oldHtml !== $input['html'] || $oldName !== $input['name'] || $summary !== null) {
+        $summary = trim($input['summary'] ?? "");
+        $htmlChanged = isset($input['html']) && $input['html'] !== $oldHtml;
+        $nameChanged = isset($input['name']) && $input['name'] !== $oldName;
+        $markdownChanged = isset($input['markdown']) && $input['markdown'] !== $oldMarkdown;
+        if ($htmlChanged || $nameChanged || $markdownChanged || $summary) {
             $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 (!empty($input['markdown'] ?? '')) {
+            $pageContent->setNewMarkdown($input['markdown']);
+        } else {
+            $pageContent->setNewHTML($input['html']);
+        }
+    }
+
     /**
      * Saves a page revision into the system.
      */
-    protected function savePageRevision(Page $page, string $summary = null)
+    protected function savePageRevision(Page $page, string $summary = null): PageRevision
     {
         $revision = new PageRevision($page->getAttributes());
 
-        if (setting('app-editor') !== 'markdown') {
-            $revision->markdown = '';
-        }
-
         $revision->page_id = $page->id;
         $revision->slug = $page->slug;
         $revision->book_slug = $page->book->slug;
@@ -238,11 +244,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;
         }
@@ -266,6 +271,7 @@ class PageRepo
     {
         $trashCan = new TrashCan();
         $trashCan->softDestroyPage($page);
+        Activity::addForEntity($page, ActivityType::PAGE_DELETE);
         $trashCan->autoClearOld();
     }
 
@@ -275,17 +281,26 @@ class PageRepo
     public function restoreRevision(Page $page, int $revisionId): Page
     {
         $page->revision_count++;
-        $this->savePageRevision($page);
-
         $revision = $page->revisions()->where('id', '=', $revisionId)->first();
+
         $page->fill($revision->toArray());
         $content = new PageContent($page);
-        $content->setNewHTML($revision->html);
+
+        if (!empty($revision->markdown)) {
+            $content->setNewMarkdown($revision->markdown);
+        } else {
+            $content->setNewHTML($revision->html);
+        }
+        
         $page->updated_by = user()->id;
         $page->refreshSlug();
         $page->save();
-
         $page->indexForSearch();
+
+        $summary = trans('entities.pages_revision_restored_from', ['id' => strval($revisionId), 'summary' => $revision->summary]);
+        $this->savePageRevision($page, $summary);
+
+        Activity::addForEntity($page, ActivityType::PAGE_RESTORE);
         return $page;
     }
 
@@ -296,7 +311,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) {
@@ -311,7 +326,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;
     }
 
     /**
@@ -370,14 +386,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.
      */
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')) {
diff --git a/app/Entities/Tools/Markdown/CustomStrikeThroughExtension.php b/app/Entities/Tools/Markdown/CustomStrikeThroughExtension.php
new file mode 100644 (file)
index 0000000..cb8b0ff
--- /dev/null
@@ -0,0 +1,16 @@
+<?php namespace BookStack\Entities\Tools\Markdown;
+
+use League\CommonMark\ConfigurableEnvironmentInterface;
+use League\CommonMark\Extension\ExtensionInterface;
+use League\CommonMark\Extension\Strikethrough\Strikethrough;
+use League\CommonMark\Extension\Strikethrough\StrikethroughDelimiterProcessor;
+
+class CustomStrikeThroughExtension implements ExtensionInterface
+{
+
+    public function register(ConfigurableEnvironmentInterface $environment)
+    {
+        $environment->addDelimiterProcessor(new StrikethroughDelimiterProcessor());
+        $environment->addInlineRenderer(Strikethrough::class, new CustomStrikethroughRenderer());
+    }
+}
\ No newline at end of file
diff --git a/app/Entities/Tools/Markdown/CustomStrikethroughRenderer.php b/app/Entities/Tools/Markdown/CustomStrikethroughRenderer.php
new file mode 100644 (file)
index 0000000..4371fb8
--- /dev/null
@@ -0,0 +1,24 @@
+<?php namespace BookStack\Entities\Tools\Markdown;
+
+use League\CommonMark\ElementRendererInterface;
+use League\CommonMark\Extension\Strikethrough\Strikethrough;
+use League\CommonMark\HtmlElement;
+use League\CommonMark\Inline\Element\AbstractInline;
+use League\CommonMark\Inline\Renderer\InlineRendererInterface;
+
+/**
+ * This is a somewhat clone of the League\CommonMark\Extension\Strikethrough\StrikethroughRender
+ * class but modified slightly to use <s> HTML tags instead of <del> in order to
+ * match front-end markdown-it rendering.
+ */
+class CustomStrikethroughRenderer implements InlineRendererInterface
+{
+    public function render(AbstractInline $inline, ElementRendererInterface $htmlRenderer)
+    {
+        if (!($inline instanceof Strikethrough)) {
+            throw new \InvalidArgumentException('Incompatible inline type: ' . get_class($inline));
+        }
+
+        return new HtmlElement('s', $inline->getData('attributes', []), $htmlRenderer->renderInlines($inline->children()));
+    }
+}
\ No newline at end of file
similarity index 89%
rename from app/Entities/Managers/PageContent.php
rename to app/Entities/Tools/PageContent.php
index 7338a36b393631289a8f30ce09888d403d52763e..62982f4ad5dbbb3ea2524b83f7ed028da4c3971c 100644 (file)
@@ -1,9 +1,14 @@
-<?php namespace BookStack\Entities\Managers;
+<?php namespace BookStack\Entities\Tools;
 
-use BookStack\Entities\Page;
+use BookStack\Entities\Models\Page;
+use BookStack\Entities\Tools\Markdown\CustomStrikeThroughExtension;
 use DOMDocument;
 use DOMNodeList;
 use DOMXPath;
+use League\CommonMark\CommonMarkConverter;
+use League\CommonMark\Environment;
+use League\CommonMark\Extension\Table\TableExtension;
+use League\CommonMark\Extension\TaskList\TaskListExtension;
 
 class PageContent
 {
@@ -25,6 +30,31 @@ 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
+    {
+        $environment = Environment::createCommonMarkEnvironment();
+        $environment->addExtension(new TableExtension());
+        $environment->addExtension(new TaskListExtension());
+        $environment->addExtension(new CustomStrikeThroughExtension());
+        $converter = new CommonMarkConverter([], $environment);
+        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 7da8192cc783665b024bdc9517c60ab28599df2b..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,105 +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()
-                ->withTrashed()
-                ->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;
similarity index 92%
rename from app/Entities/Managers/TrashCan.php
rename to app/Entities/Tools/TrashCan.php
index 48768ab9375ee81acb1a99e6ce19a60e3de112d2..df98fd318fd89e1b0d82b897cbcd038a97a4e1a3 100644 (file)
@@ -1,13 +1,13 @@
-<?php namespace BookStack\Entities\Managers;
+<?php namespace BookStack\Entities\Tools;
 
-use BookStack\Entities\Book;
-use BookStack\Entities\Bookshelf;
-use BookStack\Entities\Chapter;
-use BookStack\Entities\Deletion;
-use BookStack\Entities\Entity;
+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\HasCoverImage;
-use BookStack\Entities\Page;
+use BookStack\Entities\Models\HasCoverImage;
+use BookStack\Entities\Models\Page;
 use BookStack\Exceptions\NotifyException;
 use BookStack\Facades\Activity;
 use BookStack\Uploads\AttachmentService;
@@ -168,11 +168,10 @@ class TrashCan
      */
     public function getTrashedCounts(): array
     {
-        $provider = app(EntityProvider::class);
         $counts = [];
 
         /** @var Entity $instance */
-        foreach ($provider->all() as $key => $instance) {
+        foreach ((new EntityProvider)->all() as $key => $instance) {
             $counts[$key] = $instance->newQuery()->onlyTrashed()->count();
         }
 
@@ -274,11 +273,11 @@ class TrashCan
             $count++;
         };
 
-        if ($entity->isA('chapter') || $entity->isA('book')) {
+        if ($entity instanceof Chapter || $entity instanceof Book) {
             $entity->pages()->withTrashed()->withCount('deletions')->get()->each($restoreAction);
         }
 
-        if ($entity->isA('book')) {
+        if ($entity instanceof Book) {
             $entity->chapters()->withTrashed()->withCount('deletions')->get()->each($restoreAction);
         }
 
@@ -287,19 +286,20 @@ class TrashCan
 
     /**
      * Destroy the given entity.
+     * @throws Exception
      */
     protected function destroyEntity(Entity $entity): int
     {
-        if ($entity->isA('page')) {
+        if ($entity instanceof Page) {
             return $this->destroyPage($entity);
         }
-        if ($entity->isA('chapter')) {
+        if ($entity instanceof Chapter) {
             return $this->destroyChapter($entity);
         }
-        if ($entity->isA('book')) {
+        if ($entity instanceof Book) {
             return $this->destroyBook($entity);
         }
-        if ($entity->isA('shelf')) {
+        if ($entity instanceof Bookshelf) {
             return $this->destroyShelf($entity);
         }
     }
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..1b9bddbb166321d33c7ae4c65dd931f46141365b 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;
@@ -41,7 +37,7 @@ class BookApiController extends ApiController
     {
         $books = Book::visible();
         return $this->apiListingResponse($books, [
-            'id', 'name', 'slug', 'description', 'created_at', 'updated_at', 'created_by', 'updated_by', 'image_id',
+            'id', 'name', 'slug', 'description', 'created_at', 'updated_at', 'created_by', 'updated_by', 'owned_by', 'image_id',
         ]);
     }
 
@@ -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);
     }
 
@@ -65,7 +59,7 @@ class BookApiController extends ApiController
      */
     public function read(string $id)
     {
-        $book = Book::visible()->with(['tags', 'cover', 'createdBy', 'updatedBy'])->findOrFail($id);
+        $book = Book::visible()->with(['tags', 'cover', 'createdBy', 'updatedBy', 'ownedBy'])->findOrFail($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..57461fce55c9ce71e38d058524d5ec87fbe00ac5 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)
     {
@@ -45,7 +43,7 @@ class BookshelfApiController extends ApiController
     {
         $shelves = Bookshelf::visible();
         return $this->apiListingResponse($shelves, [
-            'id', 'name', 'slug', 'description', 'created_at', 'updated_at', 'created_by', 'updated_by', 'image_id',
+            'id', 'name', 'slug', 'description', 'created_at', 'updated_at', 'created_by', 'updated_by', 'owned_by', 'image_id',
         ]);
     }
 
@@ -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);
     }
 
@@ -73,7 +70,7 @@ class BookshelfApiController extends ApiController
     public function read(string $id)
     {
         $shelf = Bookshelf::visible()->with([
-            'tags', 'cover', 'createdBy', 'updatedBy',
+            'tags', 'cover', 'createdBy', 'updatedBy', 'ownedBy',
             'books' => function (BelongsToMany $query) {
                 $query->visible()->get(['id', 'name', 'slug']);
             }
@@ -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..e58c1c8e147dd6b8549ef10135fb9c1fa7c17730 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;
@@ -42,7 +43,7 @@ class ChapterApiController extends ApiController
         $chapters = Chapter::visible();
         return $this->apiListingResponse($chapters, [
             'id', 'book_id', 'name', 'slug', 'description', 'priority',
-            'created_at', 'updated_at', 'created_by', 'updated_by',
+            'created_at', 'updated_at', 'created_by', 'updated_by', 'owned_by',
         ]);
     }
 
@@ -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']));
     }
 
@@ -68,7 +67,7 @@ class ChapterApiController extends ApiController
      */
     public function read(string $id)
     {
-        $chapter = Chapter::visible()->with(['tags', 'createdBy', 'updatedBy', 'pages' => function (HasMany $query) {
+        $chapter = Chapter::visible()->with(['tags', 'createdBy', 'updatedBy', 'ownedBy', 'pages' => function (HasMany $query) {
             $query->visible()->get(['id', 'name', 'slug']);
         }])->findOrFail($id);
         return response()->json($chapter);
@@ -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..a6db058
--- /dev/null
@@ -0,0 +1,141 @@
+<?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', 'owned_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 fad4e8d38a0db99ef3802ea64c2500529b3a4713..eb6eecc944ec0ba29567b2147b5d88af4f39d437 100644 (file)
@@ -32,7 +32,7 @@ class AuditLogController extends Controller
             ->orderBy($listDetails['sort'], $listDetails['order']);
 
         if ($listDetails['event']) {
-            $query->where('key', '=', $listDetails['event']);
+            $query->where('type', '=', $listDetails['event']);
         }
 
         if ($listDetails['date_from']) {
@@ -45,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 25dc651945363e38140609d54772f78957f684c0..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());
     }
 
@@ -187,7 +181,6 @@ class BookController extends Controller
         $book = $this->bookRepo->getBySlug($bookSlug);
         $this->checkOwnablePermission('book-delete', $book);
 
-        Activity::add($book, 'book_delete', $book->id);
         $this->bookRepo->destroy($book);
 
         return redirect('/books');
@@ -210,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 efe280235bad18e75249b7a58ac50ff25a205d62..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::add($shelf, 'bookshelf_delete');
         $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 5d8631154c3fddfebb9fbe3b44b09d65f8587558..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::add($chapter, 'chapter_delete', $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 3b8b7c6e2e06dc6d6ccd711e8a3a57d368965852..3258f43693d5600a26985caa5de675ab04c6f8a5 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;
@@ -25,6 +25,7 @@ class HomeController extends Controller
                 ->where('draft', '=', true)
                 ->where('created_by', '=', user()->id)
                 ->orderBy('updated_at', 'desc')
+                ->with('book')
                 ->take(6)
                 ->get();
         }
@@ -33,8 +34,11 @@ class HomeController extends Controller
         $recents = $this->isSignedIn() ?
               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');
@@ -106,15 +110,16 @@ class HomeController extends Controller
 
     /**
      * Show the view for /robots.txt
-     * @return $this
      */
     public function getRobots()
     {
         $sitePublic = setting('app-public', false);
         $allowRobots = config('app.allow_robots');
+
         if ($allowRobots === null) {
             $allowRobots = $sitePublic;
         }
+
         return response()
             ->view('common.robots', ['allowRobots' => $allowRobots])
             ->header('Content-Type', 'text/plain');
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 0d6265f903092583fcb5219ecc0f76b14e8f1abd..3354a148cfd1f08a628b0f30c0f15d7a4f15b21b 100644 (file)
@@ -2,7 +2,8 @@
 
 namespace BookStack\Http\Controllers;
 
-use BookStack\Entities\Managers\TrashCan;
+use BookStack\Actions\ActivityType;
+use BookStack\Entities\Tools\TrashCan;
 use BookStack\Notifications\TestEmail;
 use BookStack\Uploads\ImageService;
 use Illuminate\Http\Request;
@@ -35,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'));
@@ -61,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 6396da23ed15b7e414221065766b489e6a689135..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();
     }
 
     /**
@@ -107,7 +106,6 @@ class PageController extends Controller
         $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,11 +301,9 @@ 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::add($page, 'page_delete', $page->book_id);
 
         return redirect($parent->getUrl());
     }
@@ -393,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());
     }
@@ -438,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());
     }
@@ -462,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();
     }
 
     /**
index 459dbb39dfd93585ed3f5b2b6913bc814bd8bc22..a644a2889ca28f1b0b2d648798050a27963696ae 100644 (file)
@@ -1,7 +1,8 @@
 <?php namespace BookStack\Http\Controllers;
 
-use BookStack\Entities\Deletion;
-use BookStack\Entities\Managers\TrashCan;
+use BookStack\Actions\ActivityType;
+use BookStack\Entities\Models\Deletion;
+use BookStack\Entities\Tools\TrashCan;
 
 class RecycleBinController extends Controller
 {
@@ -19,7 +20,6 @@ class RecycleBinController extends Controller
             $this->checkPermission('restrictions-manage-all');
             return $next($request);
         });
-        parent::__construct();
     }
 
 
@@ -30,6 +30,7 @@ class RecycleBinController extends Controller
     {
         $deletions = Deletion::query()->with(['deletable', 'deleter'])->paginate(10);
 
+        $this->setPageTitle(trans('settings.recycle_bin'));
         return view('settings.recycle-bin.index', [
             'deletions' => $deletions,
         ]);
@@ -56,6 +57,7 @@ class RecycleBinController extends Controller
     {
         /** @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]));
@@ -83,6 +85,7 @@ class RecycleBinController extends Controller
     {
         /** @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]));
@@ -97,6 +100,7 @@ class RecycleBinController extends Controller
     {
         $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, '#'));
     }
 }
diff --git a/app/Http/Controllers/StatusController.php b/app/Http/Controllers/StatusController.php
new file mode 100644 (file)
index 0000000..9f4ed4d
--- /dev/null
@@ -0,0 +1,47 @@
+<?php namespace BookStack\Http\Controllers;
+
+use Illuminate\Support\Facades\Cache;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Session;
+use Illuminate\Support\Str;
+
+class StatusController extends Controller
+{
+
+    /**
+     * Show the system status as a simple json page.
+     */
+    public function show()
+    {
+        $statuses = [
+            'database' => $this->trueWithoutError(function () {
+                return DB::table('migrations')->count() > 0;
+            }),
+            'cache' => $this->trueWithoutError(function () {
+                $rand = Str::random();
+                Cache::set('status_test', $rand);
+                return Cache::get('status_test') === $rand;
+            }),
+            'session' => $this->trueWithoutError(function () {
+                $rand = Str::random();
+                Session::put('status_test', $rand);
+                return Session::get('status_test') === $rand;
+            }),
+        ];
+
+        $hasError = in_array(false, $statuses);
+        return response()->json($statuses, $hasError ? 500 : 200);
+    }
+
+    /**
+     * Check the callable passed returns true and does not throw an exception.
+     */
+    protected function trueWithoutError(callable $test): bool
+    {
+        try {
+            return $test() === true;
+        } catch (\Exception $e) {
+            return false;
+        }
+    }
+}
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..92e1cd8b7766864527f71641efd78cf4e1c6ec39 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();
     }
 
     /**
@@ -41,6 +41,7 @@ class UserController extends Controller
             'sort' => $request->get('sort', 'name'),
         ];
         $users = $this->userRepo->getAllUsersPaginatedAndSorted(20, $listDetails);
+
         $this->setPageTitle(trans('settings.users'));
         $users->appends($listDetails);
         return view('users.index', ['users' => $users, 'listDetails' => $listDetails]);
@@ -102,6 +103,7 @@ class UserController extends Controller
 
         $this->userRepo->downloadAndAssignUserAvatar($user);
 
+        $this->logActivity(ActivityType::USER_CREATE, $user);
         return redirect('/settings/users');
     }
 
@@ -187,13 +189,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 +218,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 +236,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..075c98ec77b7509087d1f66a2981d7ed26a86328 100644 (file)
@@ -22,13 +22,13 @@ 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,
             \Illuminate\View\Middleware\ShareErrorsFromSession::class,
             \BookStack\Http\Middleware\VerifyCsrfToken::class,
             \BookStack\Http\Middleware\Localization::class,
-            \BookStack\Http\Middleware\GlobalViewData::class,
         ],
         'api' => [
             \BookStack\Http\Middleware\ThrottleApiRequests::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;
+    }
+}
diff --git a/app/Http/Middleware/GlobalViewData.php b/app/Http/Middleware/GlobalViewData.php
deleted file mode 100644 (file)
index bc132df..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php namespace BookStack\Http\Middleware;
-
-use Closure;
-use Illuminate\Http\Request;
-
-/**
- * Class GlobalViewData
- * Sets up data that is accessible to any view rendered by the web routes.
- */
-class GlobalViewData
-{
-
-    /**
-     * Handle an incoming request.
-     *
-     * @param Request $request
-     * @param Closure $next
-     * @return mixed
-     */
-    public function handle(Request $request, Closure $next)
-    {
-        view()->share('signedIn', auth()->check());
-        view()->share('currentUser', user());
-
-        return $next($request);
-    }
-}
index c0ac7a7c4ec781ccfc1939493756c695e4f7ad6d..597d2836548286ac8afb20f5677119f5b8ca2be3 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',
@@ -58,12 +57,7 @@ class Localization
         $defaultLang = config('app.locale');
         config()->set('app.default_locale', $defaultLang);
 
-        if (user()->isDefault() && config('app.auto_detect_locale')) {
-            $locale = $this->autoDetectLocale($request, $defaultLang);
-        } else {
-            $locale = setting()->getUser(user(), 'language', $defaultLang);
-        }
-
+        $locale = $this->getUserLocale($request, $defaultLang);
         config()->set('app.lang', str_replace('_', '-', $this->getLocaleIso($locale)));
 
         // Set text direction
@@ -77,14 +71,29 @@ class Localization
         return $next($request);
     }
 
+    /**
+     * Get the locale specifically for the currently logged in user if available.
+     */
+    protected function getUserLocale(Request $request, string $default): string
+    {
+        try {
+            $user = user();
+        } catch (\Exception $exception) {
+            return $default;
+        }
+
+        if ($user->isDefault() && config('app.auto_detect_locale')) {
+            return $this->autoDetectLocale($request, $default);
+        }
+
+        return setting()->getUser($user, 'language', $default);
+    }
+
     /**
      * Autodetect the visitors locale by matching locales in their headers
      * against the locales supported by BookStack.
-     * @param Request $request
-     * @param string $default
-     * @return string
      */
-    protected function autoDetectLocale(Request $request, string $default)
+    protected function autoDetectLocale(Request $request, string $default): string
     {
         $availableLocales = config('app.locales');
         foreach ($request->getLanguages() as $lang) {
@@ -97,10 +106,8 @@ class Localization
 
     /**
      * Get the ISO version of a BookStack language name
-     * @param  string $locale
-     * @return string
      */
-    public function getLocaleIso(string $locale)
+    public function getLocaleIso(string $locale): string
     {
         return $this->localeMap[$locale] ?? $locale;
     }
@@ -108,7 +115,6 @@ class Localization
     /**
      * Set the system date locale for localized date formatting.
      * Will try both the standard locale name and the UTF8 variant.
-     * @param string $locale
      */
     protected function setSystemDateLocale(string $locale)
     {
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..7673050f8f51bd8d1105ac3a4c6f5e79f7a25885 100644 (file)
@@ -1,19 +1,19 @@
 <?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\Contracts\Cache\Repository;
 use Illuminate\Database\Eloquent\Relations\Relation;
 use Illuminate\Support\Facades\View;
 use Illuminate\Support\ServiceProvider;
 use Schema;
 use URL;
-use Validator;
 
 class AppServiceProvider extends ServiceProvider
 {
@@ -32,37 +32,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);
 
@@ -86,7 +60,7 @@ class AppServiceProvider extends ServiceProvider
     public function register()
     {
         $this->app->singleton(SettingService::class, function ($app) {
-            return new SettingService($app->make(Setting::class), $app->make('Illuminate\Contracts\Cache\Repository'));
+            return new SettingService($app->make(Setting::class), $app->make(Repository::class));
         });
     }
 }
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;
+        });
+    }
+}
index 1c053b3848ea779d480adea01834dce0059d629d..042ae7aa4bf8e4aa86b5e02a50951c0574d23431 100644 (file)
@@ -1,5 +1,6 @@
 <?php namespace BookStack\Settings;
 
+use BookStack\Auth\User;
 use Illuminate\Contracts\Cache\Repository as Cache;
 
 /**
@@ -9,7 +10,6 @@ use Illuminate\Contracts\Cache\Repository as Cache;
  */
 class SettingService
 {
-
     protected $setting;
     protected $cache;
     protected $localCache = [];
@@ -18,8 +18,6 @@ class SettingService
 
     /**
      * SettingService constructor.
-     * @param Setting $setting
-     * @param Cache   $cache
      */
     public function __construct(Setting $setting, Cache $cache)
     {
@@ -30,11 +28,8 @@ class SettingService
     /**
      * Gets a setting from the database,
      * If not found, Returns default, Which is false by default.
-     * @param             $key
-     * @param string|bool $default
-     * @return bool|string
      */
-    public function get($key, $default = false)
+    public function get(string $key, $default = false)
     {
         if ($default === false) {
             $default = config('setting-defaults.' . $key, false);
@@ -44,7 +39,7 @@ class SettingService
             return $this->localCache[$key];
         }
 
-        $value = $this->getValueFromStore($key, $default);
+        $value = $this->getValueFromStore($key) ?? $default;
         $formatted = $this->formatValue($value, $default);
         $this->localCache[$key] = $formatted;
         return $formatted;
@@ -52,25 +47,17 @@ class SettingService
 
     /**
      * Get a value from the session instead of the main store option.
-     * @param $key
-     * @param bool $default
-     * @return mixed
      */
-    protected function getFromSession($key, $default = false)
+    protected function getFromSession(string $key, $default = false)
     {
         $value = session()->get($key, $default);
-        $formatted = $this->formatValue($value, $default);
-        return $formatted;
+        return $this->formatValue($value, $default);
     }
 
     /**
      * Get a user-specific setting from the database or cache.
-     * @param \BookStack\Auth\User $user
-     * @param $key
-     * @param bool $default
-     * @return bool|string
      */
-    public function getUser($user, $key, $default = false)
+    public function getUser(User $user, string $key, $default = false)
     {
         if ($user->isDefault()) {
             return $this->getFromSession($key, $default);
@@ -80,11 +67,8 @@ class SettingService
 
     /**
      * Get a value for the current logged-in user.
-     * @param $key
-     * @param bool $default
-     * @return bool|string
      */
-    public function getForCurrentUser($key, $default = false)
+    public function getForCurrentUser(string $key, $default = false)
     {
         return $this->getUser(user(), $key, $default);
     }
@@ -92,11 +76,9 @@ class SettingService
     /**
      * Gets a setting value from the cache or database.
      * Looks at the system defaults if not cached or in database.
-     * @param $key
-     * @param $default
-     * @return mixed
+     * Returns null if nothing is found.
      */
-    protected function getValueFromStore($key, $default)
+    protected function getValueFromStore(string $key)
     {
         // Check the cache
         $cacheKey = $this->cachePrefix . $key;
@@ -109,18 +91,22 @@ class SettingService
         $settingObject = $this->getSettingObjectByKey($key);
         if ($settingObject !== null) {
             $value = $settingObject->value;
+
+            if ($settingObject->type === 'array') {
+                $value = json_decode($value, true) ?? [];
+            }
+
             $this->cache->forever($cacheKey, $value);
             return $value;
         }
 
-        return $default;
+        return null;
     }
 
     /**
      * Clear an item from the cache completely.
-     * @param $key
      */
-    protected function clearFromCache($key)
+    protected function clearFromCache(string $key)
     {
         $cacheKey = $this->cachePrefix . $key;
         $this->cache->forget($cacheKey);
@@ -131,17 +117,13 @@ class SettingService
 
     /**
      * Format a settings value
-     * @param $value
-     * @param $default
-     * @return mixed
      */
     protected function formatValue($value, $default)
     {
         // Change string booleans to actual booleans
         if ($value === 'true') {
             $value = true;
-        }
-        if ($value === 'false') {
+        } else if ($value === 'false') {
             $value = false;
         }
 
@@ -154,99 +136,97 @@ class SettingService
 
     /**
      * Checks if a setting exists.
-     * @param $key
-     * @return bool
      */
-    public function has($key)
+    public function has(string $key): bool
     {
         $setting = $this->getSettingObjectByKey($key);
         return $setting !== null;
     }
 
-    /**
-     * Check if a user setting is in the database.
-     * @param $key
-     * @return bool
-     */
-    public function hasUser($key)
-    {
-        return $this->has($this->userKey($key));
-    }
-
     /**
      * Add a setting to the database.
-     * @param $key
-     * @param $value
-     * @return bool
+     * Values can be an array or a string.
      */
-    public function put($key, $value)
+    public function put(string $key, $value): bool
     {
-        $setting = $this->setting->firstOrNew([
+        $setting = $this->setting->newQuery()->firstOrNew([
             'setting_key' => $key
         ]);
+        $setting->type = 'string';
+
+        if (is_array($value)) {
+            $setting->type = 'array';
+            $value = $this->formatArrayValue($value);
+        }
+
         $setting->value = $value;
         $setting->save();
         $this->clearFromCache($key);
         return true;
     }
 
+    /**
+     * Format an array to be stored as a setting.
+     * Array setting types are expected to be a flat array of child key=>value array items.
+     * This filters out any child items that are empty.
+     */
+    protected function formatArrayValue(array $value): string
+    {
+        $values = collect($value)->values()->filter(function(array $item) {
+            return count(array_filter($item)) > 0;
+        });
+        return json_encode($values);
+    }
+
     /**
      * Put a user-specific setting into the database.
-     * @param \BookStack\Auth\User $user
-     * @param $key
-     * @param $value
-     * @return bool
      */
-    public function putUser($user, $key, $value)
+    public function putUser(User $user, string $key, string $value): bool
     {
         if ($user->isDefault()) {
-            return session()->put($key, $value);
+            session()->put($key, $value);
+            return true;
         }
+
         return $this->put($this->userKey($user->id, $key), $value);
     }
 
     /**
      * Convert a setting key into a user-specific key.
-     * @param $key
-     * @return string
      */
-    protected function userKey($userId, $key = '')
+    protected function userKey(string $userId, string $key = ''): string
     {
         return 'user:' . $userId . ':' . $key;
     }
 
     /**
      * Removes a setting from the database.
-     * @param $key
-     * @return bool
      */
-    public function remove($key)
+    public function remove(string $key): void
     {
         $setting = $this->getSettingObjectByKey($key);
         if ($setting) {
             $setting->delete();
         }
         $this->clearFromCache($key);
-        return true;
     }
 
     /**
      * Delete settings for a given user id.
-     * @param $userId
-     * @return mixed
      */
-    public function deleteUserSettings($userId)
+    public function deleteUserSettings(string $userId)
     {
-        return $this->setting->where('setting_key', 'like', $this->userKey($userId) . '%')->delete();
+        return $this->setting->newQuery()
+            ->where('setting_key', 'like', $this->userKey($userId) . '%')
+            ->delete();
     }
 
     /**
      * Gets a setting model from the database for the given key.
-     * @param $key
-     * @return mixed
      */
-    protected function getSettingObjectByKey($key)
+    protected function getSettingObjectByKey(string $key): ?Setting
     {
-        return $this->setting->where('setting_key', '=', $key)->first();
+        return $this->setting->newQuery()
+            ->where('setting_key', '=', $key)->first();
     }
 }
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..b3b9d59
--- /dev/null
@@ -0,0 +1,101 @@
+<?php namespace BookStack\Uploads;
+
+use BookStack\Auth\User;
+use BookStack\Exceptions\HttpFetchException;
+use Exception;
+use Illuminate\Support\Facades\Log;
+
+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..e6a833291db2db75a38b47a5cc0b2149fd33a78a 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.16",
+        "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": "^v1.0.1"
     },
     "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..0ecd62bb53200f34a5851051138736c3aa685084 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": "fc011a4d14d89014565f53626a0e331c",
     "packages": [
         {
             "name": "aws/aws-sdk-php",
-            "version": "3.154.6",
+            "version": "3.173.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/aws/aws-sdk-php.git",
-                "reference": "83a1382930359e4d4f4c9187239f059d5b282520"
+                "reference": "1400b2d08ded25bd9c6876edb0490b8cb9aeaa60"
             },
             "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/1400b2d08ded25bd9c6876edb0490b8cb9aeaa60",
+                "reference": "1400b2d08ded25bd9c6876edb0490b8cb9aeaa60",
                 "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.173.4"
+            },
+            "time": "2021-02-05T19:19:35+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.25",
             "source": {
                 "type": "git",
                 "url": "https://github.com/egulias/EmailValidator.git",
-                "reference": "f46887bc48db66c7f38f668eb7d6ae54583617ff"
+                "reference": "0dbf5d78455d4d6a41d186da50adc1122ec066f4"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/f46887bc48db66c7f38f668eb7d6ae54583617ff",
-                "reference": "f46887bc48db66c7f38f668eb7d6ae54583617ff",
+                "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/0dbf5d78455d4d6a41d186da50adc1122ec066f4",
+                "reference": "0dbf5d78455d4d6a41d186da50adc1122ec066f4",
                 "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.25"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/egulias",
+                    "type": "github"
+                }
+            ],
+            "time": "2020-12-29T14:50:06+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.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/filp/whoops.git",
-                "reference": "5d5fe9bb3d656b514d455645b3addc5f7ba7714d"
+                "reference": "df7933820090489623ce0be5e85c7e693638e536"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/filp/whoops/zipball/5d5fe9bb3d656b514d455645b3addc5f7ba7714d",
-                "reference": "5d5fe9bb3d656b514d455645b3addc5f7ba7714d",
+                "url": "https://api.github.com/repos/filp/whoops/zipball/df7933820090489623ce0be5e85c7e693638e536",
+                "reference": "df7933820090489623ce0be5e85c7e693638e536",
                 "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.2"
             },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Andrew Cairns",
-                    "email": "[email protected]"
-                },
-                {
-                    "name": "Mathew Chapman",
-                    "email": "[email protected]"
-                },
+            "funding": [
                 {
-                    "name": "Peter Legierski",
-                    "email": "[email protected]"
+                    "url": "https://github.com/denis-sokolov",
+                    "type": "github"
                 }
             ],
-            "description": "Compare two HTML strings",
-            "time": "2015-04-15T15:39:46+00:00"
+            "time": "2021-01-24T12: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.16",
             "source": {
                 "type": "git",
                 "url": "https://github.com/laravel/framework.git",
-                "reference": "e42450df0896b7130ccdb5290a114424e18887c9"
+                "reference": "806082fb559fe595cb17cd6aa8571f03ed287814"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/laravel/framework/zipball/e42450df0896b7130ccdb5290a114424e18887c9",
-                "reference": "e42450df0896b7130ccdb5290a114424e18887c9",
+                "url": "https://api.github.com/repos/laravel/framework/zipball/806082fb559fe595cb17cd6aa8571f03ed287814",
+                "reference": "806082fb559fe595cb17cd6aa8571f03ed287814",
                 "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": "2021-02-02T13:50:12+00:00"
         },
         {
             "name": "laravel/socialite",
-            "version": "v4.4.1",
+            "version": "v5.1.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/laravel/socialite.git",
-                "reference": "80951df0d93435b773aa00efe1fad6d5015fac75"
+                "reference": "2e6beafe911a09f2300353c102d882e9d63f1f72"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/laravel/socialite/zipball/80951df0d93435b773aa00efe1fad6d5015fac75",
-                "reference": "80951df0d93435b773aa00efe1fad6d5015fac75",
+                "url": "https://api.github.com/repos/laravel/socialite/zipball/2e6beafe911a09f2300353c102d882e9d63f1f72",
+                "reference": "2e6beafe911a09f2300353c102d882e9d63f1f72",
                 "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": "2021-01-05T17:02:09+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.7.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/thephpleague/mime-type-detection.git",
+                "reference": "3b9dff8aaf7323590c1d2e443db701eb1f9aa0d3"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/3b9dff8aaf7323590c1d2e443db701eb1f9aa0d3",
+                "reference": "3b9dff8aaf7323590c1d2e443db701eb1f9aa0d3",
+                "shasum": ""
+            },
+            "require": {
+                "ext-fileinfo": "*",
+                "php": "^7.2 || ^8.0"
+            },
+            "require-dev": {
+                "friendsofphp/php-cs-fixer": "^2.18",
+                "phpstan/phpstan": "^0.12.68",
+                "phpunit/phpunit": "^8.5.8 || ^9.3"
+            },
+            "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.7.0"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/frankdejonge",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/league/flysystem",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2021-01-18T20:58:21+00:00"
         },
         {
             "name": "league/oauth1-client",
-            "version": "v1.8.1",
+            "version": "v1.9.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/thephpleague/oauth1-client.git",
-                "reference": "3a68155c3f27a91f4b66a2dc03996cd6f3281c9f"
+                "reference": "1e7e6be2dc543bf466236fb171e5b20e1b06aee6"
             },
             "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/1e7e6be2dc543bf466236fb171e5b20e1b06aee6",
+                "reference": "1e7e6be2dc543bf466236fb171e5b20e1b06aee6",
                 "shasum": ""
             },
             "require": {
                 "ext-json": "*",
                 "ext-openssl": "*",
                 "guzzlehttp/guzzle": "^6.0|^7.0",
-                "php": ">=7.1"
+                "php": ">=7.1||>=8.0"
             },
             "require-dev": {
                 "ext-simplexml": "*",
-                "friendsofphp/php-cs-fixer": "^2.16.1",
-                "mockery/mockery": "^1.3",
+                "friendsofphp/php-cs-fixer": "^2.17",
+                "mockery/mockery": "^1.3.3",
                 "phpstan/phpstan": "^0.12.42",
-                "phpunit/phpunit": "^7.5"
+                "phpunit/phpunit": "^7.5||9.5"
             },
             "suggest": {
                 "ext-simplexml": "For decoding XML-based responses."
                 "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.9.0"
+            },
+            "time": "2021-01-20T01:40:53+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.44.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/briannesbitt/Carbon.git",
-                "reference": "6c7646154181013ecd55e80c201b9fd873c6ee5d"
+                "reference": "e6ef33cb1f67a4bed831ed6d0f7e156739a5d8cd"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/6c7646154181013ecd55e80c201b9fd873c6ee5d",
-                "reference": "6c7646154181013ecd55e80c201b9fd873c6ee5d",
+                "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/e6ef33cb1f67a4bed831ed6d0f7e156739a5d8cd",
+                "reference": "e6ef33cb1f67a4bed831ed6d0f7e156739a5d8cd",
                 "shasum": ""
             },
             "require": {
                 "kylekatarnls/multi-tester": "^2.0",
                 "phpmd/phpmd": "^2.9",
                 "phpstan/extension-installer": "^1.0",
-                "phpstan/phpstan": "^0.12.35",
-                "phpunit/phpunit": "^7.5 || ^8.0",
+                "phpstan/phpstan": "^0.12.54",
+                "phpunit/phpunit": "^7.5.20 || ^8.5.14",
                 "squizlabs/php_codesniffer": "^3.4"
             },
             "bin": [
                 "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": "2021-01-26T20:46:41+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.6",
             "source": {
                 "type": "git",
                 "url": "https://github.com/scrivo/highlight.php.git",
-                "reference": "efb6e445494a9458aa59b0af5edfa4bdcc6809d9"
+                "reference": "44a3d4136edb5ad8551590bf90f437db80b2d466"
             },
             "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/44a3d4136edb5ad8551590bf90f437db80b2d466",
+                "reference": "44a3d4136edb5ad8551590bf90f437db80b2d466",
                 "shasum": ""
             },
             "require": {
                 "symfony/finder": "^2.8|^3.4",
                 "symfony/var-dumper": "^2.8|^3.4"
             },
-            "suggest": {
-                "ext-dom": "Needed to make use of the features in the utilities namespace"
-            },
             "type": "library",
             "autoload": {
                 "psr-0": {
                 "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-12-22T19:20:29+00:00"
         },
         {
             "name": "socialiteproviders/discord",
-            "version": "v2.0.2",
+            "version": "4.1.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/SocialiteProviders/Discord.git",
-                "reference": "e0cd8895f321943b36f533e7bf21ad29bcdece9a"
+                "reference": "c6eddeb07ace7473e82d02d4db852dfacf5ef574"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/SocialiteProviders/Discord/zipball/e0cd8895f321943b36f533e7bf21ad29bcdece9a",
-                "reference": "e0cd8895f321943b36f533e7bf21ad29bcdece9a",
+                "url": "https://api.github.com/repos/SocialiteProviders/Discord/zipball/c6eddeb07ace7473e82d02d4db852dfacf5ef574",
+                "reference": "c6eddeb07ace7473e82d02d4db852dfacf5ef574",
                 "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.1"
+            },
+            "time": "2021-01-05T22:03:58+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.2.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.2.0"
+            },
+            "time": "2020-12-01T23:10:59+00:00"
         },
         {
             "name": "socialiteproviders/okta",
-            "version": "v1.1.0",
+            "version": "4.1.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/SocialiteProviders/Okta.git",
-                "reference": "7c2512f0872316b139e3eea1c50c9351747a57ea"
+                "reference": "e3ef9f23c7d2f86b3b16a174b82333cf4e2459e8"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/SocialiteProviders/Okta/zipball/7c2512f0872316b139e3eea1c50c9351747a57ea",
-                "reference": "7c2512f0872316b139e3eea1c50c9351747a57ea",
+                "url": "https://api.github.com/repos/SocialiteProviders/Okta/zipball/e3ef9f23c7d2f86b3b16a174b82333cf4e2459e8",
+                "reference": "e3ef9f23c7d2f86b3b16a174b82333cf4e2459e8",
                 "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.1"
+            },
+            "time": "2021-01-12T23:51:01+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.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/ssddanbrown/HtmlDiff.git",
+                "reference": "f60d5cc278b60305ab980a6665f46117c5b589c0"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/ssddanbrown/HtmlDiff/zipball/f60d5cc278b60305ab980a6665f46117c5b589c0",
+                "reference": "f60d5cc278b60305ab980a6665f46117c5b589c0",
+                "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.1"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/ssddanbrown",
+                    "type": "github"
+                }
+            ],
+            "time": "2021-01-24T18:51:30+00:00"
         },
         {
             "name": "swiftmailer/swiftmailer",
-            "version": "v6.2.3",
+            "version": "v6.2.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/swiftmailer/swiftmailer.git",
-                "reference": "149cfdf118b169f7840bbe3ef0d4bc795d1780c9"
+                "reference": "698a6a9f54d7eb321274de3ad19863802c879fb7"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/149cfdf118b169f7840bbe3ef0d4bc795d1780c9",
-                "reference": "149cfdf118b169f7840bbe3ef0d4bc795d1780c9",
+                "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/698a6a9f54d7eb321274de3ad19863802c879fb7",
+                "reference": "698a6a9f54d7eb321274de3ad19863802c879fb7",
                 "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.5"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/swiftmailer/swiftmailer",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2021-01-12T09:35:59+00:00"
         },
         {
             "name": "symfony/console",
-            "version": "v4.4.13",
+            "version": "v4.4.19",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/console.git",
-                "reference": "b39fd99b9297b67fb7633b7d8083957a97e1e727"
+                "reference": "24026c44fc37099fa145707fecd43672831b837a"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/console/zipball/b39fd99b9297b67fb7633b7d8083957a97e1e727",
-                "reference": "b39fd99b9297b67fb7633b7d8083957a97e1e727",
+                "url": "https://api.github.com/repos/symfony/console/zipball/24026c44fc37099fa145707fecd43672831b837a",
+                "reference": "24026c44fc37099fa145707fecd43672831b837a",
                 "shasum": ""
             },
             "require": {
                 "symfony/process": ""
             },
             "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "4.4-dev"
-                }
-            },
             "autoload": {
                 "psr-4": {
                     "Symfony\\Component\\Console\\": ""
                     "homepage": "https://symfony.com/contributors"
                 }
             ],
-            "description": "Symfony Console Component",
+            "description": "Eases the creation of beautiful and testable command line interfaces",
             "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/console/tree/v4.4.19"
+            },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-09-02T07:07:21+00:00"
+            "time": "2021-01-27T09:09:26+00:00"
         },
         {
             "name": "symfony/css-selector",
-            "version": "v4.4.13",
+            "version": "v4.4.19",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/css-selector.git",
-                "reference": "bf17dc9f6ce144e41f786c32435feea4d8e11dcc"
+                "reference": "f907d3e53ecb2a5fad8609eb2f30525287a734c8"
             },
             "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/f907d3e53ecb2a5fad8609eb2f30525287a734c8",
+                "reference": "f907d3e53ecb2a5fad8609eb2f30525287a734c8",
                 "shasum": ""
             },
             "require": {
                 "php": ">=7.1.3"
             },
             "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "4.4-dev"
-                }
-            },
             "autoload": {
                 "psr-4": {
                     "Symfony\\Component\\CssSelector\\": ""
                     "homepage": "https://symfony.com/contributors"
                 }
             ],
-            "description": "Symfony CssSelector Component",
+            "description": "Converts CSS selectors to XPath expressions",
             "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/css-selector/tree/v4.4.19"
+            },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-07-05T09:39:30+00:00"
+            "time": "2021-01-27T09:09:26+00:00"
         },
         {
             "name": "symfony/debug",
-            "version": "v4.4.13",
+            "version": "v4.4.19",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/debug.git",
-                "reference": "aeb73aca16a8f1fe958230fe44e6cf4c84cbb85e"
+                "reference": "af4987aa4a5630e9615be9d9c3ed1b0f24ca449c"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/debug/zipball/aeb73aca16a8f1fe958230fe44e6cf4c84cbb85e",
-                "reference": "aeb73aca16a8f1fe958230fe44e6cf4c84cbb85e",
+                "url": "https://api.github.com/repos/symfony/debug/zipball/af4987aa4a5630e9615be9d9c3ed1b0f24ca449c",
+                "reference": "af4987aa4a5630e9615be9d9c3ed1b0f24ca449c",
                 "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\\": ""
                     "homepage": "https://symfony.com/contributors"
                 }
             ],
-            "description": "Symfony Debug Component",
+            "description": "Provides tools to ease debugging PHP code",
+            "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/debug/tree/v4.4.19"
+            },
+            "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": "2021-01-27T09:09: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.19",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/error-handler.git",
-                "reference": "2434fb32851f252e4f27691eee0b77c16198db62"
+                "reference": "d603654eaeb713503bba3e308b9e748e5a6d3f2e"
             },
             "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/d603654eaeb713503bba3e308b9e748e5a6d3f2e",
+                "reference": "d603654eaeb713503bba3e308b9e748e5a6d3f2e",
                 "shasum": ""
             },
             "require": {
                 "symfony/serializer": "^4.4|^5.0"
             },
             "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "4.4-dev"
-                }
-            },
             "autoload": {
                 "psr-4": {
                     "Symfony\\Component\\ErrorHandler\\": ""
                     "homepage": "https://symfony.com/contributors"
                 }
             ],
-            "description": "Symfony ErrorHandler Component",
+            "description": "Provides tools to manage errors and ease debugging PHP code",
             "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/error-handler/tree/v4.4.19"
+            },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-08-17T09:56:45+00:00"
+            "time": "2021-01-27T09:09:26+00:00"
         },
         {
             "name": "symfony/event-dispatcher",
-            "version": "v4.4.13",
+            "version": "v4.4.19",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/event-dispatcher.git",
-                "reference": "3e8ea5ccddd00556b86d69d42f99f1061a704030"
+                "reference": "c352647244bd376bf7d31efbd5401f13f50dad0c"
             },
             "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/c352647244bd376bf7d31efbd5401f13f50dad0c",
+                "reference": "c352647244bd376bf7d31efbd5401f13f50dad0c",
                 "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\\": ""
                     "homepage": "https://symfony.com/contributors"
                 }
             ],
-            "description": "Symfony EventDispatcher Component",
+            "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them",
             "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/event-dispatcher/tree/v4.4.19"
+            },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-08-13T14:18:44+00:00"
+            "time": "2021-01-27T09:09:26+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.19",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/finder.git",
-                "reference": "2a78590b2c7e3de5c429628457c47541c58db9c7"
+                "reference": "25d79cfccfc12e84e7a63a248c3f0720fdd92db6"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/finder/zipball/2a78590b2c7e3de5c429628457c47541c58db9c7",
-                "reference": "2a78590b2c7e3de5c429628457c47541c58db9c7",
+                "url": "https://api.github.com/repos/symfony/finder/zipball/25d79cfccfc12e84e7a63a248c3f0720fdd92db6",
+                "reference": "25d79cfccfc12e84e7a63a248c3f0720fdd92db6",
                 "shasum": ""
             },
             "require": {
                 "php": ">=7.1.3"
             },
             "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "4.4-dev"
-                }
-            },
             "autoload": {
                 "psr-4": {
                     "Symfony\\Component\\Finder\\": ""
                     "homepage": "https://symfony.com/contributors"
                 }
             ],
-            "description": "Symfony Finder Component",
+            "description": "Finds files and directories via an intuitive fluent interface",
+            "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/finder/tree/v4.4.19"
+            },
+            "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": "2021-01-27T09:09:26+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.19",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/http-foundation.git",
-                "reference": "e3e5a62a6631a461954d471e7206e3750dbe8ee1"
+                "reference": "8888741b633f6c3d1e572b7735ad2cae3e03f9c5"
             },
             "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/8888741b633f6c3d1e572b7735ad2cae3e03f9c5",
+                "reference": "8888741b633f6c3d1e572b7735ad2cae3e03f9c5",
                 "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\\": ""
                     "homepage": "https://symfony.com/contributors"
                 }
             ],
-            "description": "Symfony HttpFoundation Component",
+            "description": "Defines an object-oriented layer for the HTTP specification",
             "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/http-foundation/tree/v4.4.19"
+            },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-08-17T07:39:58+00:00"
+            "time": "2021-01-27T09:09:26+00:00"
         },
         {
             "name": "symfony/http-kernel",
-            "version": "v4.4.13",
+            "version": "v4.4.19",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/http-kernel.git",
-                "reference": "2bb7b90ecdc79813c0bf237b7ff20e79062b5188"
+                "reference": "07ea794a327d7c8c5d76e3058fde9fec6a711cb4"
             },
             "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/07ea794a327d7c8c5d76e3058fde9fec6a711cb4",
+                "reference": "07ea794a327d7c8c5d76e3058fde9fec6a711cb4",
                 "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/console": ">=5",
                 "symfony/dependency-injection": "<4.3",
                 "symfony/translation": "<4.2",
-                "twig/twig": "<1.34|<2.4,>=2"
+                "twig/twig": "<1.43|<2.13,>=2"
             },
             "provide": {
                 "psr/log-implementation": "1.0"
                 "symfony/templating": "^3.4|^4.0|^5.0",
                 "symfony/translation": "^4.2|^5.0",
                 "symfony/translation-contracts": "^1.1|^2",
-                "twig/twig": "^1.34|^2.4|^3.0"
+                "twig/twig": "^1.43|^2.13|^3.0.4"
             },
             "suggest": {
                 "symfony/browser-kit": "",
                 "symfony/dependency-injection": ""
             },
             "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "4.4-dev"
-                }
-            },
             "autoload": {
                 "psr-4": {
                     "Symfony\\Component\\HttpKernel\\": ""
                     "homepage": "https://symfony.com/contributors"
                 }
             ],
-            "description": "Symfony HttpKernel Component",
+            "description": "Provides a structured process for converting a Request into a Response",
             "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/http-kernel/tree/v4.4.19"
+            },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-09-02T08:09:29+00:00"
+            "time": "2021-01-27T13:50:53+00:00"
         },
         {
             "name": "symfony/mime",
-            "version": "v4.4.13",
+            "version": "v5.2.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/mime.git",
-                "reference": "50ad671306d3d3ffb888d95b4fb1859496831e3a"
+                "reference": "7dee6a43493f39b51ff6c5bb2bd576fe40a76c86"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/mime/zipball/50ad671306d3d3ffb888d95b4fb1859496831e3a",
-                "reference": "50ad671306d3d3ffb888d95b4fb1859496831e3a",
+                "url": "https://api.github.com/repos/symfony/mime/zipball/7dee6a43493f39b51ff6c5bb2bd576fe40a76c86",
+                "reference": "7dee6a43493f39b51ff6c5bb2bd576fe40a76c86",
                 "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": {
+                "phpdocumentor/reflection-docblock": "<3.2.2",
+                "phpdocumentor/type-resolver": "<1.4.0",
                 "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\\": ""
                     "homepage": "https://symfony.com/contributors"
                 }
             ],
-            "description": "A library to manipulate MIME messages",
+            "description": "Allows manipulating MIME messages",
             "homepage": "https://symfony.com",
             "keywords": [
                 "mime",
                 "mime-type"
             ],
+            "support": {
+                "source": "https://github.com/symfony/mime/tree/v5.2.3"
+            },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-08-17T09:56:45+00:00"
+            "time": "2021-02-02T06:10:15+00:00"
         },
         {
             "name": "symfony/polyfill-ctype",
-            "version": "v1.18.1",
+            "version": "v1.22.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-ctype.git",
-                "reference": "1c302646f6efc070cd46856e600e5e0684d6b454"
+                "reference": "c6c942b1ac76c82448322025e084cadc56048b4e"
             },
             "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/c6c942b1ac76c82448322025e084cadc56048b4e",
+                "reference": "c6c942b1ac76c82448322025e084cadc56048b4e",
                 "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.22-dev"
                 },
                 "thanks": {
                     "name": "symfony/polyfill",
                 "polyfill",
                 "portable"
             ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-ctype/tree/v1.22.0"
+            },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-07-14T12:35:20+00:00"
+            "time": "2021-01-07T16:49:33+00:00"
         },
         {
             "name": "symfony/polyfill-iconv",
-            "version": "v1.18.1",
+            "version": "v1.22.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-iconv.git",
-                "reference": "6c2f78eb8f5ab8eaea98f6d414a5915f2e0fce36"
+                "reference": "b34bfb8c4c22650ac080d2662ae3502e5f2f4ae6"
             },
             "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/b34bfb8c4c22650ac080d2662ae3502e5f2f4ae6",
+                "reference": "b34bfb8c4c22650ac080d2662ae3502e5f2f4ae6",
                 "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.22-dev"
                 },
                 "thanks": {
                     "name": "symfony/polyfill",
                 "portable",
                 "shim"
             ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-iconv/tree/v1.22.0"
+            },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-07-14T12:35:20+00:00"
+            "time": "2021-01-07T16:49:33+00:00"
         },
         {
             "name": "symfony/polyfill-intl-idn",
-            "version": "v1.18.1",
+            "version": "v1.22.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-intl-idn.git",
-                "reference": "5dcab1bc7146cf8c1beaa4502a3d9be344334251"
+                "reference": "0eb8293dbbcd6ef6bf81404c9ce7d95bcdf34f44"
             },
             "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/0eb8293dbbcd6ef6bf81404c9ce7d95bcdf34f44",
+                "reference": "0eb8293dbbcd6ef6bf81404c9ce7d95bcdf34f44",
                 "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.22-dev"
                 },
                 "thanks": {
                     "name": "symfony/polyfill",
                 "portable",
                 "shim"
             ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.22.0"
+            },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-08-04T06:02:08+00:00"
+            "time": "2021-01-07T16:49:33+00:00"
         },
         {
             "name": "symfony/polyfill-intl-normalizer",
-            "version": "v1.18.1",
+            "version": "v1.22.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-intl-normalizer.git",
-                "reference": "37078a8dd4a2a1e9ab0231af7c6cb671b2ed5a7e"
+                "reference": "6e971c891537eb617a00bb07a43d182a6915faba"
             },
             "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/6e971c891537eb617a00bb07a43d182a6915faba",
+                "reference": "6e971c891537eb617a00bb07a43d182a6915faba",
                 "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.22-dev"
                 },
                 "thanks": {
                     "name": "symfony/polyfill",
                 "portable",
                 "shim"
             ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.22.0"
+            },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-07-14T12:35:20+00:00"
+            "time": "2021-01-07T17:09:11+00:00"
         },
         {
             "name": "symfony/polyfill-mbstring",
-            "version": "v1.18.1",
+            "version": "v1.22.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-mbstring.git",
-                "reference": "a6977d63bf9a0ad4c65cd352709e230876f9904a"
+                "reference": "f377a3dd1fde44d37b9831d68dc8dea3ffd28e13"
             },
             "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/f377a3dd1fde44d37b9831d68dc8dea3ffd28e13",
+                "reference": "f377a3dd1fde44d37b9831d68dc8dea3ffd28e13",
                 "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.22-dev"
                 },
                 "thanks": {
                     "name": "symfony/polyfill",
                 "portable",
                 "shim"
             ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.22.0"
+            },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-07-14T12:35:20+00:00"
+            "time": "2021-01-07T16:49:33+00:00"
         },
         {
-            "name": "symfony/polyfill-php70",
-            "version": "v1.18.1",
+            "name": "symfony/polyfill-php72",
+            "version": "v1.22.0",
             "source": {
                 "type": "git",
-                "url": "https://github.com/symfony/polyfill-php70.git",
-                "reference": "0dd93f2c578bdc9c72697eaa5f1dd25644e618d3"
+                "url": "https://github.com/symfony/polyfill-php72.git",
+                "reference": "cc6e6f9b39fe8075b3dabfbaf5b5f645ae1340c9"
             },
             "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/cc6e6f9b39fe8075b3dabfbaf5b5f645ae1340c9",
+                "reference": "cc6e6f9b39fe8075b3dabfbaf5b5f645ae1340c9",
                 "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.22-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.22.0"
+            },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-07-14T12:35:20+00:00"
-        },
-        {
-            "name": "symfony/polyfill-php72",
-            "version": "v1.18.1",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/symfony/polyfill-php72.git",
-                "reference": "639447d008615574653fb3bc60d1986d7172eaae"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/639447d008615574653fb3bc60d1986d7172eaae",
-                "reference": "639447d008615574653fb3bc60d1986d7172eaae",
-                "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\\Php72\\": ""
-                },
-                "files": [
-                    "bootstrap.php"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Nicolas Grekas",
-                    "email": "[email protected]"
-                },
-                {
-                    "name": "Symfony Community",
-                    "homepage": "https://symfony.com/contributors"
-                }
-            ],
-            "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions",
-            "homepage": "https://symfony.com",
-            "keywords": [
-                "compatibility",
-                "polyfill",
-                "portable",
-                "shim"
-            ],
-            "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"
+            "time": "2021-01-07T16:49:33+00:00"
         },
         {
             "name": "symfony/polyfill-php73",
-            "version": "v1.18.1",
+            "version": "v1.22.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-php73.git",
-                "reference": "fffa1a52a023e782cdcc221d781fe1ec8f87fcca"
+                "reference": "a678b42e92f86eca04b7fa4c0f6f19d097fb69e2"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/fffa1a52a023e782cdcc221d781fe1ec8f87fcca",
-                "reference": "fffa1a52a023e782cdcc221d781fe1ec8f87fcca",
+                "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/a678b42e92f86eca04b7fa4c0f6f19d097fb69e2",
+                "reference": "a678b42e92f86eca04b7fa4c0f6f19d097fb69e2",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.3.3"
+                "php": ">=7.1"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.18-dev"
+                    "dev-main": "1.22-dev"
                 },
                 "thanks": {
                     "name": "symfony/polyfill",
                 "portable",
                 "shim"
             ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-php73/tree/v1.22.0"
+            },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-07-14T12:35:20+00:00"
+            "time": "2021-01-07T16:49:33+00:00"
         },
         {
             "name": "symfony/polyfill-php80",
-            "version": "v1.18.1",
+            "version": "v1.22.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-php80.git",
-                "reference": "d87d5766cbf48d72388a9f6b85f280c8ad51f981"
+                "reference": "dc3063ba22c2a1fd2f45ed856374d79114998f91"
             },
             "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/dc3063ba22c2a1fd2f45ed856374d79114998f91",
+                "reference": "dc3063ba22c2a1fd2f45ed856374d79114998f91",
                 "shasum": ""
             },
             "require": {
-                "php": ">=7.0.8"
+                "php": ">=7.1"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.18-dev"
+                    "dev-main": "1.22-dev"
                 },
                 "thanks": {
                     "name": "symfony/polyfill",
                 "portable",
                 "shim"
             ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-php80/tree/v1.22.0"
+            },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-07-14T12:35:20+00:00"
+            "time": "2021-01-07T16:49:33+00:00"
         },
         {
             "name": "symfony/process",
-            "version": "v4.4.13",
+            "version": "v4.4.19",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/process.git",
-                "reference": "65e70bab62f3da7089a8d4591fb23fbacacb3479"
+                "reference": "7e950b6366d4da90292c2e7fa820b3c1842b965a"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/process/zipball/65e70bab62f3da7089a8d4591fb23fbacacb3479",
-                "reference": "65e70bab62f3da7089a8d4591fb23fbacacb3479",
+                "url": "https://api.github.com/repos/symfony/process/zipball/7e950b6366d4da90292c2e7fa820b3c1842b965a",
+                "reference": "7e950b6366d4da90292c2e7fa820b3c1842b965a",
                 "shasum": ""
             },
             "require": {
                 "php": ">=7.1.3"
             },
             "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "4.4-dev"
-                }
-            },
             "autoload": {
                 "psr-4": {
                     "Symfony\\Component\\Process\\": ""
                     "homepage": "https://symfony.com/contributors"
                 }
             ],
-            "description": "Symfony Process Component",
+            "description": "Executes commands in sub-processes",
             "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/process/tree/v4.4.19"
+            },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-07-23T08:31:43+00:00"
+            "time": "2021-01-27T09:09:26+00:00"
         },
         {
             "name": "symfony/routing",
-            "version": "v4.4.13",
+            "version": "v4.4.19",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/routing.git",
-                "reference": "e3387963565da9bae51d1d3ab8041646cc93bd04"
+                "reference": "87529f6e305c7acb162840d1ea57922038072425"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/routing/zipball/e3387963565da9bae51d1d3ab8041646cc93bd04",
-                "reference": "e3387963565da9bae51d1d3ab8041646cc93bd04",
+                "url": "https://api.github.com/repos/symfony/routing/zipball/87529f6e305c7acb162840d1ea57922038072425",
+                "reference": "87529f6e305c7acb162840d1ea57922038072425",
                 "shasum": ""
             },
             "require": {
                 "symfony/yaml": "<3.4"
             },
             "require-dev": {
-                "doctrine/annotations": "~1.2",
+                "doctrine/annotations": "^1.10.4",
                 "psr/log": "~1.0",
                 "symfony/config": "^4.2|^5.0",
                 "symfony/dependency-injection": "^3.4|^4.0|^5.0",
                 "symfony/yaml": "For using the YAML loader"
             },
             "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "4.4-dev"
-                }
-            },
             "autoload": {
                 "psr-4": {
                     "Symfony\\Component\\Routing\\": ""
                     "homepage": "https://symfony.com/contributors"
                 }
             ],
-            "description": "Symfony Routing Component",
+            "description": "Maps an HTTP request to a set of configuration variables",
             "homepage": "https://symfony.com",
             "keywords": [
                 "router",
                 "uri",
                 "url"
             ],
+            "support": {
+                "source": "https://github.com/symfony/routing/tree/v4.4.19"
+            },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-08-10T07:27:51+00:00"
+            "time": "2021-01-27T09:09:26+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.19",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/translation.git",
-                "reference": "700e6e50174b0cdcf0fa232773bec5c314680575"
+                "reference": "e1d0c67167a553556d9f974b5fa79c2448df317a"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/translation/zipball/700e6e50174b0cdcf0fa232773bec5c314680575",
-                "reference": "700e6e50174b0cdcf0fa232773bec5c314680575",
+                "url": "https://api.github.com/repos/symfony/translation/zipball/e1d0c67167a553556d9f974b5fa79c2448df317a",
+                "reference": "e1d0c67167a553556d9f974b5fa79c2448df317a",
                 "shasum": ""
             },
             "require": {
                 "symfony/yaml": ""
             },
             "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "4.4-dev"
-                }
-            },
             "autoload": {
                 "psr-4": {
                     "Symfony\\Component\\Translation\\": ""
                     "homepage": "https://symfony.com/contributors"
                 }
             ],
-            "description": "Symfony Translation Component",
+            "description": "Provides tools to internationalize your application",
             "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/translation/tree/v4.4.19"
+            },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-08-17T09:56:45+00:00"
+            "time": "2021-01-27T09:09:26+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.19",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/var-dumper.git",
-                "reference": "1bef32329f3166486ab7cb88599cae4875632b99"
+                "reference": "a1eab2f69906dc83c5ddba4632180260d0ab4f7f"
             },
             "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/a1eab2f69906dc83c5ddba4632180260d0ab4f7f",
+                "reference": "a1eab2f69906dc83c5ddba4632180260d0ab4f7f",
                 "shasum": ""
             },
             "require": {
                 "ext-iconv": "*",
                 "symfony/console": "^3.4|^4.0|^5.0",
                 "symfony/process": "^4.4|^5.0",
-                "twig/twig": "^1.34|^2.4|^3.0"
+                "twig/twig": "^1.43|^2.13|^3.0.4"
             },
             "suggest": {
                 "ext-iconv": "To convert non-UTF-8 strings to UTF-8 (or symfony/polyfill-iconv in case ext-iconv cannot be used).",
                 "Resources/bin/var-dump-server"
             ],
             "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "4.4-dev"
-                }
-            },
             "autoload": {
                 "files": [
                     "Resources/functions/dump.php"
                     "homepage": "https://symfony.com/contributors"
                 }
             ],
-            "description": "Symfony mechanism for exploring and dumping PHP variables",
+            "description": "Provides mechanisms for walking through any arbitrary PHP variable",
             "homepage": "https://symfony.com",
             "keywords": [
                 "debug",
                 "dump"
             ],
+            "support": {
+                "source": "https://github.com/symfony/var-dumper/tree/v4.4.19"
+            },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-08-17T07:31:35+00:00"
+            "time": "2021-01-27T09:09:26+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"
         },
         {
             "name": "vlucas/phpdotenv",
-            "version": "v3.6.7",
+            "version": "v3.6.8",
             "source": {
                 "type": "git",
                 "url": "https://github.com/vlucas/phpdotenv.git",
-                "reference": "2065beda6cbe75e2603686907b2e45f6f3a5ad82"
+                "reference": "5e679f7616db829358341e2d5cccbd18773bdab8"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/2065beda6cbe75e2603686907b2e45f6f3a5ad82",
-                "reference": "2065beda6cbe75e2603686907b2e45f6f3a5ad82",
+                "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/5e679f7616db829358341e2d5cccbd18773bdab8",
+                "reference": "5e679f7616db829358341e2d5cccbd18773bdab8",
                 "shasum": ""
             },
             "require": {
             "require-dev": {
                 "ext-filter": "*",
                 "ext-pcre": "*",
-                "phpunit/phpunit": "^4.8.35 || ^5.7.27 || ^6.5.6 || ^7.0"
+                "phpunit/phpunit": "^4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20"
             },
             "suggest": {
                 "ext-filter": "Required to use the boolean validator.",
                 "env",
                 "environment"
             ],
+            "support": {
+                "issues": "https://github.com/vlucas/phpdotenv/issues",
+                "source": "https://github.com/vlucas/phpdotenv/tree/v3.6.8"
+            },
             "funding": [
                 {
                     "url": "https://github.com/GrahamCampbell",
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-07-14T19:04:52+00:00"
+            "time": "2021-01-20T14:39:46+00:00"
         }
     ],
     "packages-dev": [
         {
             "name": "barryvdh/laravel-debugbar",
-            "version": "v3.5.1",
+            "version": "v3.5.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/barryvdh/laravel-debugbar.git",
-                "reference": "233c10688f4c1a6e66ed2ef123038b1363d1bedc"
+                "reference": "cae0a8d1cb89b0f0522f65e60465e16d738e069b"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/233c10688f4c1a6e66ed2ef123038b1363d1bedc",
-                "reference": "233c10688f4c1a6e66ed2ef123038b1363d1bedc",
+                "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/cae0a8d1cb89b0f0522f65e60465e16d738e069b",
+                "reference": "cae0a8d1cb89b0f0522f65e60465e16d738e069b",
                 "shasum": ""
             },
             "require": {
                 "symfony/finder": "^4.3|^5"
             },
             "require-dev": {
+                "mockery/mockery": "^1.3.3",
                 "orchestra/testbench-dusk": "^4|^5|^6",
                 "phpunit/phpunit": "^8.5|^9.0",
                 "squizlabs/php_codesniffer": "^3.5"
                 "profiler",
                 "webprofiler"
             ],
+            "support": {
+                "issues": "https://github.com/barryvdh/laravel-debugbar/issues",
+                "source": "https://github.com/barryvdh/laravel-debugbar/tree/v3.5.2"
+            },
             "funding": [
                 {
                     "url": "https://github.com/barryvdh",
                     "type": "github"
                 }
             ],
-            "time": "2020-09-07T19:32:39+00:00"
+            "time": "2021-01-06T14:21:44+00:00"
         },
         {
             "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"
         },
         {
             "name": "composer/ca-bundle",
-            "version": "1.2.8",
+            "version": "1.2.9",
             "source": {
                 "type": "git",
                 "url": "https://github.com/composer/ca-bundle.git",
-                "reference": "8a7ecad675253e4654ea05505233285377405215"
+                "reference": "78a0e288fdcebf92aa2318a8d3656168da6ac1a5"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/composer/ca-bundle/zipball/8a7ecad675253e4654ea05505233285377405215",
-                "reference": "8a7ecad675253e4654ea05505233285377405215",
+                "url": "https://api.github.com/repos/composer/ca-bundle/zipball/78a0e288fdcebf92aa2318a8d3656168da6ac1a5",
+                "reference": "78a0e288fdcebf92aa2318a8d3656168da6ac1a5",
                 "shasum": ""
             },
             "require": {
                 "php": "^5.3.2 || ^7.0 || ^8.0"
             },
             "require-dev": {
-                "phpunit/phpunit": "^4.8.35 || ^5.7 || 6.5 - 8",
+                "phpstan/phpstan": "^0.12.55",
                 "psr/log": "^1.0",
+                "symfony/phpunit-bridge": "^4.2 || ^5",
                 "symfony/process": "^2.5 || ^3.0 || ^4.0 || ^5.0"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.x-dev"
+                    "dev-main": "1.x-dev"
                 }
             },
             "autoload": {
                 "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.9"
+            },
             "funding": [
                 {
                     "url": "https://packagist.com",
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-08-23T12:54:47+00:00"
+            "time": "2021-01-12T12:10:35+00:00"
         },
         {
             "name": "composer/composer",
-            "version": "1.10.13",
+            "version": "2.0.9",
             "source": {
                 "type": "git",
                 "url": "https://github.com/composer/composer.git",
-                "reference": "47c841ba3b2d3fc0b4b13282cf029ea18b66d78b"
+                "reference": "591c2c155cac0d2d7f34af41d3b1e29bcbfc685e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/composer/composer/zipball/47c841ba3b2d3fc0b4b13282cf029ea18b66d78b",
-                "reference": "47c841ba3b2d3fc0b4b13282cf029ea18b66d78b",
+                "url": "https://api.github.com/repos/composer/composer/zipball/591c2c155cac0d2d7f34af41d3b1e29bcbfc685e",
+                "reference": "591c2c155cac0d2d7f34af41d3b1e29bcbfc685e",
                 "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.9"
+            },
             "funding": [
                 {
                     "url": "https://packagist.com",
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-09-09T09:46:34+00:00"
+            "time": "2021-01-27T15:09:27+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.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/maximebf/php-debugbar.git",
-                "reference": "1a1605b8e9bacb34cc0c6278206d699772e1d372"
+                "reference": "6d51ee9e94cff14412783785e79a4e7ef97b9d62"
             },
             "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/6d51ee9e94cff14412783785e79a4e7ef97b9d62",
+                "reference": "6d51ee9e94cff14412783785e79a4e7ef97b9d62",
                 "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.5"
+            },
+            "time": "2020-12-07T11:07:24+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.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpspec/prophecy.git",
-                "reference": "b20034be5efcdab4fb60ca3a29cba2949aead160"
+                "reference": "245710e971a030f42e08f4912863805570f23d39"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpspec/prophecy/zipball/b20034be5efcdab4fb60ca3a29cba2949aead160",
-                "reference": "b20034be5efcdab4fb60ca3a29cba2949aead160",
+                "url": "https://api.github.com/repos/phpspec/prophecy/zipball/245710e971a030f42e08f4912863805570f23d39",
+                "reference": "245710e971a030f42e08f4912863805570f23d39",
                 "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"
             },
             "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.2"
+            },
+            "time": "2020-12-19T10:15:11+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.14",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/phpunit.git",
-                "reference": "34c18baa6a44f1d1fbf0338907139e9dce95b997"
+                "reference": "c25f79895d27b6ecd5abfa63de1606b786a461a3"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/34c18baa6a44f1d1fbf0338907139e9dce95b997",
-                "reference": "34c18baa6a44f1d1fbf0338907139e9dce95b997",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c25f79895d27b6ecd5abfa63de1606b786a461a3",
+                "reference": "c25f79895d27b6ecd5abfa63de1606b786a461a3",
                 "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.14"
+            },
             "funding": [
                 {
                     "url": "https://phpunit.de/donate.html",
                     "type": "github"
                 }
             ],
-            "time": "2020-06-22T07:06:58+00:00"
+            "time": "2021-01-17T07:37:30+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.19",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/dom-crawler.git",
-                "reference": "6dd1e7adef4b7efeeb9691fd619279027d4dcf85"
+                "reference": "21032c566558255e551d23f4a516434c9e3a9a78"
             },
             "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/21032c566558255e551d23f4a516434c9e3a9a78",
+                "reference": "21032c566558255e551d23f4a516434c9e3a9a78",
                 "shasum": ""
             },
             "require": {
                 "symfony/css-selector": ""
             },
             "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "4.4-dev"
-                }
-            },
             "autoload": {
                 "psr-4": {
                     "Symfony\\Component\\DomCrawler\\": ""
                     "homepage": "https://symfony.com/contributors"
                 }
             ],
-            "description": "Symfony DomCrawler Component",
+            "description": "Eases DOM navigation for HTML and XML documents",
             "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/dom-crawler/tree/v4.4.19"
+            },
             "funding": [
                 {
                     "url": "https://symfony.com/sponsor",
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-08-12T06:20:35+00:00"
+            "time": "2021-01-27T09:09:26+00:00"
         },
         {
             "name": "symfony/filesystem",
-            "version": "v4.4.13",
+            "version": "v5.2.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/filesystem.git",
-                "reference": "27575bcbc68db1f6d06218891296572c9b845704"
+                "reference": "262d033b57c73e8b59cd6e68a45c528318b15038"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/filesystem/zipball/27575bcbc68db1f6d06218891296572c9b845704",
-                "reference": "27575bcbc68db1f6d06218891296572c9b845704",
+                "url": "https://api.github.com/repos/symfony/filesystem/zipball/262d033b57c73e8b59cd6e68a45c528318b15038",
+                "reference": "262d033b57c73e8b59cd6e68a45c528318b15038",
                 "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\\": ""
                     "homepage": "https://symfony.com/contributors"
                 }
             ],
-            "description": "Symfony Filesystem Component",
+            "description": "Provides basic utilities for the filesystem",
             "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/filesystem/tree/v5.2.3"
+            },
             "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": "2021-01-27T10:01:46+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",
             "version": "1.9.1",
             "source": {
                 "type": "git",
-                "url": "https://github.com/webmozart/assert.git",
+                "url": "https://github.com/webmozarts/assert.git",
                 "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/webmozart/assert/zipball/bafc69caeb4d49c39fd0779086c03a3738cbb389",
+                "url": "https://api.github.com/repos/webmozarts/assert/zipball/bafc69caeb4d49c39fd0779086c03a3738cbb389",
                 "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389",
                 "shasum": ""
             },
                 "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/webmozarts/assert/issues",
+                "source": "https://github.com/webmozarts/assert/tree/1.9.1"
             },
-            "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_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');
+        });
+    }
+}
diff --git a/database/migrations/2021_01_30_225441_add_settings_type_column.php b/database/migrations/2021_01_30_225441_add_settings_type_column.php
new file mode 100644 (file)
index 0000000..61d9bda
--- /dev/null
@@ -0,0 +1,32 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class AddSettingsTypeColumn extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::table('settings', function (Blueprint $table) {
+            $table->string('type', 50)->default('string');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::table('settings', function (Blueprint $table) {
+            $table->dropColumn('type');
+        });
+    }
+}
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 0b4336ab2a2b846950c5a01367161650b94da449..124305c8cd3527fca0c3117d78005d0fd989e93a 100644 (file)
@@ -3,6 +3,7 @@
   "description": "This is a book created via the API",
   "created_by": 1,
   "updated_by": 1,
+  "owned_by": 1,
   "slug": "my-new-book",
   "updated_at": "2020-01-12 14:05:11",
   "created_at": "2020-01-12 14:05:11",
index 29e83b1c024279f823bb6295d3238645a7384dc6..9900b5b0445a3d157237ec95830ccf735672b00e 100644 (file)
@@ -9,6 +9,7 @@
       "updated_at": "2019-12-11 20:57:31",
       "created_by": 1,
       "updated_by": 1,
+      "owned_by": 1,
       "image_id": 3
     },
     {
@@ -20,6 +21,7 @@
       "updated_at": "2019-12-11 20:57:23",
       "created_by": 4,
       "updated_by": 3,
+      "owned_by": 3,
       "image_id": 34
     }
   ],
index 2e43f5f87fc810163bc8323f53e304c6cbb070db..0b0bce4e8f91571e443063623766ee61c84efb55 100644 (file)
     "id": 1,
     "name": "Admin"
   },
+  "owned_by": {
+    "id": 1,
+    "name": "Admin"
+  },
   "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 8f20b5b9f545c92a17a52c6bc7d1f7de60ef9c12..fd93dc9aef1a3045ed874be2c9451adc7074b910 100644 (file)
@@ -7,5 +7,6 @@
   "updated_at": "2020-01-12 14:16:10",
   "created_by": 1,
   "updated_by": 1,
+  "owned_by": 1,
   "image_id": 452
 }
\ No newline at end of file
index 7aac2768789786f192c84b41f8c07e98dff82487..a990f278bac30e02c907e824c3a66841902b5bb2 100644 (file)
@@ -5,6 +5,7 @@
   "description": "This is a great new chapter that I've created via the API",
   "created_by": 1,
   "updated_by": 1,
+  "owned_by": 1,
   "slug": "my-fantastic-new-chapter",
   "updated_at": "2020-05-22 22:59:55",
   "created_at": "2020-05-22 22:59:55",
index 0c1fc5fc2a6f46992a3fd1ad7963608a5f5239b3..72ed7534df2eba47e16fa883dde9d216cb37c809 100644 (file)
@@ -10,7 +10,8 @@
       "created_at": "2019-05-05 21:49:56",
       "updated_at": "2019-09-28 11:24:23",
       "created_by": 1,
-      "updated_by": 1
+      "updated_by": 1,
+      "owned_by": 1
     },
     {
       "id": 2,
@@ -22,7 +23,8 @@
       "created_at": "2019-05-05 21:58:07",
       "updated_at": "2019-10-17 15:05:34",
       "created_by": 3,
-      "updated_by": 3
+      "updated_by": 3,
+      "owned_by": 3
     }
   ],
   "total": 40
index 2eddad8955070a36b15df967df96a3b7e7aa7dd7..41fed80efc1a3786ca4919c9d8d33a7937e2197a 100644 (file)
     "id": 1,
     "name": "Admin"
   },
+  "owned_by": {
+    "id": 1,
+    "name": "Admin"
+  },
   "tags": [
     {
       "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 +38,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 +53,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
index a7edb15b05411a389448deb10fc2c720dd43dcb0..11dedd0ca897f6efd4ebbc207e3ed988ba418ca3 100644 (file)
@@ -9,6 +9,7 @@
   "updated_at": "2020-05-22 23:07:20",
   "created_by": 1,
   "updated_by": 1,
+  "owned_by": 1,
   "book": {
     "id": 1,
     "name": "BookStack User Guide",
diff --git a/dev/api/responses/pages-create.json b/dev/api/responses/pages-create.json
new file mode 100644 (file)
index 0000000..0b19fb4
--- /dev/null
@@ -0,0 +1,39 @@
+{
+       "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"
+       },
+       "owned_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..9c162c6
--- /dev/null
@@ -0,0 +1,50 @@
+{
+       "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,
+                       "owned_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,
+                       "owned_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,
+                       "owned_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..93f7770
--- /dev/null
@@ -0,0 +1,39 @@
+{
+       "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"
+       },
+       "owned_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..ae5c0ea
--- /dev/null
@@ -0,0 +1,39 @@
+{
+       "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"
+       },
+       "owned_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 64f3c7f5394b657933ce64c1b793173f3b84dbbc..fafa4c9cd1e64f82bf4e81db8fbcf7aca256b620 100644 (file)
@@ -3,6 +3,7 @@
   "description": "This is my shelf with some books",
   "created_by": 1,
   "updated_by": 1,
+  "owned_by": 1,
   "slug": "my-shelf",
   "updated_at": "2020-04-10 13:24:09",
   "created_at": "2020-04-10 13:24:09",
index bccd08626176c84262f62477b4410ac802788b30..f5e9d03bb6f10ba6df1ed1677782ca788c7bc0a6 100644 (file)
@@ -9,6 +9,7 @@
       "updated_at": "2020-04-10 13:00:45",
       "created_by": 4,
       "updated_by": 1,
+      "owned_by": 1,
       "image_id": 31
     },
     {
@@ -20,6 +21,7 @@
       "updated_at": "2020-04-10 13:00:58",
       "created_by": 4,
       "updated_by": 1,
+      "owned_by": 1,
       "image_id": 28
     },
     {
@@ -31,6 +33,7 @@
       "updated_at": "2020-04-10 13:00:53",
       "created_by": 4,
       "updated_by": 1,
+      "owned_by": 4,
       "image_id": 30
     }
   ],
index 634fbb5a53c6fde235e72c2516b111a73f645451..d663e82c5fe2bd074c9ee1e7c24f87869094d35f 100644 (file)
     "id": 1,
     "name": "Admin"
   },
+  "owned_by": {
+    "id": 1,
+    "name": "Admin"
+  },
   "created_at": "2020-04-10 13:24:09",
   "updated_at": "2020-04-10 13:31:04",
   "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 4820150eb0afdc6c76a3b81bde2105a8858d7bb2..4bde44b54d5f2e2a188788d306fae0b0cdede022 100644 (file)
@@ -5,6 +5,7 @@
   "description": "This is my update shelf with some books",
   "created_by": 1,
   "updated_by": 1,
+  "owned_by": 1,
   "image_id": 501,
   "created_at": "2020-04-10 13:24:09",
   "updated_at": "2020-04-10 13:48:22"
index ea7a61ab554aea7540a28655d0e9c61822550f8a..39f5bdc18d11a997b5bfd71905ff1704966207a4 100644 (file)
@@ -39,6 +39,7 @@ services:
   node:
     image: node:alpine
     working_dir: /app
+    user: node
     volumes:
       - ./:/app
     entrypoint: /app/dev/docker/entrypoint.node.sh
index ad7c6f43a5d551eec767dadda32f047d64ded014..0332182883ceb77a5b477842593053aa5181ee11 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"/>
         <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"/>
         <server name="LOG_FAILED_LOGIN_MESSAGE" value=""/>
         <server name="LOG_FAILED_LOGIN_CHANNEL" value="testing"/>
+        <server name="WKHTMLTOPDF" value="false"/>
     </php>
 </phpunit>
index bf6dfac2ddc584439e357be29e6d9dadabcb3da1..a19341ce4e95dd7769871a137167e9fc90068ec1 100644 (file)
--- a/readme.md
+++ b/readme.md
@@ -4,7 +4,8 @@
 [![license](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/BookStackApp/BookStack/blob/master/LICENSE)
 [![Crowdin](https://badges.crowdin.net/bookstack/localized.svg)](https://crowdin.com/project/bookstack)
 [![Build Status](https://github.com/BookStackApp/BookStack/workflows/phpunit/badge.svg)](https://github.com/BookStackApp/BookStack/actions)
-[![Discord](https://img.shields.io/static/v1?label=Chat&message=Discord&color=738adb&logo=discord)](https://discord.gg/ztkBqR2)
+[![Discord](https://img.shields.io/static/v1?label=chat&message=discord&color=738adb&logo=discord)](https://discord.gg/ztkBqR2)
+[![Repo Stats](https://img.shields.io/static/v1?label=GitHub+project&message=stats&color=f27e3f)](https://gh-stats.bookstackapp.com/)
 
 A platform for storing and organising information and documentation. Details for BookStack can be found on the official website at https://www.bookstackapp.com/.
 
@@ -43,13 +44,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:
 
@@ -121,7 +122,7 @@ Feel free to create issues to request new features or to report bugs & problems.
 
 Pull requests are welcome. Unless a small tweak or language update, It may be best to open the pull request early or create an issue for your intended change to discuss how it will fit in to the project and plan out the merge. Just because a feature request exists, or is tagged, does not mean that feature would be accepted into the core project.
 
-Pull requests should be created from the `master` branch since they will be merged back into `master` once done. Please do not build from or request a merge into the `release` branch as this is only for publishing releases. If you are looking to alter CSS or JavaScript content please edit the source files found in `resources/assets`. Any CSS or JS files within `public` are built from these source files and therefore should not be edited directly.
+Pull requests should be created from the `master` branch since they will be merged back into `master` once done. Please do not build from or request a merge into the `release` branch as this is only for publishing releases. If you are looking to alter CSS or JavaScript content please edit the source files found in `resources/`. Any CSS or JS files within `public` are built from these source files and therefore should not be edited directly.
 
 The project's code of conduct [can be found here](https://github.com/BookStackApp/BookStack/blob/master/.github/CODE_OF_CONDUCT.md).
 
@@ -129,7 +130,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 +169,6 @@ 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)
+* [League/CommonMark](https://commonmark.thephpleague.com/)
+* [League/Flysystem](https://flysystem.thephpleague.com)
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..78581ec447f5cf099d64d681c976295d3c6875af 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});
@@ -21,7 +22,6 @@ class MarkdownEditor {
 
         this.displayStylesLoaded = false;
         this.input = this.elem.querySelector('textarea');
-        this.htmlInput = this.elem.querySelector('input[name=html]');
         this.cm = code.markdownEditor(this.input);
 
         this.onMarkdownScroll = this.onMarkdownScroll.bind(this);
@@ -124,7 +124,6 @@ class MarkdownEditor {
         // Set body content
         this.displayDoc.body.className = 'page-content';
         this.displayDoc.body.innerHTML = html;
-        this.htmlInput.value = html;
 
         // Copy styles from page head and set custom styles for editor
         this.loadStylesIntoDisplay();
@@ -373,7 +372,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 +491,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);
             });
         });
index 2be1c1c48b8cc93f5ac147b64023cf910eabc3fa..cc55fe35e1e38caae9de9f9cca013e0c7b1d14f6 100644 (file)
@@ -12,6 +12,7 @@ class PageDisplay {
         Code.highlight();
         this.setupPointer();
         this.setupNavHighlighting();
+        this.setupDetailsCodeBlockRefresh();
 
         // Check the hash on load
         if (window.location.hash) {
@@ -196,6 +197,16 @@ class PageDisplay {
             });
         }
     }
+
+    setupDetailsCodeBlockRefresh() {
+        const onToggle = event => {
+            const codeMirrors = [...event.target.querySelectorAll('.CodeMirror')];
+            codeMirrors.forEach(cm => cm.CodeMirror && cm.CodeMirror.refresh());
+        };
+
+        const details = [...this.elem.querySelectorAll('details')];
+        details.forEach(detail => detail.addEventListener('toggle', onToggle));
+    }
 }
 
 export default PageDisplay;
index 266e1918251d805fe4bc6735e5b3f801212fc2a6..f66e23b19923d2814b12ea5e2da844892c563557 100644 (file)
@@ -12,7 +12,7 @@ class PageEditor {
         this.editorType = this.$opts.editorType;
         this.pageId = Number(this.$opts.pageId);
         this.isNewDraft = this.$opts.pageNewDraft === 'true';
-        this.hasDefaultTitle = this.$opts.isDefaultTitle || false;
+        this.hasDefaultTitle = this.$opts.hasDefaultTitle || false;
 
         // Elements
         this.container = this.$el;
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..a6ab542181b9c20558bf0f030b98254181fc804c 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);
@@ -212,7 +212,7 @@ function codePlugin() {
             showPopup(editor);
         });
 
-        editor.on('SetContent', function () {
+        function parseCodeMirrorInstances() {
 
             // Recover broken codemirror instances
             $('.CodeMirrorContainer').filter((index ,elem) => {
@@ -231,12 +231,23 @@ function codePlugin() {
                     Code.wysiwygView(elem);
                 });
             });
+        }
+
+        editor.on('init', function() {
+            // Parse code mirror instances on init, but delay a little so this runs after
+            // initial styles are fetched into the editor.
+            parseCodeMirrorInstances();
+            // Parsed code mirror blocks when content is set but wait before setting this handler
+            // to avoid any init 'SetContent' events.
+            setTimeout(() => {
+                editor.on('SetContent', parseCodeMirrorInstances);
+            }, 200);
         });
 
     });
 }
 
-function drawIoPlugin(drawioUrl, isDarkMode, pageId) {
+function drawIoPlugin(drawioUrl, isDarkMode, pageId, wysiwygComponent) {
 
     let pageEditor = null;
     let currentNode = null;
@@ -280,7 +291,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 +306,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,15 +425,15 @@ 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";
+        this.plugins = "image imagetools table textcolor paste link autolink fullscreen code customhr autosave lists codeeditor media";
         this.loadPlugins();
 
         this.tinyMceConfig = this.getTinyMceConfig();
@@ -437,7 +448,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 e2aca1aad9e681d94a23763c95e550712e1bedfa..5727cd2b79a06f4e68b83eb68f1e1e34e2424300 100644 (file)
@@ -238,9 +238,7 @@ function wysiwygView(elem) {
         theme: getTheme(),
         readOnly: true
     });
-    setTimeout(() => {
-        cm.refresh();
-    }, 300);
+
     return {wrap: newWrap, editor: cm};
 }
 
index d7c8ed798faae1d2f6911254b45338f2c2219667..6dc8a83978dcc9e5b8f2c446fd368c68cf9f0722 100644 (file)
@@ -37,12 +37,13 @@ return [
 
     // Bookshelves
     'bookshelf_create'            => 'تم إنشاء رف الكتب',
-    'bookshelf_create_notification'    => 'Bookshelf Successfully Created',
-    'bookshelf_update'                 => 'updated bookshelf',
-    'bookshelf_update_notification'    => 'Bookshelf Successfully Updated',
-    'bookshelf_delete'                 => 'deleted bookshelf',
-    'bookshelf_delete_notification'    => 'Bookshelf Successfully Deleted',
+    'bookshelf_create_notification'    => 'تم إنشاء الرف بنجاح',
+    'bookshelf_update'                 => 'تم تحديث الرف',
+    'bookshelf_update_notification'    => 'تم تحديث الرف بنجاح',
+    'bookshelf_delete'                 => 'تم تحديث الرف',
+    'bookshelf_delete_notification'    => 'تم حذف الرف بنجاح',
 
     // Other
     'commented_on'                => 'تم التعليق',
+    'permissions_update'          => 'تحديث الأذونات',
 ];
index afe089fa0851be526b72ebc9382450c83b3c85dd..60a7af4114a4c6c43f2302f97f15c199092f98c5 100644 (file)
@@ -26,8 +26,8 @@ return [
     'remember_me' => 'تذكرني',
     'ldap_email_hint' => 'الرجاء إدخال عنوان بريد إلكتروني لاستخدامه مع الحساب.',
     'create_account' => 'إنشاء حساب',
-    'already_have_account' => 'Already have an account?',
-    'dont_have_account' => 'Don\'t have an account?',
+    'already_have_account' => 'لديك حساب بالفعل؟',
+    'dont_have_account' => 'ليس لديك حساب؟',
     'social_login' => 'تسجيل الدخول باستخدام حسابات التواصل الاجتماعي',
     'social_registration' => 'إنشاء حساب باستخدام حسابات التواصل الاجتماعي',
     'social_registration_text' => 'إنشاء حساب والدخول باستخدام خدمة أخرى.',
@@ -43,7 +43,7 @@ return [
     'reset_password' => 'استعادة كلمة المرور',
     'reset_password_send_instructions' => 'أدخل بريدك الإلكتروني بالأسفل وسيتم إرسال رسالة برابط لاستعادة كلمة المرور.',
     'reset_password_send_button' => 'أرسل رابط الاستعادة',
-    'reset_password_sent' => 'A password reset link will be sent to :email if that email address is found in the system.',
+    'reset_password_sent' => 'سيتم إرسال رابط إعادة تعيين كلمة المرور إلى عنوان البريد الإلكتروني هذا إذا كان موجودًا في النظام.',
     'reset_password_success' => 'تمت استعادة كلمة المرور بنجاح.',
     'email_reset_subject' => 'استعد كلمة المرور الخاصة بتطبيق :appName',
     'email_reset_text' => 'تم إرسال هذه الرسالة بسبب تلقينا لطلب استعادة كلمة المرور الخاصة بحسابكم.',
@@ -66,12 +66,12 @@ return [
     'email_not_confirmed_resend_button' => 'إعادة إرسال رسالة التأكيد',
 
     // User Invite
-    'user_invite_email_subject' => 'You have been invited to join :appName!',
-    'user_invite_email_greeting' => 'An account has been created for you on :appName.',
-    'user_invite_email_text' => 'Click the button below to set an account password and gain access:',
-    'user_invite_email_action' => 'Set Account Password',
-    'user_invite_page_welcome' => 'Welcome to :appName!',
-    'user_invite_page_text' => 'To finalise your account and gain access you need to set a password which will be used to log-in to :appName on future visits.',
-    'user_invite_page_confirm_button' => 'Confirm Password',
-    'user_invite_success' => 'Password set, you now have access to :appName!'
+    'user_invite_email_subject' => 'تم دعوتك للإنضمام إلى صفحة الحالة الخاصة بـ :app_name!',
+    'user_invite_email_greeting' => 'تم إنشاء حساب مستخدم لك على %site%.',
+    'user_invite_email_text' => 'انقر على الزر أدناه لتعيين كلمة مرور الحساب والحصول على الوصول:',
+    'user_invite_email_action' => 'كلمة سر المستخدم',
+    'user_invite_page_welcome' => 'مرحبا بكم في :appName!',
+    'user_invite_page_text' => 'لإكمال حسابك والحصول على حق الوصول تحتاج إلى تعيين كلمة مرور سيتم استخدامها لتسجيل الدخول إلى :appName في الزيارات المستقبلية.',
+    'user_invite_page_confirm_button' => 'تأكيد كلمة المرور',
+    'user_invite_success' => 'مجموعة كلمات المرور، لديك الآن حق الوصول إلى :appName!'
 ];
\ No newline at end of file
index 220b54892a92a417533e899f18bbe3443377db01..57d0b2dbd0629550458e7546c11d5c451a1c14aa 100644 (file)
@@ -11,7 +11,7 @@ return [
     'save' => 'حفظ',
     'continue' => 'استمرار',
     'select' => 'تحديد',
-    'toggle_all' => 'Toggle All',
+    'toggle_all' => 'تبديل الكل',
     'more' => 'المزيد',
 
     // Form Labels
@@ -24,7 +24,7 @@ return [
     // Actions
     'actions' => 'إجراءات',
     'view' => 'عرض',
-    'view_all' => 'View All',
+    'view_all' => 'عرض الكل',
     'create' => 'إنشاء',
     'update' => 'تحديث',
     'edit' => 'تعديل',
@@ -33,22 +33,22 @@ return [
     'copy' => 'نسخ',
     'reply' => 'رد',
     'delete' => 'حذف',
-    'delete_confirm' => 'Confirm Deletion',
+    'delete_confirm' => 'تأكيد الحذف',
     'search' => 'بحث',
     'search_clear' => 'مسح البحث',
     'reset' => 'إعادة تعيين',
     'remove' => 'إزالة',
     'add' => 'إضافة',
-    'fullscreen' => 'Fullscreen',
+    'fullscreen' => 'شاشة كاملة',
 
     // Sort Options
-    'sort_options' => 'Sort Options',
-    'sort_direction_toggle' => 'Sort Direction Toggle',
-    'sort_ascending' => 'Sort Ascending',
-    'sort_descending' => 'Sort Descending',
-    'sort_name' => 'Name',
-    'sort_created_at' => 'Created Date',
-    'sort_updated_at' => 'Updated Date',
+    'sort_options' => 'خيارات الترتيب',
+    'sort_direction_toggle' => 'الترتيب وفق الإتجاه',
+    'sort_ascending' => 'فرز تصاعدي',
+    'sort_descending' => 'فرز تنازلي',
+    'sort_name' => 'الاسم',
+    'sort_created_at' => 'تاريخ الإنشاء',
+    'sort_updated_at' => 'تاريخ التحديث',
 
     // Misc
     'deleted_user' => 'حذف مستخدم',
@@ -60,19 +60,19 @@ return [
     'details' => 'التفاصيل',
     'grid_view' => 'عرض شبكي',
     'list_view' => 'عرض منسدل',
-    'default' => 'Default',
-    'breadcrumb' => 'Breadcrumb',
+    'default' => 'افتراضي',
+    'breadcrumb' => 'شريط التنقل',
 
     // Header
-    'profile_menu' => 'Profile Menu',
+    'profile_menu' => 'قائمة ملف التعريف',
     'view_profile' => 'عرض الملف الشخصي',
     'edit_profile' => 'تعديل الملف الشخصي',
-    'dark_mode' => 'Dark Mode',
-    'light_mode' => 'Light Mode',
+    'dark_mode' => 'الوضع المظلم',
+    'light_mode' => 'الوضع المضيء',
 
     // Layout tabs
-    'tab_info' => 'Info',
-    'tab_content' => 'Content',
+    'tab_info' => 'معلومات',
+    'tab_content' => 'المحتوى',
 
     // Email Content
     'email_action_help' => 'إذا واجهتكم مشكلة بضغط زر ":actionText" فبإمكانكم نسخ الرابط أدناه ولصقه بالمتصفح:',
index 74d9bcdc7853d4c59b75901013c406e02a3ba39e..422de114b2aba7490926a61b0e77c4de0245da7d 100644 (file)
@@ -15,7 +15,7 @@ return [
     'image_load_more' => 'المزيد',
     'image_image_name' => 'اسم الصورة',
     'image_delete_used' => 'هذه الصورة مستخدمة بالصفحات أدناه.',
-    'image_delete_confirm_text' => 'Are you sure you want to delete this image?',
+    'image_delete_confirm_text' => 'هل أنت متأكد من أنك تريد حذف هذه الصورة ؟',
     'image_select_image' => 'تحديد الصورة',
     'image_dropzone' => 'قم بإسقاط الصورة أو اضغط هنا للرفع',
     'images_deleted' => 'تم حذف الصور',
@@ -29,6 +29,6 @@ return [
     'code_editor' => 'تعديل الشفرة',
     'code_language' => 'لغة الشفرة',
     'code_content' => 'محتويات الشفرة',
-    'code_session_history' => 'Session History',
+    'code_session_history' => 'سجل الدورة',
     'code_save' => 'حفظ الشفرة',
 ];
index 31835cb32d13598edaa98b190ceb63d84a860389..98404373986b92ef4c61e2f7be862c5f4ae2b739 100644 (file)
@@ -22,7 +22,8 @@ return [
     'meta_created_name' => 'أنشئ :timeLength بواسطة :user',
     'meta_updated' => 'مُحدث :timeLength',
     'meta_updated_name' => 'مُحدث :timeLength بواسطة :user',
-    'entity_select' => 'Entity Select',
+    'meta_owned_name' => 'Owned by :user',
+    'entity_select' => 'اختيار الكيان',
     'images' => 'صور',
     'my_recent_drafts' => 'مسوداتي الحديثة',
     'my_recently_viewed' => 'ما عرضته مؤخراً',
@@ -39,6 +40,7 @@ return [
     'permissions_intro' => 'في حال التفعيل, ستتم تبدية هذه الأذونات على أذونات الأدوار.',
     'permissions_enable' => 'تفعيل الأذونات المخصصة',
     'permissions_save' => 'حفظ الأذونات',
+    'permissions_owner' => 'Owner',
 
     // Search
     'search_results' => 'نتائج البحث',
@@ -73,30 +75,30 @@ return [
     'shelves_long' => 'أرفف الكتب',
     'shelves_empty' => 'لم يتم إنشاء أي أرفف',
     'shelves_create' => 'إنشاء رف جديد',
-    'shelves_popular' => 'Popular Shelves',
+    'shelves_popular' => 'أرفف شعبية',
     'shelves_new' => 'أرفف جديدة',
     'shelves_new_action' => 'رف جديد',
-    'shelves_popular_empty' => 'The most popular shelves will appear here.',
-    'shelves_new_empty' => 'The most recently created shelves will appear here.',
+    'shelves_popular_empty' => 'ستظهر هنا الأرفف الأكثر رواجًا.',
+    'shelves_new_empty' => 'ستظهر هنا الأرفف التي تم إنشاؤها مؤخرًا.',
     'shelves_save' => 'حفظ الرف',
-    'shelves_books' => 'Books on this shelf',
+    'shelves_books' => 'كتب على هذا الرف',
     'shelves_add_books' => 'إضافة كتب لهذا الرف',
     'shelves_drag_books' => 'اسحب الكتب هنا لإضافتها لهذا الرف',
     'shelves_empty_contents' => 'لا توجد كتب مخصصة لهذا الرف',
-    'shelves_edit_and_assign' => 'Edit shelf to assign books',
-    'shelves_edit_named' => 'Edit Bookshelf :name',
-    'shelves_edit' => 'Edit Bookshelf',
-    'shelves_delete' => 'Delete Bookshelf',
-    'shelves_delete_named' => 'Delete Bookshelf :name',
-    'shelves_delete_explain' => "This will delete the bookshelf with the name ':name'. Contained books will not be deleted.",
-    'shelves_delete_confirmation' => 'Are you sure you want to delete this bookshelf?',
-    'shelves_permissions' => 'Bookshelf Permissions',
-    'shelves_permissions_updated' => 'Bookshelf Permissions Updated',
-    'shelves_permissions_active' => 'Bookshelf Permissions Active',
-    'shelves_copy_permissions_to_books' => 'Copy Permissions to Books',
-    'shelves_copy_permissions' => 'Copy Permissions',
-    'shelves_copy_permissions_explain' => 'This will apply the current permission settings of this bookshelf to all books contained within. Before activating, ensure any changes to the permissions of this bookshelf have been saved.',
-    'shelves_copy_permission_success' => 'Bookshelf permissions copied to :count books',
+    'shelves_edit_and_assign' => 'تحرير الرف لإدراج كتب',
+    'shelves_edit_named' => 'تحرير رف الكتب: الاسم',
+    'shelves_edit' => 'تحرير رف الكتب',
+    'shelves_delete' => 'حذف رف الكتب',
+    'shelves_delete_named' => 'حذف رف الكتب: الاسم',
+    'shelves_delete_explain' => "سيؤدي هذا إلى حذف رف الكتب مع الاسم ':المُسمى به'. لن يتم حذف الكتب المتضمنة.",
+    'shelves_delete_confirmation' => 'هل أنت متأكد من أنك تريد حذف هذا الرف؟',
+    'shelves_permissions' => 'أذونات رف الكتب',
+    'shelves_permissions_updated' => 'تم تحديث أذونات رف الكتب',
+    'shelves_permissions_active' => 'أذونات رف الكتب نشطة',
+    'shelves_copy_permissions_to_books' => 'نسخ أذونات الوصول إلى الكتب',
+    'shelves_copy_permissions' => 'نسخ الأذونات',
+    'shelves_copy_permissions_explain' => 'سيؤدي هذا إلى تطبيق إعدادات الأذونات الحالية لهذا الرف على جميع الكتب المتضمنة فيه. قبل التفعيل، تأكد من حفظ أي تغييرات في أذونات هذا الرف.',
+    'shelves_copy_permission_success' => 'تم نسخ أذونات رف الكتب إلى: عد الكتب',
 
     // Books
     'book' => 'كتاب',
@@ -131,9 +133,9 @@ return [
     'books_sort_named' => 'فرز كتاب :bookName',
     'books_sort_name' => 'ترتيب حسب الإسم',
     'books_sort_created' => 'ترتيب حسب تاريخ الإنشاء',
-    'books_sort_updated' => 'Sort by Updated Date',
-    'books_sort_chapters_first' => 'Chapters First',
-    'books_sort_chapters_last' => 'Chapters Last',
+    'books_sort_updated' => 'فرز حسب تاريخ التحديث',
+    'books_sort_chapters_first' => 'الفصول الأولى',
+    'books_sort_chapters_last' => 'الفصول الأخيرة',
     'books_sort_show_other' => 'عرض كتب أخرى',
     'books_sort_save' => 'حفظ الترتيب الجديد',
 
@@ -146,7 +148,7 @@ return [
     'chapters_create' => 'إنشاء فصل جديد',
     'chapters_delete' => 'حذف الفصل',
     'chapters_delete_named' => 'حذف فصل :chapterName',
-    'chapters_delete_explain' => 'سيتم حذف فصل \':chapterName\'. جميع الصفحات ستزال وستتم إضافتها مباشرة للكتاب الرئيسي.',
+    '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' => 'تأكيد حذف الفصل؟',
     'chapters_edit' => 'تعديل الفصل',
     'chapters_edit_named' => 'تعديل فصل :chapterName',
@@ -177,7 +179,7 @@ return [
     'pages_delete_confirm' => 'تأكيد حذف الصفحة؟',
     'pages_delete_draft_confirm' => 'تأكيد حذف المسودة؟',
     'pages_editing_named' => ':pageName قيد التعديل',
-    'pages_edit_draft_options' => 'Draft Options',
+    'pages_edit_draft_options' => 'خيارات المسودة',
     'pages_edit_save_draft' => 'حفظ المسودة',
     'pages_edit_draft' => 'تعديل مسودة الصفحة',
     'pages_editing_draft' => 'المسودة قيد التعديل',
@@ -194,7 +196,7 @@ return [
     'pages_md_editor' => 'المحرر',
     'pages_md_preview' => 'معاينة',
     'pages_md_insert_image' => 'إدخال صورة',
-    'pages_md_insert_link' => 'Insert Entity Link',
+    'pages_md_insert_link' => 'إدراج ارتباط الكيان',
     'pages_md_insert_drawing' => 'إدخال رسمة',
     'pages_not_in_chapter' => 'صفحة ليست في فصل',
     'pages_move' => 'نقل الصفحة',
@@ -208,11 +210,12 @@ return [
     'pages_revisions' => 'مراجعات الصفحة',
     'pages_revisions_named' => 'مراجعات صفحة :pageName',
     'pages_revision_named' => 'مراجعة صفحة :pageName',
+    'pages_revision_restored_from' => 'Restored from #:id; :summary',
     'pages_revisions_created_by' => 'أنشئ بواسطة',
     'pages_revisions_date' => 'تاريخ المراجعة',
     'pages_revisions_number' => '#',
-    'pages_revisions_numbered' => 'Revision #:id',
-    'pages_revisions_numbered_changes' => 'Revision #:id Changes',
+    'pages_revisions_numbered' => 'مراجعة #: رقم تعريفي',
+    'pages_revisions_numbered_changes' => 'مراجعة #: رقم تعريفي التغييرات',
     'pages_revisions_changelog' => 'سجل التعديل',
     'pages_revisions_changes' => 'التعديلات',
     'pages_revisions_current' => 'النسخة الحالية',
@@ -231,24 +234,24 @@ return [
         'start_b' => ':userName بدأ بتعديل هذه الصفحة',
         'time_a' => 'منذ أن تم تحديث هذه الصفحة',
         'time_b' => 'في آخر :minCount دقيقة/دقائق',
-        'message' => ':start :time. Take care not to overwrite each other\'s updates!',
+        'message' => 'وقت البدء: احرص على عدم الكتابة فوق تحديثات بعضنا البعض!',
     ],
     'pages_draft_discarded' => 'تم التخلص من المسودة. تم تحديث المحرر بمحتوى الصفحة الحالي',
-    'pages_specific' => 'Specific Page',
-    'pages_is_template' => 'Page Template',
+    'pages_specific' => 'صفحة محددة',
+    'pages_is_template' => 'قالب الصفحة',
 
     // Editor Sidebar
     'page_tags' => 'وسوم الصفحة',
     'chapter_tags' => 'وسوم الفصل',
     'book_tags' => 'وسوم الكتاب',
-    'shelf_tags' => 'Shelf Tags',
+    'shelf_tags' => 'علامات الرف',
     'tag' => 'وسم',
     'tags' =>  'وسوم',
-    'tag_name' =>  'Tag Name',
+    'tag_name' =>  'اسم العلامة',
     'tag_value' => 'قيمة الوسم (اختياري)',
     'tags_explain' => "إضافة الوسوم تساعد بترتيب وتقسيم المحتوى. \n من الممكن وضع قيمة لكل وسم لترتيب أفضل وأدق.",
     'tags_add' => 'إضافة وسم آخر',
-    'tags_remove' => 'Remove this tag',
+    'tags_remove' => 'إزالة هذه العلامة',
     'attachments' => 'المرفقات',
     'attachments_explain' => 'ارفع بعض الملفات أو أرفق بعض الروابط لعرضها بصفحتك. ستكون الملفات والروابط معروضة في الشريط الجانبي للصفحة.',
     'attachments_explain_instant_save' => 'سيتم حفظ التغييرات هنا بلحظتها',
@@ -256,16 +259,16 @@ return [
     'attachments_upload' => 'رفع ملف',
     'attachments_link' => 'إرفاق رابط',
     'attachments_set_link' => 'تحديد الرابط',
-    'attachments_delete' => 'Are you sure you want to delete this attachment?',
+    'attachments_delete' => 'هل أنت متأكد من أنك تريد حذف هذا المرفق؟',
     'attachments_dropzone' => 'أسقط الملفات أو اضغط هنا لإرفاق ملف',
     'attachments_no_files' => 'لم يتم رفع أي ملفات',
     'attachments_explain_link' => 'بالإمكان إرفاق رابط في حال عدم تفضيل رفع ملف. قد يكون الرابط لصفحة أخرى أو لملف في أحد خدمات التخزين السحابي.',
     'attachments_link_name' => 'اسم الرابط',
     'attachment_link' => 'رابط المرفق',
-    'attachments_link_url' => 'Link to file',
+    'attachments_link_url' => 'رابط الملف',
     'attachments_link_url_hint' => 'رابط الموقع أو الملف',
     'attach' => 'إرفاق',
-    'attachments_insert_link' => 'Add Attachment Link to Page',
+    'attachments_insert_link' => 'إضافة رابط مرفق إلى الصفحة',
     'attachments_edit_file' => 'تعديل الملف',
     'attachments_edit_file_name' => 'اسم الملف',
     'attachments_edit_drop_upload' => 'أسقط الملفات أو اضغط هنا للرفع والاستبدال',
@@ -279,16 +282,16 @@ return [
     'templates_set_as_template' => 'هذه الصفحة عبارة عن قالب',
     'templates_explain_set_as_template' => 'يمكنك تعيين هذه الصفحة كقالب بحيث تستخدم محتوياتها عند إنشاء صفحات أخرى. سيتمكن المستخدمون الآخرون من استخدام هذا القالب إذا كان لديهم أذونات عرض لهذه الصفحة.',
     'templates_replace_content' => 'استبدال محتوى الصفحة',
-    'templates_append_content' => 'Append to page content',
-    'templates_prepend_content' => 'Prepend to page content',
+    'templates_append_content' => 'تذييل محتوى الصفحة',
+    'templates_prepend_content' => 'بادئة محتوى الصفحة',
 
     // Profile View
-    'profile_user_for_x' => 'User for :time',
+    'profile_user_for_x' => 'المستخدم لـ : الوقت',
     'profile_created_content' => 'المحتوى المنشأ',
     'profile_not_created_pages' => 'لم يتم إنشاء أي صفحات بواسطة :userName',
     'profile_not_created_chapters' => 'لم يتم إنشاء أي فصول بواسطة :userName',
     'profile_not_created_books' => 'لم يتم إنشاء أي كتب بواسطة :userName',
-    'profile_not_created_shelves' => ':userName has not created any shelves',
+    'profile_not_created_shelves' => 'لم يقم "اسم المستخدم"بإنشاء أي أرفف',
 
     // Comments
     'comment' => 'تعليق',
index cbf1d59583b4223d96881dce944bf6640b897fc7..93e8201ab7599e3a11a40fb44d076461d07137b8 100644 (file)
@@ -13,16 +13,16 @@ return [
     'email_already_confirmed' => 'تم تأكيد البريد الإلكتروني من قبل, الرجاء محاولة تسجيل الدخول.',
     'email_confirmation_invalid' => 'رابط التأكيد غير صحيح أو قد تم استخدامه من قبل, الرجاء محاولة التسجيل من جديد.',
     'email_confirmation_expired' => 'صلاحية رابط التأكيد انتهت, تم إرسال رسالة تأكيد جديدة لعنوان البريد الإلكتروني.',
-    'email_confirmation_awaiting' => 'The email address for the account in use needs to be confirmed',
+    'email_confirmation_awaiting' => 'عنوان البريد الإلكتروني للحساب قيد الاستخدام يحتاج إلى تأكيد',
     'ldap_fail_anonymous' => 'فشل الوصول إلى LDAP باستخدام الربط المجهول',
     'ldap_fail_authed' => 'فشل الوصول إلى LDAP باستخدام dn و password المعطاة',
     'ldap_extension_not_installed' => 'لم يتم تثبيت إضافة LDAP PHP',
     'ldap_cannot_connect' => 'لا يمكن الاتصال بخادم ldap, فشل الاتصال المبدئي',
-    'saml_already_logged_in' => 'Already logged in',
-    'saml_user_not_registered' => 'The user :name is not registered and automatic registration is disabled',
-    'saml_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system',
-    'saml_invalid_response_id' => 'The request from the external authentication system is not recognised by a process started by this application. Navigating back after a login could cause this issue.',
-    'saml_fail_authed' => 'Login using :system failed, system did not provide successful authorization',
+    'saml_already_logged_in' => 'تم تسجيل الدخول بالفعل',
+    'saml_user_not_registered' => 'المستخدم :name غير مسجل ويتم تعطيل التسجيل التلقائي',
+    'saml_no_email_address' => 'تعذر العثور على عنوان بريد إلكتروني، لهذا المستخدم، في البيانات المقدمة من نظام المصادقة الخارجي',
+    'saml_invalid_response_id' => 'لم يتم التعرف على الطلب من نظام التوثيق الخارجي من خلال عملية تبدأ بهذا التطبيق. العودة بعد تسجيل الدخول يمكن أن يسبب هذه المشكلة.',
+    'saml_fail_authed' => 'تسجيل الدخول باستخدام :system فشل، النظام لم يوفر التفويض الناجح',
     'social_no_action_defined' => 'لم يتم تعريف أي إجراء',
     'social_login_bad_response' => "حصل خطأ خلال تسجيل الدخول باستخدام :socialAccount \n:error",
     'social_account_in_use' => 'حساب :socialAccount قيد الاستخدام حالياً, الرجاء محاولة الدخول باستخدام خيار :socialAccount.',
@@ -31,9 +31,9 @@ return [
     'social_account_already_used_existing' => 'حساب :socialAccount مستخدَم من قبل مستخدم آخر.',
     'social_account_not_used' => 'حساب :socialAccount غير مرتبط بأي مستخدم. الرجاء ربطه من خلال إعدادات ملفكم. ',
     'social_account_register_instructions' => 'إذا لم يكن لديكم حساب فيمكنكم التجسيل باستخدام خيار :socialAccount.',
-    'social_driver_not_found' => 'Social driver not found',
-    'social_driver_not_configured' => 'Your :socialAccount social settings are not configured correctly.',
-    'invite_token_expired' => 'This invitation link has expired. You can instead try to reset your account password.',
+    'social_driver_not_found' => 'لم يتم العثور على السوشيال درايفر "Social driver"',
+    'social_driver_not_configured' => 'لم يتم تهيئة إعدادات حسابك الاجتماعي بشكل صحيح.',
+    'invite_token_expired' => 'انتهت صلاحية رابط هذه الدعوة. يمكنك بدلاً من ذلك محاولة إعادة تعيين كلمة مرور حسابك.',
 
     // System
     'path_not_writable' => 'لا يمكن الرفع إلى مسار :filePath. الرجاء التأكد من قابلية الكتابة إلى الخادم.',
@@ -53,8 +53,8 @@ return [
     'page_custom_home_deletion' => 'لا يمكن حذف الصفحة إذا كانت محددة كصفحة رئيسية',
 
     // Entities
-    'entity_not_found' => 'Entity not found',
-    'bookshelf_not_found' => 'Bookshelf not found',
+    'entity_not_found' => 'الكيان غير موجود',
+    'bookshelf_not_found' => 'رف الكتب غير موجود',
     'book_not_found' => 'لم يتم العثور على الكتاب',
     'page_not_found' => 'لم يتم العثور على الصفحة',
     'chapter_not_found' => 'لم يتم العثور على الفصل',
@@ -70,7 +70,7 @@ return [
     'role_cannot_be_edited' => 'لا يمكن تعديل هذا الدور',
     'role_system_cannot_be_deleted' => 'هذا الدور خاص بالنظام ولا يمكن حذفه',
     'role_registration_default_cannot_delete' => 'لا يمكن حذف الدور إذا كان مسجل كالدور الأساسي بعد تسجيل الحساب',
-    'role_cannot_remove_only_admin' => 'This user is the only user assigned to the administrator role. Assign the administrator role to another user before attempting to remove it here.',
+    'role_cannot_remove_only_admin' => 'هذا المستخدم هو المستخدم الوحيد المعين لدور المسؤول. قم بتعيين دور المسؤول لمستخدم آخر قبل محاولة إزالته هنا.',
 
     // Comments
     'comment_list' => 'حصل خطأ خلال جلب التعليقات.',
@@ -82,21 +82,21 @@ return [
     // Error pages
     '404_page_not_found' => 'لم يتم العثور على الصفحة',
     'sorry_page_not_found' => 'عفواً, لا يمكن العثور على الصفحة التي تبحث عنها.',
-    'sorry_page_not_found_permission_warning' => 'If you expected this page to exist, you might not have permission to view it.',
+    'sorry_page_not_found_permission_warning' => 'إذا كنت تتوقع أن تكون هذه الصفحة موجودة، قد لا يكون لديك تصريح بمشاهدتها.',
     'return_home' => 'العودة للصفحة الرئيسية',
     'error_occurred' => 'حدث خطأ',
     'app_down' => ':appName لا يعمل حالياً',
     'back_soon' => 'سيعود للعمل قريباً.',
 
     // API errors
-    'api_no_authorization_found' => 'No authorization token found on the request',
-    'api_bad_authorization_format' => 'An authorization token was found on the request but the format appeared incorrect',
-    'api_user_token_not_found' => 'No matching API token was found for the provided authorization token',
-    'api_incorrect_token_secret' => 'The secret provided for the given used API token is incorrect',
-    'api_user_no_api_permission' => 'The owner of the used API token does not have permission to make API calls',
-    'api_user_token_expired' => 'The authorization token used has expired',
+    'api_no_authorization_found' => 'لم يتم العثور على رمز ترخيص مميز في الطلب',
+    'api_bad_authorization_format' => 'تم العثور على رمز ترخيص مميز في الطلب ولكن يبدو أن التنسيق غير صحيح',
+    'api_user_token_not_found' => 'لم يتم العثور على رمز API مطابق لرمز الترخيص المُقدم',
+    'api_incorrect_token_secret' => 'الشفرة المُقدمة لرمز API المستخدم المحدد غير صحيحة',
+    'api_user_no_api_permission' => 'مالك رمز API المستخدم ليس لديه الصلاحية لإجراء مكالمات API',
+    'api_user_token_expired' => 'انتهت صلاحية رمز الترخيص المستخدم',
 
     // Settings & Maintenance
-    'maintenance_test_email_failure' => 'Error thrown when sending a test email:',
+    'maintenance_test_email_failure' => 'حدث خطأ عند إرسال بريد إلكتروني تجريبي:',
 
 ];
index db3dce49fa42490110a079d3d1bdd191b153bdd8..4ab447d5d56e5c3226063fbd6f085111d9cdb34e 100644 (file)
@@ -8,7 +8,7 @@ return [
 
     'password' => 'يجب أن تتكون كلمة المرور من ستة أحرف على الأقل وأن تطابق التأكيد.',
     'user' => "لم يتم العثور على مستخدم بعنوان البريد الإلكتروني المعطى.",
-    'token' => 'The password reset token is invalid for this email address.',
+    'token' => 'رمز إعادة تعيين كلمة المرور غير صالح لعنوان هذا البريد الإلكتروني.',
     'sent' => 'تم إرسال رابط تجديد كلمة المرور إلى بريدكم الإلكتروني!',
     'reset' => 'تم تجديد كلمة المرور الخاصة بكم!',
 
index 3b94cdfc6315e5c64a420d341c39741f49ef33f2..f55451ff63e300db86b02dc9a8be4c44f77ad3c8 100755 (executable)
@@ -12,88 +12,109 @@ return [
     'settings_save_success' => 'تم حفظ الإعدادات',
 
     // App Settings
-    'app_customization' => 'Customization',
-    'app_features_security' => 'Features & Security',
+    'app_customization' => 'تخصيص',
+    'app_features_security' => 'الميزات و الأمان',
     'app_name' => 'اسم التطبيق',
     'app_name_desc' => 'سيتم عرض هذا الاسم في الترويسة وفي أي رسالة بريد إلكتروني.',
     'app_name_header' => 'عرض اسم التطبيق في الترويسة؟',
-    'app_public_access' => 'Public Access',
-    'app_public_access_desc' => 'Enabling this option will allow visitors, that are not logged-in, to access content in your BookStack instance.',
-    'app_public_access_desc_guest' => 'Access for public visitors can be controlled through the "Guest" user.',
-    'app_public_access_toggle' => 'Allow public access',
+    'app_public_access' => 'الوصول العام',
+    'app_public_access_desc' => 'تمكين هذا الخيار سيسمح للزوار، الذين لم يتم تسجيل دخولهم، بالوصول إلى المحتوى في مثيل مكدس الكتب الخاص بك.',
+    'app_public_access_desc_guest' => 'يمكن التحكم في وصول الزوار العموميين من خلال المستخدم "الضيف".',
+    'app_public_access_toggle' => 'السماح بالوصول العام',
     'app_public_viewing' => 'السماح بالعرض على العامة؟',
     'app_secure_images' => 'تفعيل حماية أكبر لرفع الصور؟',
-    'app_secure_images_toggle' => 'Enable higher security image uploads',
+    'app_secure_images_toggle' => 'لمزيد من الحماية',
     'app_secure_images_desc' => 'لتحسين أداء النظام, ستكون جميع الصور متاحة للعامة. هذا الخيار يضيف سلسلة من الحروف والأرقام العشوائية صعبة التخمين إلى رابط الصورة. الرجاء التأكد من تعطيل فهرسة المسارات لمنع الوصول السهل.',
     'app_editor' => 'محرر الصفحة',
     'app_editor_desc' => 'الرجاء اختيار محرر النص الذي سيستخدم من قبل جميع المستخدمين لتحرير الصفحات.',
     'app_custom_html' => 'Custom HTML head content',
-    'app_custom_html_desc' => 'Any content added here will be inserted into the bottom of the <head> section of every page. This is handy for overriding styles or adding analytics code.',
-    'app_custom_html_disabled_notice' => 'Custom HTML head content is disabled on this settings page to ensure any breaking changes can be reverted.',
+    'app_custom_html_desc' => 'سيتم إدراج أي محتوى مضاف هنا في الجزء السفلي من قسم <head> من كل صفحة. هذا أمر مفيد لتجاوز الأنماط أو إضافة رمز التحليل.',
+    'app_custom_html_disabled_notice' => 'تم تعطيل محتوى HTML الرئيسي المخصص في صفحة الإعدادات هذه لضمان عكس أي تغييرات متتالية.',
     'app_logo' => 'شعار التطبيق',
     'app_logo_desc' => 'يجب أن تكون الصورة بارتفاع 43 بكسل. <br>سيتم تصغير الصور الأكبر من ذلك.',
     'app_primary_color' => 'اللون الأساسي للتطبيق',
     'app_primary_color_desc' => 'يجب أن تكون القيمة من نوع hex. <br>اترك الخانة فارغة للرجوع للون الافتراضي.',
     'app_homepage' => 'الصفحة الرئيسية للتطبيق',
     'app_homepage_desc' => 'الرجاء اختيار صفحة لتصبح الصفحة الرئيسية بدل من الافتراضية. سيتم تجاهل جميع الأذونات الخاصة بالصفحة المختارة.',
-    'app_homepage_select' => 'Select a page',
+    'app_homepage_select' => 'اختر صفحة',
     'app_disable_comments' => 'تعطيل التعليقات',
-    'app_disable_comments_toggle' => 'Disable comments',
+    'app_disable_comments_toggle' => 'تعطيل التعليقات',
     'app_disable_comments_desc' => 'تعطيل التعليقات على جميع الصفحات داخل التطبيق. التعليقات الموجودة من الأصل لن تكون ظاهرة.',
 
     // Color settings
-    'content_colors' => 'Content Colors',
-    'content_colors_desc' => 'Sets colors for all elements in the page organisation hierarchy. Choosing colors with a similar brightness to the default colors is recommended for readability.',
-    'bookshelf_color' => 'Shelf Color',
-    'book_color' => 'Book Color',
-    'chapter_color' => 'Chapter Color',
-    'page_color' => 'Page Color',
-    'page_draft_color' => 'Page Draft Color',
+    'content_colors' => 'ألوان المحتوى',
+    'content_colors_desc' => 'تعيين الألوان لجميع العناصر في التسلسل الهرمي لتنظيم الصفحات. يوصى باختيار الألوان ذات السطوع المماثل للألوان الافتراضية للقراءة.',
+    'bookshelf_color' => 'لون الرف',
+    'book_color' => 'لون الكتاب',
+    'chapter_color' => 'لون الفصل',
+    'page_color' => 'لون الصفحة',
+    'page_draft_color' => 'لون مسودة الصفحة',
 
     // Registration Settings
     'reg_settings' => 'إعدادات التسجيل',
-    'reg_enable' => 'Enable Registration',
-    'reg_enable_toggle' => 'Enable registration',
-    'reg_enable_desc' => 'When registration is enabled user will be able to sign themselves up as an application user. Upon registration they are given a single, default user role.',
+    'reg_enable' => 'تمكين التسجيل',
+    'reg_enable_toggle' => 'تمكين التسجيل',
+    'reg_enable_desc' => 'عند تمكين التسجيل سيكون المستخدم قادرا على تسجيل نفسه كمستخدم تطبيق. عند التسجيل يعطى لهم دور مستخدم افتراضي وحيد.',
     'reg_default_role' => 'دور المستخدم الأساسي بعد التسجيل',
-    'reg_enable_external_warning' => 'The option above is ignored while external LDAP or SAML authentication is active. User accounts for non-existing members will be auto-created if authentication, against the external system in use, is successful.',
-    'reg_email_confirmation' => 'Email Confirmation',
-    'reg_email_confirmation_toggle' => 'Require email confirmation',
+    'reg_enable_external_warning' => 'يتم تجاهل الخيار أعلاه بينما يتم تفعيل مصادقة LDAP الخارجية أو SAML. حسابات المستخدم للأعضاء غير الحاليين سيتم إنشاؤها تلقائياً إذا كانت المصادقة، مقابل النظام الخارجي المستخدم، ناجحة.',
+    'reg_email_confirmation' => 'تأكيد البريد الإلكتروني',
+    'reg_email_confirmation_toggle' => 'يتطلب تأكيد البريد الإلكتروني',
     'reg_confirm_email_desc' => 'إذا تم استخدام قيود للمجال سيصبح التأكيد عن طريق البريد الإلكتروني إلزامي وسيتم تجاهل القيمة أسفله.',
     'reg_confirm_restrict_domain' => 'تقييد التسجيل على مجال محدد',
-    'reg_confirm_restrict_domain_desc' => 'Enter a comma separated list of email domains you would like to restrict registration to. Users will be sent an email to confirm their address before being allowed to interact with the application. <br> Note that users will be able to change their email addresses after successful registration.',
+    'reg_confirm_restrict_domain_desc' => 'أدخل قائمة مفصولة بفواصل لنطاقات البريد الإلكتروني التي ترغب في تقييد التسجيل إليها. سيتم إرسال بريد إلكتروني للمستخدمين لتأكيد عنوانهم قبل السماح لهم بالتفاعل مع التطبيق. <br> لاحظ أن المستخدمين سيكونون قادرين على تغيير عناوين البريد الإلكتروني الخاصة بهم بعد التسجيل بنجاح.',
     'reg_confirm_restrict_domain_placeholder' => 'لم يتم اختيار أي قيود',
 
     // Maintenance settings
     'maint' => 'الصيانة',
     'maint_image_cleanup' => 'تنظيف الصور',
-    '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' => 'تجاهل الصور في المراجعات',
+    'maint_image_cleanup_desc' => "مسح الصفحة ومراجعة المحتوى للتحقق من أي الصور والرسوم المستخدمة حاليًا وأي الصور زائدة عن الحاجة. تأكد من إنشاء قاعدة بيانات كاملة و نسخة احتياطية للصور قبل تشغيل هذا.",
+    'maint_delete_images_only_in_revisions' => 'Also delete images that only exist in old page revisions',
     'maint_image_cleanup_run' => 'بدء التنظيف',
     'maint_image_cleanup_warning' => 'يوجد عدد :count من الصور المحتمل عدم استخدامها. تأكيد حذف الصور؟',
     'maint_image_cleanup_success' => 'تم إيجاد وحذف عدد :count من الصور المحتمل عدم استخدامها!',
     'maint_image_cleanup_nothing_found' => 'لم يتم حذف أي شيء لعدم وجود أي صور غير مسمتخدمة',
-    'maint_send_test_email' => 'Send a Test Email',
-    'maint_send_test_email_desc' => 'This sends a test email to your email address specified in your profile.',
-    'maint_send_test_email_run' => 'Send test email',
-    'maint_send_test_email_success' => 'Email sent to :address',
-    '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_send_test_email' => 'إرسال بريد إلكتروني تجريبي',
+    'maint_send_test_email_desc' => 'يرسل هذا بريدًا إلكترونيًا تجريبيًا إلى عنوان بريدك الإلكتروني المحدد في ملفك الشخصي.',
+    'maint_send_test_email_run' => 'إرسال بريد إليكتروني تجريبي',
+    'maint_send_test_email_success' => 'تم إرسال البريد الإلكتروني إلى:العنوان',
+    'maint_send_test_email_mail_subject' => 'اختبار البريد الإلكتروني',
+    'maint_send_test_email_mail_greeting' => 'يبدو أن تسليم البريد الإلكتروني يعمل!',
+    'maint_send_test_email_mail_text' => 'تهانينا! كما تلقيت إشعار هذا البريد الإلكتروني، يبدو أن إعدادات البريد الإلكتروني الخاص بك قد تم تكوينها بشكل صحيح.',
+    'maint_recycle_bin_desc' => 'تُرسل الأرفف والكتب والفصول والصفحات المحذوفة إلى سلة المحذوفات حتى يمكن استعادتها أو حذفها نهائيًا. قد يتم إزالة العناصر الأقدم في سلة المحذوفات تلقائيًا بعد فترة اعتمادًا على تكوين النظام.',
+    'maint_recycle_bin_open' => 'افتح سلة المحذوفات',
+
+    // Recycle Bin
+    'recycle_bin' => 'سلة المحذوفات',
+    'recycle_bin_desc' => 'هنا يمكنك استعادة العناصر التي تم حذفها أو اختيار إزالتها نهائيا من النظام. هذه القائمة غير مصفاة خلافاً لقوائم الأنشطة المماثلة في النظام حيث يتم تطبيق عوامل تصفية الأذونات.',
+    'recycle_bin_deleted_item' => 'عنصر محذوف',
+    'recycle_bin_deleted_by' => 'حُذف بواسطة',
+    'recycle_bin_deleted_at' => 'وقت الحذف',
+    'recycle_bin_permanently_delete' => 'حُذف نهائيًا',
+    'recycle_bin_restore' => 'استرجاع',
+    'recycle_bin_contents_empty' => 'سلة المحذوفات فارغة حاليًا',
+    'recycle_bin_empty' => 'إفراغ سلة المحذوفات',
+    'recycle_bin_empty_confirm' => 'سيؤدي هذا إلى إتلاف جميع العناصر الموجودة في سلة المحذوفات بشكل دائم بما في ذلك المحتوى الموجود داخل كل عنصر. هل أنت متأكد من أنك تريد إفراغ سلة المحذوفات؟',
+    'recycle_bin_destroy_confirm' => 'سيؤدي هذا الإجراء إلى حذف هذا العنصر نهائيًا ، إلى جانب أي عناصر فرعية مدرجة أدناه ، من النظام ولن تتمكن من استعادة هذا المحتوى. هل أنت متأكد من أنك تريد حذف هذا العنصر نهائيًا؟',
+    'recycle_bin_destroy_list' => 'العناصر المراد تدميرها',
+    'recycle_bin_restore_list' => 'العناصر المراد استرجاعها',
+    'recycle_bin_restore_confirm' => 'سيعيد هذا الإجراء العنصر المحذوف ، بما في ذلك أي عناصر فرعية ، إلى موقعه الأصلي. إذا تم حذف الموقع الأصلي منذ ذلك الحين ، وهو الآن في سلة المحذوفات ، فسيلزم أيضًا استعادة العنصر الأصلي.',
+    'recycle_bin_restore_deleted_parent' => 'تم حذف أصل هذا العنصر أيضًا. سيبقى حذفه حتى يتم استعادة ذلك الأصل أيضًا.',
+    'recycle_bin_destroy_notification' => 'المحذوف: قُم بعد إجمالي العناصر من سلة المحذوفات.',
+    'recycle_bin_restore_notification' => 'المرتجع: قُم بعد إجمالي العناصر من سلة المحذوفات.',
 
     // Audit Log
-    'audit' => 'Audit Log',
-    'audit_desc' => 'This audit log displays a list of activities tracked in the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.',
-    'audit_event_filter' => 'Event Filter',
-    'audit_event_filter_no_filter' => 'No Filter',
-    'audit_deleted_item' => 'Deleted Item',
-    'audit_deleted_item_name' => 'Name: :name',
-    'audit_table_user' => 'User',
-    'audit_table_event' => 'Event',
-    'audit_table_item' => 'Related Item',
-    'audit_table_date' => 'Activity Date',
-    'audit_date_from' => 'Date Range From',
-    'audit_date_to' => 'Date Range To',
+    'audit' => 'سجل المراجعة',
+    'audit_desc' => 'يعرض هذا السجل قائمة بالأنشطة المتعقبة في النظام. هذه القائمة غير مصفاة خلافاً لقوائم الأنشطة المماثلة في النظام حيث يتم تطبيق عوامل تصفية الأذونات.',
+    'audit_event_filter' => 'تصفية الحدث',
+    'audit_event_filter_no_filter' => 'لا يوجد فلتر',
+    'audit_deleted_item' => 'عنصر محذوف',
+    'audit_deleted_item_name' => 'الاسم: كتابة الاسم',
+    'audit_table_user' => 'المستخدم',
+    'audit_table_event' => 'الحدث',
+    'audit_table_related' => 'العنصر أو التفاصيل ذات الصلة',
+    'audit_table_date' => 'تاريخ النشاط',
+    'audit_date_from' => 'نطاق التاريخ من',
+    'audit_date_to' => 'نطاق التاريخ إلى',
 
     // Role Settings
     'roles' => 'الأدوار',
@@ -102,7 +123,7 @@ return [
     'role_create_success' => 'تم إنشاء الدور بنجاح',
     'role_delete' => 'حذف الدور',
     'role_delete_confirm' => 'سيتم حذف الدور المسمى \':roleName\'.',
-    'role_delete_users_assigned' => 'This role has :userCount users assigned to it. If you would like to migrate the users from this role select a new role below.',
+    'role_delete_users_assigned' => 'هذا الدور له: عدد المستخدمين المعينين له. إذا كنت ترغب في ترحيل المستخدمين من هذا الدور ، فحدد دورًا جديدًا أدناه.',
     'role_delete_no_migration' => "لا تقم بترجيل المستخدمين",
     'role_delete_sure' => 'تأكيد حذف الدور؟',
     'role_delete_success' => 'تم حذف الدور بنجاح',
@@ -110,22 +131,22 @@ return [
     'role_details' => 'تفاصيل الدور',
     'role_name' => 'اسم الدور',
     'role_desc' => 'وصف مختصر للدور',
-    'role_external_auth_id' => 'External Authentication IDs',
+    'role_external_auth_id' => 'ربط الحساب بمواقع التواصل',
     'role_system' => 'أذونات النظام',
     'role_manage_users' => 'إدارة المستخدمين',
     'role_manage_roles' => 'إدارة الأدوار وأذوناتها',
     'role_manage_entity_permissions' => 'إدارة جميع أذونات الكتب والفصول والصفحات',
     'role_manage_own_entity_permissions' => 'إدارة الأذونات الخاصة بكتابك أو فصلك أو صفحاتك',
-    'role_manage_page_templates' => 'Manage page templates',
-    'role_access_api' => 'Access system API',
+    'role_manage_page_templates' => 'إدارة قوالب الصفحة',
+    'role_access_api' => 'الوصول إلى واجهة برمجة تطبيقات النظام API',
     'role_manage_settings' => 'إدارة إعدادات التطبيق',
-    'role_asset' => 'Asset Permissions',
-    'roles_system_warning' => 'Be aware that access to any of the above three permissions can allow a user to alter their own privileges or the privileges of others in the system. Only assign roles with these permissions to trusted users.',
-    'role_asset_desc' => 'These permissions control default access to the assets within the system. Permissions on Books, Chapters and Pages will override these permissions.',
-    'role_asset_admins' => 'Admins are automatically given access to all content but these options may show or hide UI options.',
+    'role_asset' => 'أذونات الأصول',
+    'roles_system_warning' => 'اعلم أن الوصول إلى أي من الأذونات الثلاثة المذكورة أعلاه يمكن أن يسمح للمستخدم بتغيير امتيازاته الخاصة أو امتيازات الآخرين في النظام. قم بتعيين الأدوار مع هذه الأذونات فقط للمستخدمين الموثوق بهم.',
+    'role_asset_desc' => 'تتحكم هذه الأذونات في الوصول الافتراضي إلى الأصول داخل النظام. ستتجاوز الأذونات الخاصة بالكتب والفصول والصفحات هذه الأذونات.',
+    'role_asset_admins' => 'يُمنح المسؤولين حق الوصول تلقائيًا إلى جميع المحتويات ولكن هذه الخيارات قد تعرض خيارات واجهة المستخدم أو تخفيها.',
     'role_all' => 'الكل',
-    'role_own' => 'Own',
-    'role_controlled_by_asset' => 'Controlled by the asset they are uploaded to',
+    'role_own' => 'ما يخص',
+    'role_controlled_by_asset' => 'يتحكم فيها الأصول التي يتم رفعها إلى',
     'role_save' => 'حفظ الدور',
     'role_update_success' => 'تم تحديث الدور بنجاح',
     'role_users' => 'مستخدمون داخل هذا الدور',
@@ -136,63 +157,67 @@ return [
     'user_profile' => 'ملف المستخدم',
     'users_add_new' => 'إضافة مستخدم جديد',
     'users_search' => 'بحث عن مستخدم',
-    '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.',
+    'users_latest_activity' => 'أحدث نشاط',
+    'users_details' => 'بيانات المستخدم',
+    'users_details_desc' => 'قم بتعيين اسم عرض وعنوان بريد إلكتروني لهذا المستخدم. سيتم استخدام عنوان البريد الإلكتروني لتسجيل الدخول إلى التطبيق.',
+    'users_details_desc_no_email' => 'قم بتعيين اسم عرض لهذا المستخدم حتى يتمكن الآخرون من التعرف عليه.',
     'users_role' => 'أدوار المستخدمين',
-    'users_role_desc' => 'Select which roles this user will be assigned to. If a user is assigned to multiple roles the permissions from those roles will stack and they will receive all abilities of the assigned roles.',
-    'users_password' => 'User Password',
-    'users_password_desc' => 'Set a password used to log-in to the application. This must be at least 6 characters long.',
-    'users_send_invite_text' => 'You can choose to send this user an invitation email which allows them to set their own password otherwise you can set their password yourself.',
-    'users_send_invite_option' => 'Send user invite email',
-    'users_external_auth_id' => 'External Authentication ID',
-    'users_external_auth_id_desc' => 'This is the ID used to match this user when communicating with your external authentication system.',
+    'users_role_desc' => 'حدد الأدوار التي سيتم تعيين هذا المستخدم لها. إذا تم تعيين مستخدم لأدوار متعددة ، فسيتم تكديس الأذونات من هذه الأدوار وسيتلقى كل قدرات الأدوار المعينة.',
+    'users_password' => 'كلمة مرور المستخدم',
+    'users_password_desc' => 'قم بتعيين كلمة مرور مستخدمة لتسجيل الدخول إلى التطبيق. يجب ألا يقل طول هذه الكلمة عن 6 أحرف.',
+    'users_send_invite_text' => 'يمكنك اختيار إرسال دعوة بالبريد الإلكتروني إلى هذا المستخدم مما يسمح له بتعيين كلمة المرور الخاصة به أو يمكنك تعيين كلمة المرور الخاصة به بنفسك.',
+    'users_send_invite_option' => 'أرسل بريدًا إلكترونيًا لدعوة المستخدم',
+    'users_external_auth_id' => 'ربط الحساب بمواقع التواصل',
+    'users_external_auth_id_desc' => 'تستخدم هذه الهوية لإثبات شخصية المستخدم عند الدخول إلى مواقع التواصل الخاصة بك.',
     'users_password_warning' => 'الرجاء ملئ الحقل أدناه فقط في حال أردتم تغيير كلمة المرور:',
     'users_system_public' => 'هذا المستخدم يمثل أي ضيف يقوم بزيارة شيء يخصك. لا يمكن استخدامه لتسجيل الدخول ولكن يتم تعيينه تلقائياً.',
     'users_delete' => 'حذف المستخدم',
     'users_delete_named' => 'حذف المستخدم :userName',
     'users_delete_warning' => 'سيتم حذف المستخدم \':userName\' بشكل تام من النظام.',
     'users_delete_confirm' => 'تأكيد حذف المستخدم؟',
-    'users_delete_success' => 'تم حذف المستخدم بنجاح',
+    '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' => 'تعديل المستخدم',
     'users_edit_profile' => 'تعديل الملف',
     'users_edit_success' => 'تم تحديث المستخدم بنجاح',
     'users_avatar' => 'صورة المستخدم',
     'users_avatar_desc' => 'يجب أن تكون الصورة مربعة ومقاربة لحجم 256 بكسل',
     'users_preferred_language' => 'اللغة المفضلة',
-    'users_preferred_language_desc' => 'This option will change the language used for the user-interface of the application. This will not affect any user-created content.',
+    'users_preferred_language_desc' => 'سيؤدي هذا الخيار إلى تغيير اللغة المستخدمة لواجهة المستخدم الخاصة بالتطبيق. لن يؤثر هذا على أي محتوى قد أنشائه المستخدم.',
     'users_social_accounts' => 'الحسابات الاجتماعية',
     'users_social_accounts_info' => 'Here you can connect your other accounts for quicker and easier login. Disconnecting an account here does not previously authorized access. Revoke access from your profile settings on the connected social account.',
     'users_social_connect' => 'ربط الحساب',
     'users_social_disconnect' => 'فصل الحساب',
     'users_social_connected' => 'تم ربط حساب :socialAccount بملفك بنجاح.',
     'users_social_disconnected' => 'تم فصل حساب :socialAccount من ملفك بنجاح.',
-    'users_api_tokens' => 'API Tokens',
-    'users_api_tokens_none' => 'No API tokens have been created for this user',
-    'users_api_tokens_create' => 'Create Token',
-    'users_api_tokens_expires' => 'Expires',
-    'users_api_tokens_docs' => 'API Documentation',
+    'users_api_tokens' => 'رموز الـ API',
+    'users_api_tokens_none' => 'لم يتم إنشاء رموز API لهذا المستخدم',
+    'users_api_tokens_create' => 'قم بإنشاء رمز مميز',
+    'users_api_tokens_expires' => 'انتهاء مدة الصلاحية',
+    'users_api_tokens_docs' => 'وثائق API',
 
     // API Tokens
-    'user_api_token_create' => 'Create API Token',
-    'user_api_token_name' => 'Name',
-    'user_api_token_name_desc' => 'Give your token a readable name as a future reminder of its intended purpose.',
-    'user_api_token_expiry' => 'Expiry Date',
-    'user_api_token_expiry_desc' => 'Set a date at which this token expires. After this date, requests made using this token will no longer work. Leaving this field blank will set an expiry 100 years into the future.',
-    'user_api_token_create_secret_message' => 'Immediately after creating this token a "Token ID" & "Token Secret" will be generated and displayed. The secret will only be shown a single time so be sure to copy the value to somewhere safe and secure before proceeding.',
-    'user_api_token_create_success' => 'API token successfully created',
-    'user_api_token_update_success' => 'API token successfully updated',
-    'user_api_token' => 'API Token',
-    'user_api_token_id' => 'Token ID',
-    'user_api_token_id_desc' => 'This is a non-editable system generated identifier for this token which will need to be provided in API requests.',
-    'user_api_token_secret' => 'Token Secret',
-    'user_api_token_secret_desc' => 'This is a system generated secret for this token which will need to be provided in API requests. This will only be displayed this one time so copy this value to somewhere safe and secure.',
-    'user_api_token_created' => 'Token created :timeAgo',
-    'user_api_token_updated' => 'Token updated :timeAgo',
-    'user_api_token_delete' => 'Delete Token',
-    'user_api_token_delete_warning' => 'This will fully delete this API token with the name \':tokenName\' from the system.',
-    'user_api_token_delete_confirm' => 'Are you sure you want to delete this API token?',
-    'user_api_token_delete_success' => 'API token successfully deleted',
+    'user_api_token_create' => 'قم بإنشاء رمز API',
+    'user_api_token_name' => 'الاسم',
+    'user_api_token_name_desc' => 'اعطي الرمز الخاص بك اسمًا يمكن قراءته للتذكير مستقبلًا بالغرض المقصود منه.',
+    'user_api_token_expiry' => 'تاريخ انتهاء الصلاحية',
+    'user_api_token_expiry_desc' => 'حدد التاريخ الذي تنتهي فيه صلاحية هذا الرمز. بعد هذا التاريخ ، لن تعمل الطلبات المقدمة باستخدام هذا الرمز. سيؤدي ترك هذا الحقل فارغًا إلى تعيين انتهاء صلاحية لمدة 100 عام في المستقبل.',
+    'user_api_token_create_secret_message' => 'عقب إنشاء هذا الرمز مباشرة، سيتم إنشاء "مُعرّف الرمز" و "رمز سري" وعرضهما. وسيتم عرض الرمز السري لمرة واحدة فقط ، لذا تأكد من نسخ قيمة هذا الرمز إلى مكان آمن ومضمون قبل المتابعة.',
+    'user_api_token_create_success' => 'تم إنشاء رمز الـ API بنجاح',
+    'user_api_token_update_success' => 'تم تحديث رمز الـ API بنجاح',
+    'user_api_token' => 'رمز الـ API',
+    'user_api_token_id' => 'مُعرّف الرمز',
+    'user_api_token_id_desc' => 'هذا مُعرّف تم إنشاؤه بواسطة النظام غير قابل للتحرير لهذا الرمز والذي يجب توفيره في طلبات API.',
+    'user_api_token_secret' => 'الرمز السري',
+    'user_api_token_secret_desc' => 'هذا الرمز السري تم إنشاؤه بواسطة النظام والذي يجب توفيره ضمن طلبات API. سيتم عرضه لمرة واحدة فقط ، لذا انسخ قيمة هذا الرمز إلى مكان آمن ومضمون.',
+    'user_api_token_created' => 'تم إنشاء رمز :الوقت الزمني',
+    'user_api_token_updated' => 'تم تحديث الرمز :الوقت الزمني',
+    'user_api_token_delete' => 'حذف الرمز',
+    'user_api_token_delete_warning' => 'سيؤدي هذا إلى حذف رمز API المُشار إليه بالكامل باسم \'اسم الرمز\' من النظام.',
+    'user_api_token_delete_confirm' => 'هل أنت متأكد من أنك تريد حذف رمز API؟',
+    'user_api_token_delete_success' => 'تم حذف رمز الـ API بنجاح',
 
     //! If editing translations files directly please ignore this in all
     //! languages apart from en. Content will be auto-copied from en.
@@ -214,6 +239,7 @@ return [
         'ja' => '日本語',
         'ko' => '한국어',
         'nl' => 'Nederlands',
+        'nb' => 'Norsk (Bokmål)',
         'pl' => 'Polski',
         'pt_BR' => 'Português do Brasil',
         'ru' => 'Русский',
index 7d6d13e817713fed9832856a4bc57458a054edf0..d0b17460bccbb992c93f8c71a3895d4fdbe2fad3 100644 (file)
@@ -14,7 +14,7 @@ return [
     'alpha'                => 'يجب أن يقتصر :attribute على الحروف فقط.',
     'alpha_dash'           => 'يجب أن يقتصر :attribute على حروف أو أرقام أو شرطات فقط.',
     'alpha_num'            => 'يجب أن يقتصر :attribute على الحروف والأرقام فقط.',
-    'array'                => 'The :attribute must be an array.',
+    'array'                => 'يجب أن تكون السمة مصفوفة.',
     'before'               => 'يجب أن يكون التاريخ :attribute قبل :date.',
     'between'              => [
         'numeric' => 'يجب أن يكون :attribute بين :min و :max.',
@@ -22,7 +22,7 @@ return [
         'string'  => 'يجب أن يكون :attribute بين :min و :max حرف / حروف.',
         'array'   => 'يجب أن يكون :attribute بين :min و :max عنصر / عناصر.',
     ],
-    'boolean'              => 'The :attribute field must be true or false.',
+    'boolean'              => 'يجب أن يحتمل حقل السمة الصحة أو الخطأ.',
     'confirmed'            => ':attribute غير مطابق.',
     'date'                 => ':attribute ليس تاريخ صالح.',
     'date_format'          => ':attribute لا يطابق الصيغة :format.',
@@ -30,40 +30,40 @@ return [
     'digits'               => 'يجب أن يكون :attribute بعدد :digits خانات.',
     'digits_between'       => 'يجب أن يكون :attribute بعدد خانات بين :min و :max.',
     'email'                => 'يجب أن يكون :attribute عنوان بريد إلكتروني صالح.',
-    'ends_with' => 'The :attribute must end with one of the following: :values',
+    'ends_with' => 'يجب أن تنتهي السمة بأحد القيم التالية',
     'filled'               => 'حقل :attribute مطلوب.',
     'gt'                   => [
-        'numeric' => 'The :attribute must be greater than :value.',
-        'file'    => 'The :attribute must be greater than :value kilobytes.',
-        'string'  => 'The :attribute must be greater than :value characters.',
-        'array'   => 'The :attribute must have more than :value items.',
+        'numeric' => 'يجب أن تكون السمة أكبر من: القيمة.',
+        'file'    => 'يجب أن تكون السمة أكبر من: القيمة كيلوبايت.',
+        'string'  => 'يجب أن تكون السمة أكبر من: أحرف القيمة.',
+        'array'   => 'يجب أن تحتوي السمة على أكثر من: عناصر القيمة.',
     ],
     'gte'                  => [
-        'numeric' => 'The :attribute must be greater than or equal :value.',
-        'file'    => 'The :attribute must be greater than or equal :value kilobytes.',
-        'string'  => 'The :attribute must be greater than or equal :value characters.',
-        'array'   => 'The :attribute must have :value items or more.',
+        'numeric' => 'يجب أن تكون السمة أكبر من أو تساوي: القيمة.',
+        'file'    => 'يجب أن تكون السمة أكبر من أو تساوي: القيمة كيلوبايت.',
+        'string'  => 'يجب أن تكون السمة أكبر من أو تساوي: أحرف القيمة.',
+        'array'   => 'يجب أن تحتوي السمة على: عناصر القيمة أو أكثر.',
     ],
     'exists'               => ':attribute المحدد غير صالح.',
     'image'                => 'يجب أن يكون :attribute صورة.',
-    'image_extension'      => 'The :attribute must have a valid & supported image extension.',
+    'image_extension'      => 'يجب أن تحتوي السمة على امتداد صورة صالح ومدعوم.',
     'in'                   => ':attribute المحدد غير صالح.',
     'integer'              => 'يجب أن يكون :attribute عدد صحيح.',
     'ip'                   => 'يجب أن يكون :attribute عنوان IP صالح.',
-    'ipv4'                 => 'The :attribute must be a valid IPv4 address.',
-    'ipv6'                 => 'The :attribute must be a valid IPv6 address.',
-    'json'                 => 'The :attribute must be a valid JSON string.',
+    'ipv4'                 => 'يجب أن تكون السمة: عنوان IPv4 صالحًا.',
+    'ipv6'                 => 'يجب أن تكون السمة: عنوان IPv6 صالحًا.',
+    'json'                 => 'يجب أن تكون السمة: سلسلة من نوع جسون JSON صالح.',
     'lt'                   => [
-        'numeric' => 'The :attribute must be less than :value.',
-        'file'    => 'The :attribute must be less than :value kilobytes.',
-        'string'  => 'The :attribute must be less than :value characters.',
-        'array'   => 'The :attribute must have less than :value items.',
+        'numeric' => 'يجب أن تكون السمة أقل من: القيمة.',
+        'file'    => 'يجب أن تكون السمة أقل من: القيمة كيلوبايت.',
+        'string'  => 'يجب أن تكون السمة أقل من: أحرف القيمة.',
+        'array'   => 'يجب أن تحتوي السمة على أقل من: عناصر القيمة.',
     ],
     'lte'                  => [
-        'numeric' => 'The :attribute must be less than or equal :value.',
-        'file'    => 'The :attribute must be less than or equal :value kilobytes.',
-        'string'  => 'The :attribute must be less than or equal :value characters.',
-        'array'   => 'The :attribute must not have more than :value items.',
+        'numeric' => 'يجب أن تكون السمة أقل من أو تساوي: القيمة.',
+        'file'    => 'يجب أن تكون السمة أقل من أو تساوي: القيمة كيلوبايت.',
+        'string'  => 'يجب أن تكون السمة أقل من أو تساوي: أحرف القيمة.',
+        'array'   => 'يجب ألا تحتوي السمة على أكثر من: عناصر القيمة.',
     ],
     'max'                  => [
         'numeric' => 'يجب ألا يكون :attribute أكبر من :max.',
@@ -78,9 +78,9 @@ return [
         'string'  => 'يجب أن يكون :attribute على الأقل :min حرف / حروف.',
         'array'   => 'يجب أن يحتوي :attribute على :min عنصر / عناصر كحد أدنى.',
     ],
-    'no_double_extension'  => 'The :attribute must only have a single file extension.',
+    'no_double_extension'  => 'يجب أن يكون للسمة: امتداد ملف واحد فقط.',
     'not_in'               => ':attribute المحدد غير صالح.',
-    'not_regex'            => 'The :attribute format is invalid.',
+    'not_regex'            => 'صيغة السمة: غير صالحة.',
     'numeric'              => 'يجب أن يكون :attribute رقم.',
     'regex'                => 'صيغة :attribute غير صالحة.',
     'required'             => 'حقل :attribute مطلوب.',
@@ -90,17 +90,18 @@ return [
     'required_without'     => 'حقل :attribute مطلوب عندما تكون :values غير موجودة.',
     'required_without_all' => 'حقل :attribute مطلوب عندما لا يكون أي من :values موجودة.',
     'same'                 => 'يجب تطابق :attribute مع :other.',
+    'safe_url'             => 'قد لايكون الرابط المتوفر آمنا.',
     'size'                 => [
         'numeric' => 'يجب أن يكون :attribute بحجم :size.',
         'file'    => 'يجب أن يكون :attribute بحجم :size كيلو بايت.',
         'string'  => 'يجب أن يكون :attribute بعدد :size حرف / حروف.',
         'array'   => 'يجب أن يحتوي :attribute على :size عنصر / عناصر.',
     ],
-    'string'               => 'The :attribute must be a string.',
+    'string'               => 'يجب أن تكون السمة: سلسلة.',
     'timezone'             => 'يجب أن تكون :attribute منطقة صالحة.',
     'unique'               => 'تم حجز :attribute من قبل.',
     'url'                  => 'صيغة :attribute غير صالحة.',
-    'uploaded'             => 'The file could not be uploaded. The server may not accept files of this size.',
+    'uploaded'             => 'تعذر تحميل الملف. قد لا يقبل الخادم ملفات بهذا الحجم.',
 
     // Custom validation lines
     'custom' => [
index a5001bc795e2c4fa8892ea66903ba6e626d63c00..a495733549313f8ece45e61a9b29fd97c6983451 100644 (file)
@@ -45,4 +45,5 @@ return [
 
     // Other
     'commented_on'                => 'коментирано на',
+    'permissions_update'          => 'updated permissions',
 ];
index 796bcd975d018e06d7778d43748b85ceab77bdd8..7f9cae6f6ca976426fc3b0d9137c0a1919a11fd1 100644 (file)
@@ -22,6 +22,7 @@ return [
     'meta_created_name' => 'Създадено преди :timeLength от :user',
     'meta_updated' => 'Актуализирано :timeLength',
     'meta_updated_name' => 'Актуализирано преди :timeLength от :user',
+    'meta_owned_name' => 'Owned by :user',
     'entity_select' => 'Избор на обект',
     'images' => 'Изображения',
     'my_recent_drafts' => 'Моите скорошни драфтове',
@@ -39,6 +40,7 @@ return [
     'permissions_intro' => 'Веднъж добавени, тези права ще вземат приоритет над всички други установени права.',
     'permissions_enable' => 'Разреши уникални права',
     'permissions_save' => 'Запази права',
+    'permissions_owner' => 'Owner',
 
     // Search
     'search_results' => 'Резултати от търсенето',
@@ -146,7 +148,7 @@ return [
     'chapters_create' => 'Създай нова глава',
     'chapters_delete' => 'Изтрий глава',
     'chapters_delete_named' => 'Изтрий глава :chapterName',
-    'chapters_delete_explain' => 'Ще бъде изтрита глава с име \':chapterName\'. Всички страници в нея ще бъдат премахнати и добавени в основната книга.',
+    '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' => 'Сигурни ли сте, че искате да изтриете тази глава?',
     'chapters_edit' => 'Редактирай глава',
     'chapters_edit_named' => 'Актуализирай глава :chapterName',
@@ -208,6 +210,7 @@ return [
     'pages_revisions' => 'Ревизии на страницата',
     'pages_revisions_named' => 'Ревизии на страницата :pageName',
     'pages_revision_named' => 'Ревизия на страницата :pageName',
+    'pages_revision_restored_from' => 'Restored from #:id; :summary',
     'pages_revisions_created_by' => 'Създадено от',
     'pages_revisions_date' => 'Дата на ревизията',
     'pages_revisions_number' => '№',
index e8612c3d7cc3fef1c36fc4111dc8a1fcb0129e93..dcfe44b3ab97d02f94e077cf255c3d445264eeea 100644 (file)
@@ -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' => 'Русский',
index 76b57a2a3b58ddb8ef41e0562c5187359cc6e542..578ea999fc31618997be7834012fb30aed3d7b1f 100644 (file)
@@ -90,6 +90,7 @@ return [
     'required_without'     => 'The :attribute field is required when :values is not present.',
     'required_without_all' => 'The :attribute field is required when none of :values are present.',
     'same'                 => 'The :attribute and :other must match.',
+    'safe_url'             => 'The provided link may not be safe.',
     'size'                 => [
         'numeric' => 'The :attribute must be :size.',
         'file'    => 'The :attribute must be :size kilobytes.',
index 92f4df8b67ce64fc44aa667b58a4376aad777b51..818369331b6ebea37e9274da3370b717da3ca6d4 100644 (file)
@@ -45,4 +45,5 @@ return [
 
     // Other
     'commented_on'                => 'okomentoval/a',
+    'permissions_update'          => 'updated permissions',
 ];
index 719d8cbc10aeba2935927cd157ae1353428e08ce..983239c82648fd2fa1ea8e437ff61d7a661ded14 100644 (file)
@@ -22,6 +22,7 @@ return [
     'meta_created_name' => 'Vytvořeno :timeLength uživatelem :user',
     'meta_updated' => 'Aktualizováno :timeLength',
     'meta_updated_name' => 'Aktualizováno :timeLength uživatelem :user',
+    'meta_owned_name' => 'Owned by :user',
     'entity_select' => 'Výběr entity',
     'images' => 'Obrázky',
     'my_recent_drafts' => 'Mé nedávné koncepty',
@@ -39,6 +40,7 @@ return [
     'permissions_intro' => 'Pokud je povoleno, tato oprávnění budou mít přednost před všemi nastavenými oprávněními role.',
     'permissions_enable' => 'Povolit vlastní oprávnění',
     'permissions_save' => 'Uložit oprávnění',
+    'permissions_owner' => 'Owner',
 
     // Search
     'search_results' => 'Výsledky hledání',
@@ -146,7 +148,7 @@ return [
     'chapters_create' => 'Vytvořit novou kapitolu',
     'chapters_delete' => 'Smazat kapitolu',
     'chapters_delete_named' => 'Smazat kapitolu :chapterName',
-    'chapters_delete_explain' => 'Kapitola \':chapterName\' bude smazána. Všechny stránky v ní obsažené budou přesunuty přímo pod samotnou knihu.',
+    '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' => 'Opravdu chcete tuto kapitolu smazat?',
     'chapters_edit' => 'Upravit kapitolu',
     'chapters_edit_named' => 'Upravit kapitolu :chapterName',
@@ -208,6 +210,7 @@ return [
     'pages_revisions' => 'Revize stránky',
     'pages_revisions_named' => 'Revize stránky pro :pageName',
     'pages_revision_named' => 'Revize stránky pro :pageName',
+    'pages_revision_restored_from' => 'Restored from #:id; :summary',
     'pages_revisions_created_by' => 'Vytvořeno uživatelem',
     'pages_revisions_date' => 'Datum revize',
     'pages_revisions_number' => 'Č.',
index d499bff5e5f93f3123d2d5bebc97b6f84e6fe9e6..80aa6dc385a445c9c0abf5b99bde240d2ee60e41 100644 (file)
@@ -68,7 +68,7 @@ return [
     'maint' => 'Údržba',
     'maint_image_cleanup' => 'Pročistění obrázků',
     'maint_image_cleanup_desc' => "Prohledá stránky a jejich revize, aby zjistil, které obrázky a kresby jsou momentálně používány a které jsou zbytečné. Zajistěte plnou zálohu databáze a obrázků než se do toho pustíte.",
-    'maint_image_cleanup_ignore_revisions' => 'Ignorovat obrázky v revizích',
+    'maint_delete_images_only_in_revisions' => 'Also delete images that only exist in old page revisions',
     'maint_image_cleanup_run' => 'Spustit pročištění',
     'maint_image_cleanup_warning' => 'Nalezeno :count potenciálně nepoužitých obrázků. Jste si jistí, že je chcete smazat?',
     'maint_image_cleanup_success' => 'Potenciálně nepoužité obrázky byly smazány. Celkem :count.',
@@ -80,6 +80,27 @@ return [
     'maint_send_test_email_mail_subject' => 'Testovací e-mail',
     'maint_send_test_email_mail_greeting' => 'Zdá se, že posílání e-mailů funguje!',
     'maint_send_test_email_mail_text' => 'Gratulujeme! Protože jste dostali tento e-mail, zdá se, že nastavení e-mailů je v pořádku.',
+    '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' => 'Protokol auditu',
@@ -90,7 +111,7 @@ return [
     'audit_deleted_item_name' => 'Jméno: :name',
     'audit_table_user' => 'Uživatel',
     'audit_table_event' => 'Událost',
-    'audit_table_item' => 'Související položka',
+    'audit_table_related' => 'Related Item or Detail',
     'audit_table_date' => 'Datum aktivity',
     'audit_date_from' => 'Časový rozsah od',
     'audit_date_to' => 'Časový rozsah do',
@@ -136,6 +157,7 @@ return [
     'user_profile' => 'Profil uživatele',
     'users_add_new' => 'Přidat nového uživatele',
     'users_search' => 'Vyhledávání uživatelů',
+    'users_latest_activity' => 'Latest Activity',
     'users_details' => 'Údaje o uživateli',
     'users_details_desc' => 'Nastavte zobrazované jméno a e-mailovou adresu pro tohoto uživatele. E-mailová adresa bude použita pro přihlášení do aplikace.',
     'users_details_desc_no_email' => 'Nastavte zobrazované jméno pro tohoto uživatele, aby jej ostatní uživatele poznali.',
@@ -153,7 +175,10 @@ return [
     'users_delete_named' => 'Odstranit uživatele :userName',
     'users_delete_warning' => 'Uživatel \':userName\' bude zcela smazán ze systému.',
     'users_delete_confirm' => 'Opravdu chcete tohoto uživatele smazat?',
-    'users_delete_success' => 'Uživatel byl úspěšně smazán',
+    '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' => 'Upravit uživatele',
     'users_edit_profile' => 'Upravit profil',
     'users_edit_success' => 'Uživatel byl úspěšně aktualizován',
@@ -214,6 +239,7 @@ return [
         'ja' => '日本語',
         'ko' => '한국어',
         'nl' => 'Nederlands',
+        'nb' => 'Norsk (Bokmål)',
         'pl' => 'Polski',
         'pt_BR' => 'Português do Brasil',
         'ru' => 'Русский',
index 9fc2f38ecbf59fb738ac18fd6d7376a0ae80438a..a843973497637e5b2905c98021e0b115fa4e17c9 100644 (file)
@@ -90,6 +90,7 @@ return [
     'required_without'     => ':attribute musí být vyplněno pokud :values není vyplněno.',
     'required_without_all' => ':attribute musí být vyplněno pokud není žádné z :values zvoleno.',
     'same'                 => ':attribute a :other se musí shodovat.',
+    'safe_url'             => 'The provided link may not be safe.',
     'size'                 => [
         'numeric' => ':attribute musí být přesně :size.',
         'file'    => ':attribute musí mít přesně :size Kilobytů.',
index a72e9210bb15fc25669725f698e77933a30759ba..264deec609721fd7aafcabf40a3b1b582d10d81b 100644 (file)
@@ -45,4 +45,5 @@ return [
 
     // Other
     'commented_on'                => 'kommenterede til',
+    'permissions_update'          => 'updated permissions',
 ];
index a38ac83714834f42fb714982f9cb80b30117f67a..fee4d4134d74d50fc5bc1b8083ee33a140611b61 100644 (file)
@@ -22,6 +22,7 @@ return [
     'meta_created_name' => 'Oprettet :timeLength af :user',
     'meta_updated' => 'Opdateret :timeLength',
     'meta_updated_name' => 'Opdateret :timeLength af :user',
+    'meta_owned_name' => 'Owned by :user',
     'entity_select' => 'Vælg emne',
     'images' => 'Billeder',
     'my_recent_drafts' => 'Mine seneste kladder',
@@ -39,6 +40,7 @@ return [
     'permissions_intro' => 'Når de er aktiveret, vil disse tilladelser have prioritet over alle indstillede rolletilladelser.',
     'permissions_enable' => 'Aktivér tilpassede tilladelser',
     'permissions_save' => 'Gem tilladelser',
+    'permissions_owner' => 'Owner',
 
     // Search
     'search_results' => 'Søgeresultater',
@@ -146,7 +148,7 @@ return [
     'chapters_create' => 'Opret nyt kapitel',
     'chapters_delete' => 'Slet kapitel',
     'chapters_delete_named' => 'Slet kapitel :chapterName',
-    'chapters_delete_explain' => 'Dette vil slette kapitlet med navnet \':chapterName\'. Alle sider fjernes og tilføjes direkte til den tilhørende bog.',
+    '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' => 'Er du sikker på du vil slette dette kapitel?',
     'chapters_edit' => 'Rediger kapitel',
     'chapters_edit_named' => 'Rediger kapitel :chapterName',
@@ -208,6 +210,7 @@ return [
     'pages_revisions' => 'Sidserevisioner',
     'pages_revisions_named' => 'Siderevisioner for :pageName',
     'pages_revision_named' => 'Siderevision for :pageName',
+    'pages_revision_restored_from' => 'Restored from #:id; :summary',
     'pages_revisions_created_by' => 'Oprettet af',
     'pages_revisions_date' => 'Revisionsdato',
     'pages_revisions_number' => '#',
index 0cb62a186875e491b1892a9bd808b37fc84c8449..80ebcb1239fda4ccfa4bab8972cb81206495a088 100644 (file)
@@ -68,7 +68,7 @@ return [
     'maint' => 'Vedligeholdelse',
     'maint_image_cleanup' => 'Ryd op i billeder',
     'maint_image_cleanup_desc' => "Scanner side & revisionsindhold for at kontrollere, hvilke billeder og tegninger, der i øjeblikket er i brug, og hvilke billeder, der er overflødige. Sørg for, at du opretter en komplet database og billedbackup, før du kører dette.",
-    'maint_image_cleanup_ignore_revisions' => 'Ignorer billeder i revisioner',
+    'maint_delete_images_only_in_revisions' => 'Also delete images that only exist in old page revisions',
     'maint_image_cleanup_run' => 'Kør Oprydning',
     'maint_image_cleanup_warning' => 'der blev fundet :count potentielt ubrugte billeder. Er du sikker på, at du vil slette disse billeder?',
     'maint_image_cleanup_success' => ':count: potentielt ubrugte billeder fundet og slettet!',
@@ -80,6 +80,27 @@ return [
     'maint_send_test_email_mail_subject' => 'Test E-Mail',
     'maint_send_test_email_mail_greeting' => 'E-Mail levering ser ud til at virke!',
     'maint_send_test_email_mail_text' => 'Tillykke! Da du har modtaget denne mailnotifikation, ser det ud som om, at dine mailindstillinger er opsat korrekt.',
+    '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' => 'Brugerprofil',
     'users_add_new' => 'Tilføj ny bruger',
     'users_search' => 'Søg efter brugere',
+    'users_latest_activity' => 'Latest Activity',
     'users_details' => 'Brugeroplysninger',
     'users_details_desc' => 'Angiv et visningsnavn og en E-Mail-adresse for denne bruger. E-Mail-adressen bruges til at logge ind på applikationen.',
     'users_details_desc_no_email' => 'Sætter et visningsnavn for denne bruger, så andre kan genkende dem.',
@@ -153,7 +175,10 @@ return [
     'users_delete_named' => 'Slet bruger :userName',
     'users_delete_warning' => 'Dette vil helt slette denne bruger med navnet \':userName\' fra systemet.',
     'users_delete_confirm' => 'Er du sikker på, at du vil slette denne bruger?',
-    'users_delete_success' => 'Brugere blev fjernet',
+    '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' => 'Rediger bruger',
     'users_edit_profile' => 'Rediger profil',
     'users_edit_success' => 'Bruger suscesfuldt opdateret',
@@ -214,6 +239,7 @@ return [
         'ja' => '日本語',
         'ko' => '한국어',
         'nl' => 'Nederlands',
+        'nb' => 'Norsk (Bokmål)',
         'pl' => 'Polski',
         'pt_BR' => 'Português do Brasil',
         'ru' => 'Русский',
index 79ed1bdf455aa67c10e7a2b2704e4ec7cbc20347..7647132ca114134ac8370228802d2c06edd27730 100644 (file)
@@ -90,6 +90,7 @@ return [
     'required_without'     => ':attribute skal udfyldes når :values ikke er udfyldt.',
     'required_without_all' => ':attribute skal udfyldes når ingen af :values er udfyldt.',
     'same'                 => ':attribute og :other skal være ens.',
+    'safe_url'             => 'The provided link may not be safe.',
     'size'                 => [
         'numeric' => ':attribute skal være :size.',
         'file'    => ':attribute skal være :size kilobytes.',
index fadf5d6380caee444c365db2c8117f8434eabdb9..562a9add365decee4f8d5fe06b37dcd91f35dd0c 100644 (file)
@@ -6,43 +6,44 @@
 return [
 
     // Pages
-    'page_create'                 => 'erstellte Seite',
-    'page_create_notification'    => 'Die Seite wurde erfolgreich erstellt.',
-    'page_update'                 => 'aktualisierte Seite',
-    'page_update_notification'    => 'Die Seite wurde erfolgreich aktualisiert.',
-    'page_delete'                 => 'gelöschte Seite',
-    'page_delete_notification'    => 'Die Seite wurde erfolgreich gelöscht.',
-    'page_restore'                => 'wiederhergestellte Seite',
-    'page_restore_notification'   => 'Die Seite wurde erfolgreich wiederhergestellt.',
-    'page_move'                   => 'Seite verschoben',
+    'page_create'                 => 'hat die Seite erstellt',
+    'page_create_notification'    => 'Die Seite wurde erfolgreich erstellt',
+    'page_update'                 => 'hat die Seite aktualisiert',
+    'page_update_notification'    => 'Die Seite wurde erfolgreich aktualisiert',
+    'page_delete'                 => 'hat die Seite gelöscht',
+    'page_delete_notification'    => 'Die Seite wurde erfolgreich gelöscht',
+    'page_restore'                => 'hat die Seite wiederhergestellt',
+    'page_restore_notification'   => 'Die Seite wurde erfolgreich wiederhergestellt',
+    'page_move'                   => 'hat die Seite verschoben',
 
     // Chapters
-    'chapter_create'              => 'erstellte Kapitel',
-    'chapter_create_notification' => 'Das Kapitel wurde erfolgreich erstellt.',
-    'chapter_update'              => 'aktualisierte Kapitel',
-    'chapter_update_notification' => 'Das Kapitel wurde erfolgreich aktualisiert.',
-    'chapter_delete'              => 'löschte Kapitel',
-    'chapter_delete_notification' => 'Das Kapitel wurde erfolgreich gelöscht.',
-    'chapter_move'                => 'verschob Kapitel',
+    'chapter_create'              => 'hat das Kapitel erstellt',
+    'chapter_create_notification' => 'Das Kapitel wurde erfolgreich erstellt',
+    'chapter_update'              => 'hat das Kapitel geändert',
+    'chapter_update_notification' => 'Das Kapitel wurde erfolgreich aktualisiert',
+    'chapter_delete'              => 'hat das Kapitel gelöscht',
+    'chapter_delete_notification' => 'Das Kapitel wurde erfolgreich gelöscht',
+    'chapter_move'                => 'hat das Kapitel verschoben',
 
     // Books
-    'book_create'                 => 'erstellte Buch',
-    'book_create_notification'    => 'Das Buch wurde erfolgreich erstellt.',
-    'book_update'                 => 'aktualisierte Buch',
-    'book_update_notification'    => 'Das Buch wurde erfolgreich aktualisiert.',
-    'book_delete'                 => 'löschte Buch',
-    'book_delete_notification'    => 'Das Buch wurde erfolgreich gelöscht.',
-    'book_sort'                   => 'sortierte Buch',
-    'book_sort_notification'      => 'Das Buch wurde erfolgreich umsortiert.',
+    'book_create'                 => 'hat das Buch erstellt',
+    'book_create_notification'    => 'Das Buch wurde erfolgreich erstellt',
+    'book_update'                 => 'hat das Buch aktualisiert',
+    'book_update_notification'    => 'Das Buch wurde erfolgreich aktualisiert',
+    'book_delete'                 => 'hat das Buch gelöscht',
+    'book_delete_notification'    => 'Das Buch wurde erfolgreich gelöscht',
+    'book_sort'                   => 'hat die Buch-Sortierung geändert',
+    'book_sort_notification'      => 'Das Buch wurde erfolgreich umsortiert',
 
     // Bookshelves
-    'bookshelf_create'            => 'erstellt Bücherregal',
+    'bookshelf_create'            => 'hat das Bücherregal erstellt',
     'bookshelf_create_notification'    => 'Das Bücherregal wurde erfolgreich erstellt',
-    'bookshelf_update'                 => 'aktualisiert Bücherregal',
-    'bookshelf_update_notification'    => 'Das Bücherregal wurde erfolgreich aktualisiert',
-    'bookshelf_delete'                 => 'löscht Bücherregal',
+    'bookshelf_update'                 => 'hat das Bücherregal geändert',
+    'bookshelf_update_notification'    => 'Das Bücherregal wurde erfolgreich geändert',
+    'bookshelf_delete'                 => 'hat das Bücherregal gelöscht',
     'bookshelf_delete_notification'    => 'Das Bücherregal wurde erfolgreich gelöscht',
 
     // Other
-    'commented_on'                => 'kommentiert',
+    'commented_on'                => 'hat einen Kommentar hinzugefügt',
+    'permissions_update'          => 'hat die Berechtigungen aktualisiert',
 ];
index 73f7ef6e07f34d682a4e088ec3373844ad940e4d..2ea11a6cf979faed1f6e4c77aec02d6fe31ed722 100644 (file)
@@ -22,6 +22,7 @@ return [
     'meta_created_name' => 'Erstellt: :timeLength von :user',
     'meta_updated' => 'Zuletzt aktualisiert: :timeLength',
     'meta_updated_name' => 'Zuletzt aktualisiert: :timeLength von :user',
+    'meta_owned_name' => 'Owned by :user',
     'entity_select' => 'Eintrag auswählen',
     'images' => 'Bilder',
     'my_recent_drafts' => 'Meine kürzlichen Entwürfe',
@@ -39,6 +40,7 @@ return [
     'permissions_intro' => 'Wenn individuelle Berechtigungen aktiviert werden, überschreiben diese Einstellungen durch Rollen zugewiesene Berechtigungen.',
     'permissions_enable' => 'Individuelle Berechtigungen aktivieren',
     'permissions_save' => 'Berechtigungen speichern',
+    'permissions_owner' => 'Owner',
 
     // Search
     'search_results' => 'Suchergebnisse',
@@ -146,7 +148,7 @@ return [
     'chapters_create' => 'Neues Kapitel anlegen',
     'chapters_delete' => 'Kapitel entfernen',
     'chapters_delete_named' => 'Kapitel ":chapterName" entfernen',
-    'chapters_delete_explain' => 'Das Kapitel ":chapterName" wird gelöscht und alle zugehörigen Seiten dem übergeordneten Buch zugeordnet.',
+    'chapters_delete_explain' => 'Dies löscht das Kapitel mit dem Namen \':chapterName\'. Alle Seiten, die innerhalb dieses Kapitels existieren, werden ebenfalls gelöscht.',
     'chapters_delete_confirm' => 'Sind Sie sicher, dass Sie dieses Kapitel löschen möchten?',
     'chapters_edit' => 'Kapitel bearbeiten',
     'chapters_edit_named' => 'Kapitel ":chapterName" bearbeiten',
@@ -208,6 +210,7 @@ return [
     'pages_revisions' => 'Seitenversionen',
     'pages_revisions_named' => 'Seitenversionen von ":pageName"',
     'pages_revision_named' => 'Seitenversion von ":pageName"',
+    'pages_revision_restored_from' => 'Restored from #:id; :summary',
     'pages_revisions_created_by' => 'Erstellt von',
     'pages_revisions_date' => 'Versionsdatum',
     'pages_revisions_number' => '#',
index 226eb2ce33a0046f8055197bd33b37bea34bf35c..60d4361d9e851225b235b46315ab0070f185ce34 100644 (file)
@@ -71,7 +71,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 
     'maint' => 'Wartung',
     'maint_image_cleanup' => 'Bilder bereinigen',
     'maint_image_cleanup_desc' => "Überprüft Seiten- und Versionsinhalte auf ungenutzte und mehrfach vorhandene Bilder. Erstellen Sie vor dem Start ein Backup Ihrer Datenbank und Bilder.",
-    'maint_image_cleanup_ignore_revisions' => 'Bilder in Versionen ignorieren',
+    'maint_delete_images_only_in_revisions' => 'Also delete images that only exist in old page revisions',
     'maint_image_cleanup_run' => 'Reinigung starten',
     'maint_image_cleanup_warning' => ':count eventuell unbenutze Bilder wurden gefunden. Möchten Sie diese Bilder löschen?',
     'maint_image_cleanup_success' => ':count eventuell unbenutze Bilder wurden gefunden und gelöscht.',
@@ -83,6 +83,27 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 
     'maint_send_test_email_mail_subject' => 'Test E-Mail',
     'maint_send_test_email_mail_greeting' => 'E-Mail-Versand scheint zu funktionieren!',
     'maint_send_test_email_mail_text' => 'Glückwunsch! Da Sie diese E-Mail Benachrichtigung erhalten haben, scheinen Ihre E-Mail-Einstellungen korrekt konfiguriert zu sein.',
+    'maint_recycle_bin_desc' => 'Gelöschte Regale, Bücher, Kapitel & Seiten werden in den Papierkorb verschoben, so dass sie wiederhergestellt oder dauerhaft gelöscht werden können. Ältere Gegenstände im Papierkorb können, in Abhängigkeit von der Systemkonfiguration, nach einer Weile automatisch entfernt werden.',
+    'maint_recycle_bin_open' => 'Papierkorb öffnen',
+
+    // Recycle Bin
+    'recycle_bin' => 'Papierkorb',
+    'recycle_bin_desc' => 'Hier können Sie gelöschte Elemente wiederherstellen oder sie dauerhaft aus dem System entfernen. Diese Liste ist nicht gefiltert, im Gegensatz zu ähnlichen Aktivitätslisten im System, wo Berechtigungsfilter angewendet werden.',
+    'recycle_bin_deleted_item' => 'Gelöschtes Element',
+    'recycle_bin_deleted_by' => 'Gelöscht von',
+    'recycle_bin_deleted_at' => 'Löschzeitpunkt',
+    'recycle_bin_permanently_delete' => 'Dauerhaft löschen',
+    'recycle_bin_restore' => 'Wiederherstellen',
+    'recycle_bin_contents_empty' => 'Der Papierkorb ist derzeit leer',
+    'recycle_bin_empty' => 'Papierkorb leeren',
+    'recycle_bin_empty_confirm' => 'Dies wird alle Gegenstände im Papierkorb dauerhaft entfernen, einschließlich der Inhalte, die darin enthalten sind. Sind Sie sicher, dass Sie den Papierkorb leeren möchten?',
+    'recycle_bin_destroy_confirm' => 'Diese Aktion wird dieses Element zusammen mit allen unten aufgeführten Unterelementen dauerhaft aus dem System löschen und Sie werden nicht in der Lage sein, diesen Inhalt wiederherzustellen. Sind Sie sicher, dass Sie dieses Element endgültig löschen möchten?',
+    'recycle_bin_destroy_list' => 'Zu löschende Elemente',
+    'recycle_bin_restore_list' => 'Zu wiederherzustellende Elemente',
+    'recycle_bin_restore_confirm' => 'Mit dieser Aktion wird das gelöschte Element einschließlich aller untergeordneten Elemente an seinen ursprünglichen Ort wiederherstellen. Wenn der ursprüngliche Ort gelöscht wurde und sich nun im Papierkorb befindet, muss auch das übergeordnete Element wiederhergestellt werden.',
+    'recycle_bin_restore_deleted_parent' => 'Das übergeordnete Elements wurde ebenfalls gelöscht. Dieses Element wird weiterhin als gelöscht zählen, bis auch das übergeordnete Element wiederhergestellt wurde.',
+    'recycle_bin_destroy_notification' => ':count Elemente wurden aus dem Papierkorb gelöscht.',
+    'recycle_bin_restore_notification' => ':count Elemente wurden aus dem Papierkorb wiederhergestellt.',
 
     // Audit Log
     'audit' => 'Audit-Protokoll',
@@ -93,7 +114,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 
     'audit_deleted_item_name' => 'Name: :name',
     'audit_table_user' => 'Benutzer',
     'audit_table_event' => 'Ereignis',
-    'audit_table_item' => 'Verwendetes Objekt',
+    'audit_table_related' => 'Verknüpftes Element oder Detail',
     'audit_table_date' => 'Aktivitätsdatum',
     'audit_date_from' => 'Zeitraum von',
     'audit_date_to' => 'Zeitraum bis',
@@ -139,6 +160,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 
     'user_profile' => 'Benutzerprofil',
     'users_add_new' => 'Benutzer hinzufügen',
     'users_search' => 'Benutzer suchen',
+    'users_latest_activity' => 'Neueste Aktivitäten',
     'users_details' => 'Benutzerdetails',
     'users_details_desc' => 'Legen Sie für diesen Benutzer einen Anzeigenamen und eine E-Mail-Adresse fest. Die E-Mail-Adresse wird bei der Anmeldung verwendet.',
     'users_details_desc_no_email' => 'Legen Sie für diesen Benutzer einen Anzeigenamen fest, damit andere ihn erkennen können.',
@@ -156,7 +178,10 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 
     'users_delete_named' => 'Benutzer ":userName" löschen',
     'users_delete_warning' => 'Der Benutzer ":userName" wird aus dem System gelöscht.',
     'users_delete_confirm' => 'Sind Sie sicher, dass Sie diesen Benutzer löschen möchten?',
-    'users_delete_success' => 'Benutzer erfolgreich gelöscht.',
+    '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' => 'Benutzer bearbeiten',
     'users_edit_profile' => 'Profil bearbeiten',
     'users_edit_success' => 'Benutzer erfolgreich aktualisisert',
@@ -217,6 +242,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 
         'ja' => '日本語',
         'ko' => '한국어',
         'nl' => 'Nederlands',
+        'nb' => 'Norsk (Bokmål)',
         'pl' => 'Polski',
         'pt_BR' => 'Português do Brasil',
         'ru' => 'Русский',
index b6105d192858da66ce689644cfc6ef6cee60a34e..8b1febc85d8da7078606d6c0e76f8f8a6364db2e 100644 (file)
@@ -90,6 +90,7 @@ return [
     'required_without'     => ':attribute ist erforderlich, wenn :values nicht vorhanden ist.',
     'required_without_all' => ':attribute ist erforderlich, wenn :values nicht vorhanden sind.',
     'same'                 => ':attribute und :other müssen übereinstimmen.',
+    'safe_url'             => 'Der angegebene Link ist möglicherweise nicht sicher.',
     'size'                 => [
         'numeric' => ':attribute muss :size sein.',
         'file'    => ':attribute muss :size Kilobytes groß sein.',
index 170a1910825ac8637a702183c274b87f55dda810..01c54bf4f714b3ba616da646e72d32115245653e 100644 (file)
@@ -45,4 +45,5 @@ return [
 
     // Other
     'commented_on'                => 'kommentiert',
+    'permissions_update'          => 'hat die Berechtigungen aktualisiert',
 ];
index d03504b00c4fb3f0127171b98e1aadb2b187048d..b899324732faebf2f0497981f879e586a921ed3b 100644 (file)
@@ -22,6 +22,7 @@ return [
     'meta_created_name' => 'Erstellt: :timeLength von :user',
     'meta_updated' => 'Zuletzt aktualisiert: :timeLength',
     'meta_updated_name' => 'Zuletzt aktualisiert: :timeLength von :user',
+    'meta_owned_name' => 'Owned by :user',
     'entity_select' => 'Eintrag auswählen',
     'images' => 'Bilder',
     'my_recent_drafts' => 'Meine kürzlichen Entwürfe',
@@ -39,6 +40,7 @@ return [
     'permissions_intro' => 'Wenn individuelle Berechtigungen aktiviert werden, überschreiben diese Einstellungen durch Rollen zugewiesene Berechtigungen.',
     'permissions_enable' => 'Individuelle Berechtigungen aktivieren',
     'permissions_save' => 'Berechtigungen speichern',
+    'permissions_owner' => 'Owner',
 
     // Search
     'search_results' => 'Suchergebnisse',
@@ -146,7 +148,7 @@ return [
     'chapters_create' => 'Neues Kapitel anlegen',
     'chapters_delete' => 'Kapitel entfernen',
     'chapters_delete_named' => 'Kapitel ":chapterName" entfernen',
-    'chapters_delete_explain' => 'Das Kapitel ":chapterName" wird gelöscht und alle zugehörigen Seiten dem übergeordneten Buch zugeordnet.',
+    'chapters_delete_explain' => 'Dies löscht das Kapitel mit dem Namen \':chapterName\'. Alle Seiten, die innerhalb dieses Kapitels existieren, werden ebenfalls gelöscht.',
     'chapters_delete_confirm' => 'Bist Du sicher, dass Du dieses Kapitel löschen möchtest?',
     'chapters_edit' => 'Kapitel bearbeiten',
     'chapters_edit_named' => 'Kapitel ":chapterName" bearbeiten',
@@ -208,6 +210,7 @@ return [
     'pages_revisions' => 'Seitenversionen',
     'pages_revisions_named' => 'Seitenversionen von ":pageName"',
     'pages_revision_named' => 'Seitenversion von ":pageName"',
+    'pages_revision_restored_from' => 'Restored from #:id; :summary',
     'pages_revisions_created_by' => 'Erstellt von',
     'pages_revisions_date' => 'Versionsdatum',
     'pages_revisions_number' => '#',
index 0f7ebd7da8ce048f166e07ae7210d775ab9fc52c..d0caef9ee9b81422ff56865d259e1a918c866f1e 100644 (file)
@@ -71,7 +71,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 
     'maint' => 'Wartung',
     'maint_image_cleanup' => 'Bilder bereinigen',
     'maint_image_cleanup_desc' => "Überprüft Seiten- und Versionsinhalte auf ungenutzte und mehrfach vorhandene Bilder. Erstelle vor dem Start ein Backup Deiner Datenbank und Bilder.",
-    'maint_image_cleanup_ignore_revisions' => 'Bilder in Versionen ignorieren',
+    'maint_delete_images_only_in_revisions' => 'Also delete images that only exist in old page revisions',
     'maint_image_cleanup_run' => 'Reinigung starten',
     'maint_image_cleanup_warning' => ':count eventuell unbenutze Bilder wurden gefunden. Möchtest Du diese Bilder löschen?',
     'maint_image_cleanup_success' => ':count eventuell unbenutze Bilder wurden gefunden und gelöscht.',
@@ -83,6 +83,27 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 
     'maint_send_test_email_mail_subject' => 'Test E-Mail',
     'maint_send_test_email_mail_greeting' => 'E-Mail-Versand scheint zu funktionieren!',
     'maint_send_test_email_mail_text' => 'Glückwunsch! Da du diese E-Mail Benachrichtigung erhalten hast, scheinen deine E-Mail-Einstellungen korrekt konfiguriert zu sein.',
+    'maint_recycle_bin_desc' => 'Gelöschte Regale, Bücher, Kapitel & Seiten werden in den Papierkorb verschoben, so dass sie wiederhergestellt oder dauerhaft gelöscht werden können. Ältere Gegenstände im Papierkorb können, in Abhängigkeit von der Systemkonfiguration, nach einer Weile automatisch entfernt werden.',
+    'maint_recycle_bin_open' => 'Papierkorb öffnen',
+
+    // Recycle Bin
+    'recycle_bin' => 'Papierkorb',
+    'recycle_bin_desc' => 'Hier können Sie gelöschte Elemente wiederherstellen oder sie dauerhaft aus dem System entfernen. Diese Liste ist nicht gefiltert, im Gegensatz zu ähnlichen Aktivitätslisten im System, wo Berechtigungsfilter angewendet werden.',
+    'recycle_bin_deleted_item' => 'Gelöschtes Element',
+    'recycle_bin_deleted_by' => 'Gelöscht von',
+    'recycle_bin_deleted_at' => 'Löschzeitpunkt',
+    'recycle_bin_permanently_delete' => 'Dauerhaft löschen',
+    'recycle_bin_restore' => 'Wiederherstellen',
+    'recycle_bin_contents_empty' => 'Der Papierkorb ist derzeit leer',
+    'recycle_bin_empty' => 'Papierkorb leeren',
+    'recycle_bin_empty_confirm' => 'Dies wird alle Gegenstände im Papierkorb dauerhaft entfernen, einschließlich der Inhalte, die darin enthalten sind. Sind Sie sicher, dass Sie den Papierkorb leeren möchten?',
+    'recycle_bin_destroy_confirm' => 'Diese Aktion wird dieses Element zusammen mit allen unten aufgeführten Unterelementen dauerhaft aus dem System löschen und Sie werden nicht in der Lage sein, diesen Inhalt wiederherzustellen. Sind Sie sicher, dass Sie dieses Element endgültig löschen möchten?',
+    'recycle_bin_destroy_list' => 'Zu löschende Elemente',
+    'recycle_bin_restore_list' => 'Zu wiederherzustellende Elemente',
+    'recycle_bin_restore_confirm' => 'Mit dieser Aktion wird das gelöschte Element einschließlich aller untergeordneten Elemente an seinen ursprünglichen Ort wiederherstellen. Wenn der ursprüngliche Ort gelöscht wurde und sich nun im Papierkorb befindet, muss auch das übergeordnete Element wiederhergestellt werden.',
+    'recycle_bin_restore_deleted_parent' => 'Das übergeordnete Elements wurde ebenfalls gelöscht. Dieses Element wird weiterhin als gelöscht zählen, bis auch das übergeordnete Element wiederhergestellt wurde.',
+    'recycle_bin_destroy_notification' => ':count Elemente wurden aus dem Papierkorb gelöscht.',
+    'recycle_bin_restore_notification' => ':count Elemente wurden aus dem Papierkorb wiederhergestellt.',
 
     // Audit Log
     'audit' => 'Audit-Protokoll',
@@ -93,7 +114,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 
     'audit_deleted_item_name' => 'Name: :name',
     'audit_table_user' => 'Benutzer',
     'audit_table_event' => 'Ereignis',
-    'audit_table_item' => 'Verwendetes Objekt',
+    'audit_table_related' => 'Verknüpftes Element oder Detail',
     'audit_table_date' => 'Aktivitätsdatum',
     'audit_date_from' => 'Zeitraum von',
     'audit_date_to' => 'Zeitraum bis',
@@ -139,6 +160,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 
     'user_profile' => 'Benutzerprofil',
     'users_add_new' => 'Benutzer hinzufügen',
     'users_search' => 'Benutzer suchen',
+    'users_latest_activity' => 'Neueste Aktivitäten',
     'users_details' => 'Benutzerdetails',
     'users_details_desc' => 'Legen Sie für diesen Benutzer einen Anzeigenamen und eine E-Mail-Adresse fest. Die E-Mail-Adresse wird bei der Anmeldung verwendet.',
     'users_details_desc_no_email' => 'Legen Sie für diesen Benutzer einen Anzeigenamen fest, damit andere ihn erkennen können.',
@@ -156,7 +178,10 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 
     'users_delete_named' => 'Benutzer ":userName" löschen',
     'users_delete_warning' => 'Der Benutzer ":userName" wird aus dem System gelöscht.',
     'users_delete_confirm' => 'Bist Du sicher, dass Du diesen Benutzer löschen möchtest?',
-    'users_delete_success' => 'Benutzer erfolgreich gelöscht.',
+    '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' => 'Benutzer bearbeiten',
     'users_edit_profile' => 'Profil bearbeiten',
     'users_edit_success' => 'Benutzer erfolgreich aktualisisert',
@@ -217,6 +242,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 
         'ja' => '日本語',
         'ko' => '한국어',
         'nl' => 'Nederlands',
+        'nb' => 'Norsk (Bokmål)',
         'pl' => 'Polski',
         'pt_BR' => 'Português do Brasil',
         'ru' => 'Русский',
index 4be38446889ce05c2ded5504e498f23083fdd215..24c1e8addb8e224e4ce848427a3da45a3ff49e70 100644 (file)
@@ -90,6 +90,7 @@ return [
     'required_without'     => ':attribute ist erforderlich, wenn :values nicht vorhanden ist.',
     'required_without_all' => ':attribute ist erforderlich, wenn :values nicht vorhanden sind.',
     'same'                 => ':attribute und :other müssen übereinstimmen.',
+    'safe_url'             => 'Der angegebene Link ist möglicherweise nicht sicher.',
     'size'                 => [
         'numeric' => ':attribute muss :size sein.',
         'file'    => ':attribute muss :size Kilobytes groß sein.',
index 4cac54b2a706efa35cb8873ebc247c20420e65b5..fe937b061930262a060465699446647adab763d9 100644 (file)
@@ -45,4 +45,5 @@ return [
 
     // Other
     'commented_on'                => 'commented on',
+    'permissions_update'          => 'updated permissions',
 ];
index e87bd11a5e343173fadf78e62a042e1ca0579a4a..e048db90f4c2ff13d17a5fe29d449dac302c95e2 100644 (file)
@@ -77,4 +77,9 @@ return [
     // Email Content
     'email_action_help' => 'If you’re having trouble clicking the ":actionText" button, copy and paste the URL below into your web browser:',
     'email_rights' => 'All rights reserved',
+
+    // Footer Link Options
+    // Not directly used but available for convenience to users.
+    'privacy_policy' => 'Privacy Policy',
+    'terms_of_service' => 'Terms of Service',
 ];
index f64867a56c31736a1730d58c51f3fe0c088364d1..6c0980fab380d430f50d12ca8928f69be522b203 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',
@@ -208,6 +210,7 @@ return [
     'pages_revisions' => 'Page Revisions',
     'pages_revisions_named' => 'Page Revisions for :pageName',
     'pages_revision_named' => 'Page Revision for :pageName',
+    'pages_revision_restored_from' => 'Restored from #:id; :summary',
     'pages_revisions_created_by' => 'Created By',
     'pages_revisions_date' => 'Revision Date',
     'pages_revisions_number' => '#',
index 269c775ba868639e4de9abf9a73f91fb5e9e2ef7..bd55668a2e94e59b13f395b9fe0655fb28790889 100755 (executable)
@@ -37,6 +37,11 @@ return [
     'app_homepage' => 'Application Homepage',
     'app_homepage_desc' => 'Select a view to show on the homepage instead of the default view. Page permissions are ignored for selected pages.',
     'app_homepage_select' => 'Select a page',
+    'app_footer_links' => 'Footer Links',
+    'app_footer_links_desc' => 'Add links to show within the site footer. These will be displayed at the bottom of most pages, including those that do not require login. You can use a label of "trans::<key>" to use system-defined translations. For example: Using "trans::common.privacy_policy" will provide the translated text "Privacy Policy" and "trans::common.terms_of_service" will provide the translated text "Terms of Service".',
+    'app_footer_links_label' => 'Link Label',
+    'app_footer_links_url' => 'Link URL',
+    'app_footer_links_add' => 'Add Footer Link',
     'app_disable_comments' => 'Disable Comments',
     'app_disable_comments_toggle' => 'Disable comments',
     'app_disable_comments_desc' => 'Disables comments across all pages in the application. <br> Existing comments are not shown.',
@@ -68,7 +73,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!',
@@ -111,7 +116,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',
@@ -157,6 +162,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.',
@@ -174,7 +180,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',
@@ -235,6 +244,7 @@ return [
         'ja' => '日本語',
         'ko' => '한국어',
         'nl' => 'Nederlands',
+        'nb' => 'Norsk (Bokmål)',
         'pl' => 'Polski',
         'pt_BR' => 'Português do Brasil',
         'ru' => 'Русский',
index e89b69fd385b337917d22d9468249d183d012c79..b67781e18969774d39ed646fcabfb5c25335ea66 100644 (file)
@@ -45,4 +45,5 @@ return [
 
     // Other
     'commented_on'                => 'comentada el',
+    'permissions_update'          => 'permisos actualizados',
 ];
index aac632419aa33163b92e3952161516a23a9f717b..5b1d3fcbab62cedd8c58580f60eade38039049e4 100644 (file)
@@ -22,6 +22,7 @@ return [
     'meta_created_name' => 'Creado :timeLength por :user',
     'meta_updated' => 'Actualizado :timeLength',
     'meta_updated_name' => 'Actualizado :timeLength por :user',
+    'meta_owned_name' => 'Propiedad de :user',
     'entity_select' => 'Seleccione entidad',
     'images' => 'Imágenes',
     'my_recent_drafts' => 'Mis borradores recientes',
@@ -39,6 +40,7 @@ return [
     'permissions_intro' => 'Una vez habilitado, estos permisos tendrán prioridad por encima de cualquier permiso establecido.',
     'permissions_enable' => 'Habilitar permisos personalizados',
     'permissions_save' => 'Guardar permisos',
+    'permissions_owner' => 'Propietario',
 
     // Search
     'search_results' => 'Resultados de búsqueda',
@@ -146,7 +148,7 @@ return [
     'chapters_create' => 'Crear nuevo capítulo',
     'chapters_delete' => 'Borrar capítulo',
     'chapters_delete_named' => 'Borrar capítulo :chapterName',
-    'chapters_delete_explain' => 'Esto borrará el capítulo con el nombre \':chapterName\', todas las páginas serán eliminadas y agregadas directamente al libro padre.',
+    'chapters_delete_explain' => 'Esto eliminará el capítulo con el nombre \':chapterName\'. También se eliminarán todas las páginas que existen dentro de este capítulo.',
     'chapters_delete_confirm' => '¿Está seguro de borrar este capítulo?',
     'chapters_edit' => 'Editar capítulo',
     'chapters_edit_named' => 'Editar capítulo :chapterName',
@@ -208,6 +210,7 @@ return [
     'pages_revisions' => 'Revisiones de página',
     'pages_revisions_named' => 'Revisiones de página para :pageName',
     'pages_revision_named' => 'Revisión de página para :pageName',
+    'pages_revision_restored_from' => 'Restaurado de #:id; :summary',
     'pages_revisions_created_by' => 'Creado por',
     'pages_revisions_date' => 'Fecha de revisión',
     'pages_revisions_number' => '#',
index 82cb2861553f03b5e8cff10b59d3e2859d8be434..d6488eded53f98c67521d2b7d9a8005a3f636d84 100644 (file)
@@ -68,7 +68,7 @@ return [
     'maint' => 'Mantenimiento',
     'maint_image_cleanup' => 'Limpiar imágenes',
     'maint_image_cleanup_desc' => "Analiza las páginas y sus revisiones para comprobar qué imágenes y dibujos están siendo utilizadas y cuales no son necesarias. Asegúrate de crear una copia completa de la base de datos y de las imágenes antes de lanzar esta opción.",
-    'maint_image_cleanup_ignore_revisions' => 'Ignorar imágenes en revisiones',
+    'maint_delete_images_only_in_revisions' => 'Elimina también imágenes que sólo existen en antiguas revisiones de páginas',
     'maint_image_cleanup_run' => 'Lanzar limpieza',
     'maint_image_cleanup_warning' => 'Se han encontrado :count imágenes posiblemente no utilizadas . ¿Estás seguro de querer borrar estas imágenes?',
     'maint_image_cleanup_success' => '¡Se han encontrado y borrado :count imágenes posiblemente no utilizadas!',
@@ -80,6 +80,27 @@ return [
     'maint_send_test_email_mail_subject' => 'Probar correo electrónico',
     'maint_send_test_email_mail_greeting' => '¡El envío de correos electrónicos parece funcionar!',
     'maint_send_test_email_mail_text' => '¡Enhorabuena! Al recibir esta notificación de correo electrónico, tu configuración de correo electrónico parece estar ajustada correctamente.',
+    'maint_recycle_bin_desc' => 'Los estantes, libros, capítulos y páginas eliminados se envían a la papelera de reciclaje para que puedan ser restauradas o eliminadas permanentemente. Los elementos más antiguos en la papelera de reciclaje pueden ser eliminados automáticamente después de un tiempo dependiendo de la configuración del sistema.',
+    'maint_recycle_bin_open' => 'Abrir papelera de reciclaje',
+
+    // Recycle Bin
+    'recycle_bin' => 'Papelera de Reciclaje',
+    'recycle_bin_desc' => 'Aquí puede restaurar elementos que hayan sido eliminados o elegir eliminarlos permanentemente del sistema. Esta lista no está filtrada a diferencia de las listas de actividad similares en el sistema donde se aplican los filtros de permisos.',
+    'recycle_bin_deleted_item' => 'Elemento Eliminado',
+    'recycle_bin_deleted_by' => 'Eliminado por',
+    'recycle_bin_deleted_at' => 'Fecha de eliminación',
+    'recycle_bin_permanently_delete' => 'Eliminar permanentemente',
+    'recycle_bin_restore' => 'Restaurar',
+    'recycle_bin_contents_empty' => 'La papelera de reciclaje está vacía',
+    'recycle_bin_empty' => 'Vaciar Papelera de reciclaje',
+    'recycle_bin_empty_confirm' => 'Esto destruirá permanentemente todos los elementos de la papelera de reciclaje, incluyendo el contenido existente en cada elemento. ¿Está seguro de que desea vaciar la papelera de reciclaje?',
+    'recycle_bin_destroy_confirm' => 'Esta acción eliminará permanentemente este elemento del sistema, junto con los elementos secundarios listados a continuación, y no podrá restaurar este contenido de nuevo. ¿Está seguro de que desea eliminar permanentemente este elemento?',
+    'recycle_bin_destroy_list' => 'Elementos a eliminar',
+    'recycle_bin_restore_list' => 'Elementos a restaurar',
+    'recycle_bin_restore_confirm' => 'Esta acción restaurará el elemento eliminado, incluyendo cualquier elemento secundario, a su ubicación original. Si la ubicación original ha sido eliminada, y ahora está en la papelera de reciclaje, el elemento padre también tendrá que ser restaurado.',
+    'recycle_bin_restore_deleted_parent' => 'El padre de este elemento también ha sido eliminado. Estos permanecerán eliminados hasta que el padre también sea restaurado.',
+    'recycle_bin_destroy_notification' => 'Eliminados :count artículos de la papelera de reciclaje.',
+    'recycle_bin_restore_notification' => 'Restaurados :count artículos desde la papelera de reciclaje.',
 
     // Audit Log
     'audit' => 'Registro de Auditoría',
@@ -90,7 +111,7 @@ return [
     'audit_deleted_item_name' => 'Nombre: :name',
     'audit_table_user' => 'Usuario',
     'audit_table_event' => 'Evento',
-    'audit_table_item' => 'Elemento relacionado',
+    'audit_table_related' => 'Elemento o detalle relacionados',
     'audit_table_date' => 'Fecha de la actividad',
     'audit_date_from' => 'Rango de fecha desde',
     'audit_date_to' => 'Rango de fecha hasta',
@@ -136,6 +157,7 @@ return [
     'user_profile' => 'Perfil de Usuario',
     'users_add_new' => 'Agregar Nuevo Usuario',
     'users_search' => 'Buscar usuarios',
+    'users_latest_activity' => 'Actividad Reciente',
     'users_details' => 'Detalles de Usuario',
     'users_details_desc' => 'Ajusta un nombre público y email para este usuario. El email será empleado para acceder a la aplicación.',
     'users_details_desc_no_email' => 'Ajusta un nombre público para este usuario para que pueda ser reconocido por otros.',
@@ -153,7 +175,10 @@ return [
     'users_delete_named' => 'Borrar usuario :userName',
     'users_delete_warning' => 'Se borrará completamente el usuario con el nombre \':userName\' del sistema.',
     'users_delete_confirm' => '¿Está seguro que desea borrar este usuario?',
-    'users_delete_success' => 'Usuarios removidos éxitosamente',
+    'users_migrate_ownership' => 'Cambiar Propietario',
+    'users_migrate_ownership_desc' => 'Seleccione un usuario aquí si desea que otro usuario se convierta en el dueño de todos los elementos que actualmente son propiedad de este usuario.',
+    'users_none_selected' => 'Usuario no seleccionado',
+    'users_delete_success' => 'El usuario se ha eliminado correctamente',
     'users_edit' => 'Editar Usuario',
     'users_edit_profile' => 'Editar perfil',
     'users_edit_success' => 'Usuario actualizado',
@@ -214,6 +239,7 @@ return [
         'ja' => '日本語',
         'ko' => '한국어',
         'nl' => 'Nederlands',
+        'nb' => 'Norsk (Bokmål)',
         'pl' => 'Polski',
         'pt_BR' => 'Português do Brasil',
         'ru' => 'Русский',
index de6094a71ccdddd3032dac53982ce85bbaf308aa..f6f9a1ee12bf65c98841af1aca9efb17ccad5455 100644 (file)
@@ -90,6 +90,7 @@ return [
     'required_without'     => 'El :attribute es requerido cuando no se encuentre entre los valores :values.',
     'required_without_all' => 'El :attribute es requerido cuando ninguno de los valores :values están presentes.',
     'same'                 => 'El :attribute y :other deben coincidir.',
+    'safe_url'             => 'El enlace proporcionado puede no ser seguro.',
     'size'                 => [
         'numeric' => ':attribute debe ser :size.',
         'file'    => ':attribute debe ser :size kilobytes.',
index f8f9e84367568979b8faaca2fda33af9b7d6ebb9..7c9e22450756469b593720860fe374a5542df9fc 100644 (file)
@@ -45,4 +45,5 @@ return [
 
     // Other
     'commented_on'                => 'comentado',
+    'permissions_update'          => 'permisos actualizados',
 ];
index 3c409f2781982f527728557d422427158a2536ff..093641a362346804a0a82b0205d0897e7b853aca 100644 (file)
@@ -22,6 +22,7 @@ return [
     'meta_created_name' => 'Creado el  :timeLength por :user',
     'meta_updated' => 'Actualizado el :timeLength',
     'meta_updated_name' => 'Actualizado el :timeLength por :user',
+    'meta_owned_name' => 'Propiedad de :user',
     'entity_select' => 'Seleccione entidad',
     'images' => 'Imágenes',
     'my_recent_drafts' => 'Mis borradores recientes',
@@ -39,6 +40,7 @@ return [
     'permissions_intro' => 'una vez habilitado, Estos permisos tendrán prioridad por encima de cualquier permiso establecido.',
     'permissions_enable' => 'Habilitar permisos custom',
     'permissions_save' => 'Guardar permisos',
+    'permissions_owner' => 'Propietario',
 
     // Search
     'search_results' => 'Buscar resultados',
@@ -146,7 +148,7 @@ return [
     'chapters_create' => 'Crear nuevo capítulo',
     'chapters_delete' => 'Borrar capítulo',
     'chapters_delete_named' => 'Borrar capítulo :chapterName',
-    'chapters_delete_explain' => 'Esto borrará el capítulo con el nombre \':chapterName\', todas las páginas serán removidas y agregadas directamente al libro padre.',
+    'chapters_delete_explain' => 'Esto eliminará el capítulo con el nombre \':chapterName\'. También se eliminarán todas las páginas que existen dentro de este capítulo.',
     'chapters_delete_confirm' => '¿Está seguro de borrar este capítulo?',
     'chapters_edit' => 'Editar capítulo',
     'chapters_edit_named' => 'Editar capítulo :chapterName',
@@ -208,6 +210,7 @@ return [
     'pages_revisions' => 'Revisiones de página',
     'pages_revisions_named' => 'Revisiones de página para :pageName',
     'pages_revision_named' => 'Revisión de ágina para :pageName',
+    'pages_revision_restored_from' => 'Restaurado de #:id; :summary',
     'pages_revisions_created_by' => 'Creado por',
     'pages_revisions_date' => 'Fecha de revisión',
     'pages_revisions_number' => '#',
@@ -265,7 +268,7 @@ return [
     'attachments_link_url' => 'Enlace a archivo',
     'attachments_link_url_hint' => 'URL del sitio o archivo',
     'attach' => 'Adjuntar',
-    'attachments_insert_link' => 'Añadir enlace al adjunto en la página',
+    'attachments_insert_link' => 'Agregar el Enlace del adjunto a la Página',
     'attachments_edit_file' => 'Editar archivo',
     'attachments_edit_file_name' => 'Nombre del archivo',
     'attachments_edit_drop_upload' => 'Arrastre los archivos o presione aquí para subir o sobreescribir',
index 641b10c2898fb53a1d62d07bf82832a9c22ae6b2..8ab0941839a77c03e0334d7da1f5ad18fb1a3883 100644 (file)
@@ -68,7 +68,7 @@ return [
     'maint' => 'Mantenimiento',
     'maint_image_cleanup' => 'Limpiar imágenes',
     'maint_image_cleanup_desc' => "Analizar contenido de páginas y revisiones para detectar cuáles imágenes y dibujos están en uso y cuáles son redundantes. Asegúrese de crear un respaldo completo de imágenes y base de datos antes de ejecutar esta tarea.",
-    'maint_image_cleanup_ignore_revisions' => 'Ignorar imágenes en revisión',
+    'maint_delete_images_only_in_revisions' => 'Elimina también imágenes que sólo existen en antiguas revisiones de páginas',
     'maint_image_cleanup_run' => 'Ejecutar limpieza',
     'maint_image_cleanup_warning' => 'Se encontraron :count imágenes pontencialmente sin uso. Está seguro de que quiere eliminarlas?',
     'maint_image_cleanup_success' => 'Se encontraron y se eliminaron :count imágenes pontencialmente sin uso!',
@@ -80,20 +80,41 @@ return [
     'maint_send_test_email_mail_subject' => 'Probar correo electrónico',
     'maint_send_test_email_mail_greeting' => '¡El envío de correos electrónicos parece funcionar!',
     'maint_send_test_email_mail_text' => '¡Enhorabuena! Al recibir esta notificación de correo electrónico, tu configuración de correo electrónico parece estar ajustada correctamente.',
+    'maint_recycle_bin_desc' => 'Los estantes, libros, capítulos y páginas eliminados se envían a la papelera de reciclaje para que puedan ser restauradas o eliminadas permanentemente. Los elementos más antiguos en la papelera de reciclaje pueden ser eliminados automáticamente después de un tiempo dependiendo de la configuración del sistema.',
+    'maint_recycle_bin_open' => 'Abrir papelera de reciclaje',
+
+    // Recycle Bin
+    'recycle_bin' => 'Papelera de Reciclaje',
+    'recycle_bin_desc' => 'Aquí puede restaurar elementos que hayan sido eliminados o elegir eliminarlos permanentemente del sistema. Esta lista no está filtrada a diferencia de las listas de actividad similares en el sistema donde se aplican los filtros de permisos.',
+    'recycle_bin_deleted_item' => 'Elemento Eliminado',
+    'recycle_bin_deleted_by' => 'Eliminado por',
+    'recycle_bin_deleted_at' => 'Fecha de eliminación',
+    'recycle_bin_permanently_delete' => 'Eliminar permanentemente',
+    'recycle_bin_restore' => 'Restaurar',
+    'recycle_bin_contents_empty' => 'La papelera de reciclaje está vacía',
+    'recycle_bin_empty' => 'Vaciar Papelera de reciclaje',
+    'recycle_bin_empty_confirm' => 'Esto destruirá permanentemente todos los elementos de la papelera de reciclaje, incluyendo el contenido existente en cada elemento. ¿Está seguro de que desea vaciar la papelera de reciclaje?',
+    'recycle_bin_destroy_confirm' => 'Esta acción eliminará permanentemente este elemento del sistema, junto con los elementos secundarios listados a continuación, y no podrá restaurar este contenido de nuevo. ¿Está seguro de que desea eliminar permanentemente este elemento?',
+    'recycle_bin_destroy_list' => 'Elementos a eliminar',
+    'recycle_bin_restore_list' => 'Elementos a restaurar',
+    'recycle_bin_restore_confirm' => 'Esta acción restaurará el elemento eliminado, incluyendo cualquier elemento secundario, a su ubicación original. Si la ubicación original ha sido eliminada, y ahora está en la papelera de reciclaje, el elemento padre también tendrá que ser restaurado.',
+    'recycle_bin_restore_deleted_parent' => 'El padre de este elemento también ha sido eliminado. Estos permanecerán eliminados hasta que el padre también sea restaurado.',
+    'recycle_bin_destroy_notification' => 'Eliminados :count artículos de la papelera de reciclaje.',
+    'recycle_bin_restore_notification' => 'Restaurados :count artículos desde la papelera de reciclaje.',
 
     // Audit Log
     'audit' => 'Registro de Auditoría',
-    'audit_desc' => 'Este registro de auditoría muestra una lista de actividades registradas en el sistema. Esta lista no está filtrada a diferencia de las listas de actividad similares en el sistema donde se aplican los filtros de permisos.',
-    'audit_event_filter' => 'Filtro de eventos',
-    'audit_event_filter_no_filter' => 'Sin filtro',
-    'audit_deleted_item' => 'Elemento eliminado',
+    'audit_desc' => 'Este registro de auditoría muestra una lista de actividades rastreadas en el sistema. Esta lista no tiene filtrado a diferencia de listas de actividad similares en el sistema en los que se aplican filtros de permisos.',
+    'audit_event_filter' => 'Filtro de Eventos',
+    'audit_event_filter_no_filter' => 'Sin Filtro',
+    'audit_deleted_item' => 'Elemento borrado',
     'audit_deleted_item_name' => 'Nombre: :name',
     'audit_table_user' => 'Usuario',
     'audit_table_event' => 'Evento',
-    'audit_table_item' => 'Elemento relacionado',
-    'audit_table_date' => 'Fecha de la actividad',
-    'audit_date_from' => 'Rango de fecha desde',
-    'audit_date_to' => 'Rango de fecha hasta',
+    'audit_table_related' => 'Elemento o detalle relacionados',
+    'audit_table_date' => 'Fecha de la Actividad',
+    'audit_date_from' => 'Inicio del Rango de Fecha',
+    'audit_date_to' => 'Final del Rango de Fecha',
 
     // Role Settings
     'roles' => 'Roles',
@@ -121,8 +142,8 @@ return [
     'role_access_api' => 'API de sistema de acceso',
     'role_manage_settings' => 'Gestionar ajustes de activos',
     'role_asset' => 'Permisos de activos',
-    'roles_system_warning' => 'Tenga en cuenta que el acceso a cualquiera de los tres permisos anteriores puede permitir a un usuario alterar sus propios privilegios o los privilegios de otros en el sistema. Sólo asignar roles con estos permisos a usuarios de confianza.',
-    'role_asset_desc' => 'Estos permisos controlan el acceso por defecto a los activos del sistema. Permisos a Libros, Capítulos y Páginas sobreescribiran estos permisos.',
+    'roles_system_warning' => 'Tenga en cuenta que el acceso a cualquiera de los tres permisos anteriores puede permitir a un usuario modificar sus propios privilegios o los privilegios de otros usuarios en el sistema. Asignar roles con estos permisos sólo a usuarios de comfianza.',
+    'role_asset_desc' => 'Estos permisos controlan el acceso por defecto a los activos del sistema. Permisos definidos en Libros, Capítulos y Páginas ignorarán estos permisos.',
     'role_asset_admins' => 'Los administradores reciben automáticamente acceso a todo el contenido pero estas opciones pueden mostrar u ocultar opciones de UI.',
     'role_all' => 'Todo',
     'role_own' => 'Propio',
@@ -137,6 +158,7 @@ return [
     'user_profile' => 'Perfil de usuario',
     'users_add_new' => 'Agregar nuevo usuario',
     'users_search' => 'Buscar usuarios',
+    'users_latest_activity' => 'Actividad Reciente',
     'users_details' => 'Detalles del usuario',
     'users_details_desc' => 'Asigne un nombre de visualización y una dirección de correo electrónico para este usuario. La dirección de correo electrónico se usará pra ingresar a la aplicación.',
     'users_details_desc_no_email' => 'Asigne un nombre de visualización a este usuario para que los demás puedan reconocerlo.',
@@ -154,7 +176,10 @@ return [
     'users_delete_named' => 'Borrar usuario :userName',
     'users_delete_warning' => 'Se borrará completamente el usuario con el nombre \':userName\' del sistema.',
     'users_delete_confirm' => '¿Está seguro que desea borrar este usuario?',
-    'users_delete_success' => 'Usuarios removidos exitosamente',
+    'users_migrate_ownership' => 'Cambiar Propietario',
+    'users_migrate_ownership_desc' => 'Seleccione un usuario aquí si desea que otro usuario se convierta en el dueño de todos los elementos que actualmente son propiedad de este usuario.',
+    'users_none_selected' => 'Usuario no seleccionado',
+    'users_delete_success' => 'El usuario se ha eliminado correctamente',
     'users_edit' => 'Editar Usuario',
     'users_edit_profile' => 'Editar perfil',
     'users_edit_success' => 'Usuario actualizado',
@@ -180,7 +205,7 @@ return [
     'user_api_token_name_desc' => 'Dale a tu token un nombre legible como un recordatorio futuro de su propósito.',
     'user_api_token_expiry' => 'Fecha de expiración',
     'user_api_token_expiry_desc' => 'Establece una fecha en la que este token expira. Después de esta fecha, las solicitudes realizadas usando este token ya no funcionarán. Dejar este campo en blanco fijará un vencimiento de 100 años en el futuro.',
-    'user_api_token_create_secret_message' => 'Immediately after creating this token a "Token ID" & "Token Secret" will be generated and displayed. The secret will only be shown a single time so be sure to copy the value to somewhere safe and secure before proceeding.',
+    'user_api_token_create_secret_message' => 'Luego de crear este token, inmediatamente se generará y mostrará el "Token ID" y el "Token Secret" correspondientes. El "Token Secret" se mostrará por única vez, asegúrese de copiar el valor a un lugar seguro antes de continuar.',
     'user_api_token_create_success' => 'Token API creado correctamente',
     'user_api_token_update_success' => 'Token API actualizado correctamente',
     'user_api_token' => 'Token API',
@@ -188,8 +213,8 @@ return [
     'user_api_token_id_desc' => 'Este es un identificador no editable generado por el sistema y único para este token que necesitará ser proporcionado en solicitudes de API.',
     'user_api_token_secret' => 'Clave de Token',
     'user_api_token_secret_desc' => 'Esta es una clave no editable generada por el sistema que necesitará ser proporcionada en solicitudes de API. Solo se monstraré esta vez así que guarde su valor en un lugar seguro.',
-    'user_api_token_created' => 'Token created :timeAgo',
-    'user_api_token_updated' => 'Token updated :timeAgo',
+    'user_api_token_created' => 'Token creado :timeAgo',
+    'user_api_token_updated' => 'Token actualizado :timeAgo',
     'user_api_token_delete' => 'Borrar token',
     'user_api_token_delete_warning' => 'Esto eliminará completamente este token API con el nombre \':tokenName\' del sistema.',
     'user_api_token_delete_confirm' => '¿Está seguro de que desea borrar este API token?',
@@ -215,6 +240,7 @@ return [
         'ja' => '日本語',
         'ko' => '한국어',
         'nl' => 'Nederlands',
+        'nb' => 'Norsk (Bokmål)',
         'pl' => 'Polski',
         'pt_BR' => 'Português do Brasil',
         'ru' => 'Русский',
index cd360c8eae64e9f987a379366f40b10cf7bbfcc5..7afd2bb461fbbe5c16852b70525c8e849ab03bdb 100644 (file)
@@ -90,6 +90,7 @@ return [
     'required_without'     => ':attribute es requerido cuando no se encuentre entre los valores :values.',
     'required_without_all' => ':attribute es requerido cuando ninguno de los valores :values están presentes.',
     'same'                 => ':attribute y :other deben coincidir.',
+    'safe_url'             => 'El enlace proporcionado puede no ser seguro.',
     'size'                 => [
         'numeric' => ':attribute debe ser :size.',
         'file'    => ':attribute debe ser :size kilobytes.',
index 56db4abff0490f3c68e97b24933aa925e2b2b586..8b62bd0528a390770f9e1fe6b9694ef88867f317 100644 (file)
@@ -45,4 +45,5 @@ return [
 
     // Other
     'commented_on'                => 'a commenté',
+    'permissions_update'          => 'mettre à jour les autorisations',
 ];
index bc37fa52cbd633a9fa3e4a5071d1467066c2f8c2..72711bcb57ea7312fd59bc840c5c8986a033c951 100644 (file)
@@ -15,13 +15,14 @@ return [
     'recently_update' => 'Mis à jour récemment',
     'recently_viewed' => 'Vus récemment',
     'recent_activity' => 'Activité récente',
-    'create_now' => 'En créer un maintenant',
+    'create_now' => 'En créer une maintenant',
     'revisions' => 'Révisions',
     'meta_revision' => 'Révision #:revisionCount',
     'meta_created' => 'Créé :timeLength',
     'meta_created_name' => 'Créé :timeLength par :user',
     'meta_updated' => 'Mis à jour :timeLength',
     'meta_updated_name' => 'Mis à jour :timeLength par :user',
+    'meta_owned_name' => 'Possédé par :user',
     'entity_select' => 'Sélectionner l\'entité',
     'images' => 'Images',
     'my_recent_drafts' => 'Mes brouillons récents',
@@ -39,6 +40,7 @@ return [
     'permissions_intro' => 'Une fois activées ces permissions prendront la priorité sur tous les sets de permissions préexistants.',
     'permissions_enable' => 'Activer les permissions personnalisées',
     'permissions_save' => 'Enregistrer les permissions',
+    'permissions_owner' => 'Propriétaire',
 
     // Search
     'search_results' => 'Résultats de recherche',
@@ -146,7 +148,7 @@ return [
     'chapters_create' => 'Créer un nouveau chapitre',
     'chapters_delete' => 'Supprimer le chapitre',
     'chapters_delete_named' => 'Supprimer le chapitre :chapterName',
-    'chapters_delete_explain' => 'Ceci va supprimer le chapitre \':chapterName\', toutes les pages seront déplacées dans le livre parent.',
+    'chapters_delete_explain' => 'Ceci supprimera le chapitre portant le nom \':chapterName\'. Toutes les pages qui existent dans ce chapitre seront également supprimées.',
     'chapters_delete_confirm' => 'Etes-vous sûr(e) de vouloir supprimer ce chapitre ?',
     'chapters_edit' => 'Modifier le chapitre',
     'chapters_edit_named' => 'Modifier le chapitre :chapterName',
@@ -208,6 +210,7 @@ return [
     'pages_revisions' => 'Révisions de la page',
     'pages_revisions_named' => 'Révisions pour :pageName',
     'pages_revision_named' => 'Révision pour :pageName',
+    'pages_revision_restored_from' => 'Restauré à partir de #:id; :summary',
     'pages_revisions_created_by' => 'Créé par',
     'pages_revisions_date' => 'Date de révision',
     'pages_revisions_number' => '#',
index 0a24f9cded12e6b6bb873748bae6914f7c875f64..89e2f0ca5691bf4aaa725f3ac5b7156258f0a85b 100644 (file)
@@ -68,7 +68,7 @@ return [
     'maint' => 'Maintenance',
     'maint_image_cleanup' => 'Nettoyer les images',
     'maint_image_cleanup_desc' => "Scan le contenu des pages et des révisions pour vérifier les images et les dessins en cours d'utilisation et lesquels sont redondant. Veuillez à faire une sauvegarde de la base de données et des images avant de lancer ceci.",
-    'maint_image_cleanup_ignore_revisions' => 'Ignorer les images dans les révisions',
+    'maint_delete_images_only_in_revisions' => 'Supprimer également les images qui n\'existent que dans les anciennes révisions de page',
     'maint_image_cleanup_run' => 'Lancer le nettoyage',
     'maint_image_cleanup_warning' => ':count images potentiellement inutilisées trouvées. Etes-vous sûr de vouloir supprimer ces images ?',
     'maint_image_cleanup_success' => ':count images potentiellement inutilisées trouvées et supprimées !',
@@ -80,6 +80,27 @@ return [
     'maint_send_test_email_mail_subject' => 'Email de test',
     'maint_send_test_email_mail_greeting' => 'La livraison d\'email semble fonctionner !',
     'maint_send_test_email_mail_text' => 'Félicitations ! Lorsque vous avez reçu cette notification par courriel, vos paramètres d\'email semblent être configurés correctement.',
+    'maint_recycle_bin_desc' => 'Les étagères, livres, chapitres et pages supprimés sont envoyés dans la corbeille afin qu\'ils puissent être restaurés ou supprimés définitivement. Les éléments plus anciens de la corbeille peuvent être supprimés automatiquement après un certain temps selon la configuration du système.',
+    'maint_recycle_bin_open' => 'Ouvrir la corbeille',
+
+    // Recycle Bin
+    'recycle_bin' => 'Corbeille',
+    'recycle_bin_desc' => 'Ici, vous pouvez restaurer les éléments qui ont été supprimés ou choisir de les effacer définitivement du système. Cette liste n\'est pas filtrée contrairement aux listes d\'activités similaires dans le système pour lesquelles les filtres d\'autorisation sont appliqués.',
+    'recycle_bin_deleted_item' => 'Élément supprimé',
+    'recycle_bin_deleted_by' => 'Supprimé par',
+    'recycle_bin_deleted_at' => 'Date de suppression',
+    'recycle_bin_permanently_delete' => 'Supprimer définitivement',
+    'recycle_bin_restore' => 'Restaurer',
+    'recycle_bin_contents_empty' => 'La corbeille est vide',
+    'recycle_bin_empty' => 'Vider la Corbeille',
+    'recycle_bin_empty_confirm' => 'Cela détruira définitivement tous les éléments de la corbeille, y compris le contenu contenu de chaque élément. Êtes-vous sûr de vouloir vider la corbeille ?',
+    'recycle_bin_destroy_confirm' => 'Cette action supprimera définitivement cet élément, ainsi que tous les éléments enfants listés ci-dessous du système et vous ne pourrez pas restaurer ce contenu. Êtes-vous sûr de vouloir supprimer définitivement cet élément ?',
+    'recycle_bin_destroy_list' => 'Éléments à détruire',
+    'recycle_bin_restore_list' => 'Éléments à restaurer',
+    'recycle_bin_restore_confirm' => 'Cette action restaurera l\'élément supprimé, y compris tous les éléments enfants, à leur emplacement d\'origine. Si l\'emplacement d\'origine a été supprimé depuis et est maintenant dans la corbeille, l\'élément parent devra également être restauré.',
+    'recycle_bin_restore_deleted_parent' => 'Le parent de cet élément a également été supprimé. Ceux-ci resteront supprimés jusqu\'à ce que ce parent soit également restauré.',
+    'recycle_bin_destroy_notification' => ':count éléments totaux supprimés de la corbeille.',
+    'recycle_bin_restore_notification' => ':count éléments totaux restaurés de la corbeille.',
 
     // Audit Log
     'audit' => 'Journal d\'audit',
@@ -90,7 +111,7 @@ return [
     'audit_deleted_item_name' => 'Nom: :name',
     'audit_table_user' => 'Utilisateur',
     'audit_table_event' => 'Evènement',
-    'audit_table_item' => 'Élément Associé',
+    'audit_table_related' => 'Élément ou détail lié',
     'audit_table_date' => 'Date d\'activation',
     'audit_date_from' => 'À partir du',
     'audit_date_to' => 'Jusqu\'au',
@@ -136,6 +157,7 @@ return [
     'user_profile' => 'Profil d\'utilisateur',
     'users_add_new' => 'Ajouter un nouvel utilisateur',
     'users_search' => 'Chercher les utilisateurs',
+    'users_latest_activity' => 'Dernière activité',
     'users_details' => 'Informations de l\'utilisateur',
     'users_details_desc' => 'Définissez un nom et une adresse e-mail pour cet utilisateur. L\'adresse e-mail sera utilisée pour se connecter à l\'application.',
     'users_details_desc_no_email' => 'Définissez un nom d\'affichage pour cet utilisateur afin que les autres puissent le reconnaître.',
@@ -153,7 +175,10 @@ return [
     'users_delete_named' => 'Supprimer l\'utilisateur :userName',
     'users_delete_warning' => 'Ceci va supprimer \':userName\' du système.',
     'users_delete_confirm' => 'Êtes-vous sûr(e) de vouloir supprimer cet utilisateur ?',
-    'users_delete_success' => 'Utilisateurs supprimés avec succès',
+    'users_migrate_ownership' => 'Migré propriété',
+    'users_migrate_ownership_desc' => 'Sélectionnez un utilisateur ici si vous voulez qu\'un autre utilisateur devienne le propriétaire de tous les éléments actuellement détenus par cet utilisateur.',
+    'users_none_selected' => 'Aucun utilisateur n\'a été séléctionné',
+    'users_delete_success' => 'Utilisateur supprimé avec succès',
     'users_edit' => 'Modifier l\'utilisateur',
     'users_edit_profile' => 'Modifier le profil',
     'users_edit_success' => 'Utilisateur mis à jour avec succès',
@@ -214,6 +239,7 @@ return [
         'ja' => '日本語',
         'ko' => '한국어',
         'nl' => 'Nederlands',
+        'nb' => 'Norvegien',
         'pl' => 'Polski',
         'pt_BR' => 'Português do Brasil',
         'ru' => 'Русский',
index f59d5c50313c3fb1ea3533b0d27be85e62b70ffb..60d8d34acfb02193039909d432e6d86760ceba52 100644 (file)
@@ -90,6 +90,7 @@ return [
     'required_without'     => ':attribute est requis si:values n\'est pas présent.',
     'required_without_all' => ':attribute est requis si aucun des valeurs :values n\'est présente.',
     'same'                 => ':attribute et :other doivent être identiques.',
+    'safe_url'             => 'Le lien fourni peut ne pas être sûr.',
     'size'                 => [
         'numeric' => ':attribute doit avoir la taille :size.',
         'file'    => ':attribute doit peser :size kilobytes.',
index 34860173e0d6106e341134b493b45403889405c7..0babc38d17ed94ff4f2c7034983f230d3bda7fcd 100644 (file)
@@ -45,4 +45,5 @@ return [
 
     // Other
     'commented_on'                => 'commented on',
+    'permissions_update'          => 'updated permissions',
 ];
index 8eef64efa390280aa652fc7fa759b68f32321534..59199bd8bd9079bf17c65c79898cc35fd23c0202 100644 (file)
@@ -22,6 +22,7 @@ return [
     'meta_created_name' => 'נוצר :timeLength על ידי :user',
     'meta_updated' => 'עודכן :timeLength',
     'meta_updated_name' => 'עודכן :timeLength על ידי :user',
+    'meta_owned_name' => 'Owned by :user',
     'entity_select' => 'בחר יישות',
     'images' => 'תמונות',
     'my_recent_drafts' => 'הטיוטות האחרונות שלי',
@@ -39,6 +40,7 @@ return [
     'permissions_intro' => 'ברגע שמסומן, הרשאות אלו יגברו על כל הרשאת תפקיד שקיימת',
     'permissions_enable' => 'הפעל הרשאות מותאמות אישית',
     'permissions_save' => 'שמור הרשאות',
+    'permissions_owner' => 'Owner',
 
     // Search
     'search_results' => 'תוצאות חיפוש',
@@ -146,7 +148,7 @@ return [
     'chapters_create' => 'צור פרק חדש',
     'chapters_delete' => 'מחק פרק',
     'chapters_delete_named' => 'מחק את פרק :chapterName',
-    'chapters_delete_explain' => 'פעולה זו תמחוק את הפרק בשם \':chapterName\'. כל הדפים יועברו אוטומטית לספר עצמו',
+    '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' => 'האם ברצונך למחוק פרק זה?',
     'chapters_edit' => 'ערוך פרק',
     'chapters_edit_named' => 'ערוך פרק :chapterName',
@@ -208,6 +210,7 @@ return [
     'pages_revisions' => 'נוסחי דף',
     'pages_revisions_named' => 'נוסחי דף עבור :pageName',
     'pages_revision_named' => 'נוסח דף עבור :pageName',
+    'pages_revision_restored_from' => 'Restored from #:id; :summary',
     'pages_revisions_created_by' => 'נוצר על ידי',
     'pages_revisions_date' => 'תאריך נוסח',
     'pages_revisions_number' => '#',
index c90e834b350fa380f2811a39913aa5c4de0970aa..a0632d061f7a3dec41fdb4d83932228393c0e95c 100755 (executable)
@@ -68,7 +68,7 @@ return [
     'maint' => 'תחזוקה',
     'maint_image_cleanup' => 'ניקוי תמונות',
     'maint_image_cleanup_desc' => "סורק את הדפים והגרסאות על מנת למצוא אילו תמונות לא בשימוש. יש לוודא גיבוי מלא של מסד הנתונים והתמונות לפני הרצה",
-    'maint_image_cleanup_ignore_revisions' => 'התעלם מהתמונות בגרסאות',
+    'maint_delete_images_only_in_revisions' => 'Also delete images that only exist in old page revisions',
     'maint_image_cleanup_run' => 'הפעל ניקוי תמונות',
     'maint_image_cleanup_warning' => 'נמצאו כ :count תמונות אשר לא בשימוש האם ברצונך להמשיך?',
     'maint_image_cleanup_success' => ':count תמונות שלא בשימוש נמחקו',
@@ -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' => 'פרופיל משתמש',
     'users_add_new' => 'הוסף משתמש חדש',
     'users_search' => 'חפש משתמשים',
+    'users_latest_activity' => 'Latest Activity',
     'users_details' => 'פרטי משתמש',
     'users_details_desc' => 'הגדר שם לתצוגה ומייל עבור משתמש זה. כתובת המייל תשמש על מנת להתחבר למערכת',
     'users_details_desc_no_email' => 'הגדר שם לתצוגה כדי שאחרים יוכלו לזהות',
@@ -153,7 +175,10 @@ return [
     'users_delete_named' => 'מחק משתמש :userName',
     'users_delete_warning' => 'פעולה זו תמחק את המשתמש \':userName\' מהמערכת',
     'users_delete_confirm' => 'האם ברצונך למחוק משתמש זה?',
-    'users_delete_success' => 'המשתמש נמחק בהצלחה',
+    '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' => 'עריכת משתמש',
     'users_edit_profile' => 'עריכת פרופיל',
     'users_edit_success' => 'המשתמש עודכן בהצלחה',
@@ -214,6 +239,7 @@ return [
         'ja' => '日本語',
         'ko' => '한국어',
         'nl' => 'Nederlands',
+        'nb' => 'Norsk (Bokmål)',
         'pl' => 'Polski',
         'pt_BR' => 'Português do Brasil',
         'ru' => 'Русский',
index 2797a9c21878f4fb2a6f93f376e685547ab19b50..7c02735ca067f8e7876a016323c24c69d39db7a2 100644 (file)
@@ -90,6 +90,7 @@ return [
     'required_without'     => 'שדה :attribute נחוץ כאשר :values לא בנמצא.',
     'required_without_all' => 'שדה :attribute נחוץ כאשר אף אחד מ-:values נמצאים.',
     'same'                 => 'שדה :attribute ו-:other חייבים להיות זהים.',
+    'safe_url'             => 'The provided link may not be safe.',
     'size'                 => [
         'numeric' => 'שדה :attribute חייב להיות :size.',
         'file'    => 'שדה :attribute חייב להיות :size קילובייטים.',
index 575e9e509e0bdefd6dad5542d03a319bbd5455bc..8d22605e36c1c0549ebc99f83866163d44296c7b 100644 (file)
@@ -45,4 +45,5 @@ return [
 
     // Other
     'commented_on'                => 'megjegyzést fűzött hozzá:',
+    'permissions_update'          => 'updated permissions',
 ];
index 51a6476d08d3b1de60fa8f41d8c77ed78df78cb6..5070adccd770dc5e1380836809ac116947ca93ac 100644 (file)
@@ -33,7 +33,7 @@ return [
     'copy' => 'Másolás',
     'reply' => 'Válasz',
     'delete' => 'Törlés',
-    'delete_confirm' => 'Confirm Deletion',
+    'delete_confirm' => 'Törlés megerősítése',
     'search' => 'Keresés',
     'search_clear' => 'Keresés törlése',
     'reset' => 'Visszaállítás',
@@ -67,8 +67,8 @@ return [
     'profile_menu' => 'Profil menü',
     'view_profile' => 'Profil megtekintése',
     'edit_profile' => 'Profil szerkesztése',
-    'dark_mode' => 'Dark Mode',
-    'light_mode' => 'Light Mode',
+    'dark_mode' => 'Sötét mód',
+    'light_mode' => 'Világos mód',
 
     // Layout tabs
     'tab_info' => 'Információ',
index 80afacf33cc4458fef11941dd7343643bb586894..c1c57d27e76183b5139501cdb9658437d2630f32 100644 (file)
@@ -15,7 +15,7 @@ return [
     'image_load_more' => 'Több betöltése',
     'image_image_name' => 'Kép neve',
     'image_delete_used' => 'Ez a kép a lenti oldalakon van használatban.',
-    'image_delete_confirm_text' => 'Are you sure you want to delete this image?',
+    'image_delete_confirm_text' => 'Biztosan törölhető ez a kép?',
     'image_select_image' => 'Kép kiválasztása',
     'image_dropzone' => 'Képek feltöltése ejtéssel vagy kattintással',
     'images_deleted' => 'Képek törölve',
@@ -29,6 +29,6 @@ return [
     'code_editor' => 'Kód szerkesztése',
     'code_language' => 'Kód nyelve',
     'code_content' => 'Kód tartalom',
-    'code_session_history' => 'Session History',
+    'code_session_history' => 'Munkamenet előzményei',
     'code_save' => 'Kód mentése',
 ];
index b580d0b028bc00fc21b8e8ca0572db7488a81d29..c248d63a057714d33131d39f6ec4bfd9c34bb23c 100644 (file)
@@ -22,6 +22,7 @@ return [
     'meta_created_name' => ':user hozta létre :timeLength',
     'meta_updated' => 'Frissítve :timeLength',
     'meta_updated_name' => ':user frissítette :timeLength',
+    'meta_owned_name' => 'Owned by :user',
     'entity_select' => 'Entitás kiválasztása',
     'images' => 'Képek',
     'my_recent_drafts' => 'Legutóbbi vázlataim',
@@ -39,6 +40,7 @@ return [
     'permissions_intro' => 'Ha engedélyezett, ezek a jogosultságok elsőbbséget élveznek bármely beállított szerepkör jogosultsággal szemben.',
     'permissions_enable' => 'Egyéni jogosultságok engedélyezése',
     'permissions_save' => 'Jogosultságok mentése',
+    'permissions_owner' => 'Owner',
 
     // Search
     'search_results' => 'Keresési eredmények',
@@ -47,8 +49,8 @@ return [
     'search_no_pages' => 'Nincsenek a keresésnek megfelelő oldalak',
     'search_for_term' => ':term keresése',
     'search_more' => 'További eredmények',
-    'search_advanced' => 'Advanced Search',
-    'search_terms' => 'Search Terms',
+    'search_advanced' => 'Részletes keresés',
+    'search_terms' => 'Keresési kifejezések',
     'search_content_type' => 'Tartalomtípus',
     'search_exact_matches' => 'Pontos egyezések',
     'search_tags' => 'Címke keresések',
@@ -146,7 +148,7 @@ return [
     'chapters_create' => 'Új fejezet létrehozása',
     'chapters_delete' => 'Fejezet törlése',
     'chapters_delete_named' => ':chapterName fejezet törlése',
-    'chapters_delete_explain' => '\':chapterName\' nevű fejezet törölve lesz. Minden oldal el lesz távolítva és közvetlenül a szülő könyvhöz lesz hozzáadva.',
+    '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' => 'Biztosan törölhető ez a fejezet?',
     'chapters_edit' => 'Fejezet szerkesztése',
     'chapters_edit_named' => ':chapterName fejezet szerkesztése',
@@ -208,6 +210,7 @@ return [
     'pages_revisions' => 'Oldal változatai',
     'pages_revisions_named' => ':pageName oldal változatai',
     'pages_revision_named' => ':pageName oldal változata',
+    'pages_revision_restored_from' => 'Restored from #:id; :summary',
     'pages_revisions_created_by' => 'Létrehozta:',
     'pages_revisions_date' => 'Változat dátuma',
     'pages_revisions_number' => '#',
@@ -256,7 +259,7 @@ return [
     'attachments_upload' => 'Fájlfeltöltés',
     'attachments_link' => 'Hivatkozás csatolása',
     'attachments_set_link' => 'Hivatkozás beállítása',
-    'attachments_delete' => 'Are you sure you want to delete this attachment?',
+    'attachments_delete' => 'Biztosan törölhető ez a melléklet?',
     'attachments_dropzone' => 'Fájlok csatolása ejtéssel vagy kattintással',
     'attachments_no_files' => 'Nincsenek fájlok feltöltve',
     'attachments_explain_link' => 'Fájl feltöltése helyett hozzá lehet kapcsolni egy hivatkozást. Ez egy hivatkozás lesz egy másik oldalra vagy egy fájlra a felhőben.',
@@ -265,7 +268,7 @@ return [
     'attachments_link_url' => 'Hivatkozás fájlra',
     'attachments_link_url_hint' => 'Weboldal vagy fájl webcíme',
     'attach' => 'Csatolás',
-    'attachments_insert_link' => 'Add Attachment Link to Page',
+    'attachments_insert_link' => 'Melléklet hivatkozás hozzáadása oldalhoz',
     'attachments_edit_file' => 'Fájl szerkesztése',
     'attachments_edit_file_name' => 'Fájl neve',
     'attachments_edit_drop_upload' => 'Feltöltés és felülírás ejtéssel vagy kattintással',
index 82485e104c14c3a89b8c6688eac773ac1c2f812c..9b148a61e2e11e0c8e343abf98db57555f416697 100644 (file)
@@ -68,7 +68,7 @@ return [
     'maint' => 'Karbantartás',
     'maint_image_cleanup' => 'Képek tisztítása',
     'maint_image_cleanup_desc' => "Végigolvassa az oldalakat és a tartalmak változatait, hogy leellenőrizze jelenleg mely képek és rajzok vannak használatban, és mely képek szerepelnek többször. A futtatása előtt feltétlen készíteni kell egy teljes adatbázis és lemezkép mentést.",
-    'maint_image_cleanup_ignore_revisions' => 'Képek figyelmen kívül hagyása a változatokban',
+    'maint_delete_images_only_in_revisions' => 'Also delete images that only exist in old page revisions',
     'maint_image_cleanup_run' => 'Tisztítás futtatása',
     'maint_image_cleanup_warning' => ':count potenciálisan nem használt képet találtam. Biztosan törölhetőek ezek a képek?',
     'maint_image_cleanup_success' => ':count potenciálisan nem használt kép megtalálva és törölve!',
@@ -80,17 +80,38 @@ return [
     'maint_send_test_email_mail_subject' => 'Teszt e-mail',
     'maint_send_test_email_mail_greeting' => 'Az email kézbesítés működőképesnek tűnik!',
     'maint_send_test_email_mail_text' => 'Gratulálunk! Mivel ez az email figyelmeztetés megérkezett az email beállítások megfelelőek.',
+    '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' => 'Lomtár megnyitása',
+
+    // Recycle Bin
+    'recycle_bin' => 'Lomtár',
+    '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' => 'Törölt elem',
+    'recycle_bin_deleted_by' => 'Törölte',
+    'recycle_bin_deleted_at' => 'Törlés ideje',
+    'recycle_bin_permanently_delete' => 'Permanently Delete',
+    'recycle_bin_restore' => 'Visszaállítás',
+    'recycle_bin_contents_empty' => 'A lomtár jelenleg üres',
+    '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',
     'audit_desc' => 'This audit log displays a list of activities tracked in the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.',
-    'audit_event_filter' => 'Event Filter',
-    'audit_event_filter_no_filter' => 'No Filter',
-    'audit_deleted_item' => 'Deleted Item',
-    'audit_deleted_item_name' => 'Name: :name',
-    'audit_table_user' => 'User',
-    'audit_table_event' => 'Event',
-    'audit_table_item' => 'Related Item',
+    'audit_event_filter' => 'Eseményszűrő',
+    'audit_event_filter_no_filter' => 'Nincs szűrő',
+    'audit_deleted_item' => 'Törölt elem',
+    'audit_deleted_item_name' => 'Név: :name',
+    'audit_table_user' => 'Felhasználó',
+    'audit_table_event' => 'Esemény',
+    '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' => 'Felhasználói profil',
     'users_add_new' => 'Új felhasználó hozzáadása',
     'users_search' => 'Felhasználók keresése',
+    'users_latest_activity' => 'Latest Activity',
     'users_details' => 'Felhasználó részletei',
     'users_details_desc' => 'Egy megjelenítendő név és email cím beállítása ennek a felhasználónak. Az email cím az alkalmazásba történő bejelentkezéshez lesz használva.',
     'users_details_desc_no_email' => 'Egy megjelenítendő név beállítása ennek a felhasználónak amiről mások felismerik.',
@@ -153,7 +175,10 @@ return [
     'users_delete_named' => ':userName felhasználó törlése',
     'users_delete_warning' => '\':userName\' felhasználó teljesen törölve lesz a rendszerből.',
     'users_delete_confirm' => 'Biztosan törölhető ez a felhasználó?',
-    'users_delete_success' => 'Felhasználó sikeresen eltávolítva',
+    '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' => 'Felhasználó szerkesztése',
     'users_edit_profile' => 'Profil szerkesztése',
     'users_edit_success' => 'Felhasználó sikeresen frissítve',
@@ -214,6 +239,7 @@ return [
         'ja' => '日本語',
         'ko' => '한국어',
         'nl' => 'Nederlands',
+        'nb' => 'Norsk (Bokmål)',
         'pl' => 'Polski',
         'pt_BR' => 'Português do Brasil',
         'ru' => 'Русский',
index 845126cda95c0b6a897087fc098d8d45995885f5..7c378e983cc58cf629c05005e11c9c1b938ce98d 100644 (file)
@@ -90,6 +90,7 @@ return [
     'required_without'     => ':attribute mező kötelező ha :values nincs beállítva.',
     'required_without_all' => ':attribute mező kötelező ha egyik :values sincs beállítva.',
     'same'                 => ':attribute és :other értékének egyeznie kell.',
+    'safe_url'             => 'The provided link may not be safe.',
     'size'                 => [
         'numeric' => ':attribute :size méretű kell legyen.',
         'file'    => ':attribute :size kilobájt méretű kell legyen.',
index c66651489394fc5b0ea34c8eda4d37729a93583c..24b484181ef75f34c16350b0ee454163dd0a1e73 100755 (executable)
@@ -45,4 +45,5 @@ return [
 
     // Other
     'commented_on'                => 'ha commentato in',
+    'permissions_update'          => 'updated permissions',
 ];
index ebc1976503b09441900acec94c211d55af58dd53..2d8766655218455247a398dfb4f4a6afeb76d7c4 100755 (executable)
@@ -22,6 +22,7 @@ return [
     'meta_created_name' => 'Creato :timeLength da :user',
     'meta_updated' => 'Aggiornato :timeLength',
     'meta_updated_name' => 'Aggiornato :timeLength da :user',
+    'meta_owned_name' => 'Owned by :user',
     'entity_select' => 'Selezione Entità',
     'images' => 'Immagini',
     'my_recent_drafts' => 'Bozze Recenti',
@@ -39,6 +40,7 @@ return [
     'permissions_intro' => 'Una volta abilitati, questi permessi avranno la priorità su tutti gli altri.',
     'permissions_enable' => 'Abilita Permessi Custom',
     'permissions_save' => 'Salva Permessi',
+    'permissions_owner' => 'Owner',
 
     // Search
     'search_results' => 'Risultati Ricerca',
@@ -146,7 +148,7 @@ return [
     'chapters_create' => 'Crea un nuovo capitolo',
     'chapters_delete' => 'Elimina Capitolo',
     'chapters_delete_named' => 'Elimina il capitolo :chapterName',
-    'chapters_delete_explain' => 'Questo eliminerà il capitolo \':chapterName\'. Tutte le pagine verranno spostate nel libro.',
+    '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' => 'Sei sicuro di voler eliminare questo capitolo?',
     'chapters_edit' => 'Elimina Capitolo',
     'chapters_edit_named' => 'Modifica il capitolo :chapterName',
@@ -208,6 +210,7 @@ return [
     'pages_revisions' => 'Versioni Pagina',
     'pages_revisions_named' => 'Versioni della pagina :pageName',
     'pages_revision_named' => 'Versione della pagina :pageName',
+    'pages_revision_restored_from' => 'Restored from #:id; :summary',
     'pages_revisions_created_by' => 'Creata Da',
     'pages_revisions_date' => 'Data Versione',
     'pages_revisions_number' => '#',
index b49281851b4bf07530c6bcf52b422bac3e13e6c0..f925de9620d3af06f896b8490fb7099c8ad72aae 100755 (executable)
@@ -68,7 +68,7 @@ return [
     'maint' => 'Manutenzione',
     'maint_image_cleanup' => 'Pulizia Immagini',
     'maint_image_cleanup_desc' => "Esegue la scansione del contenuto delle pagine e delle revisioni per verificare quali immagini e disegni sono attualmente in uso e quali immagini sono ridondanti. Assicurati di creare backup completo del database e delle immagini prima di eseguire la pulizia.",
-    'maint_image_cleanup_ignore_revisions' => 'Ignora le immagini nelle revisioni',
+    'maint_delete_images_only_in_revisions' => 'Also delete images that only exist in old page revisions',
     'maint_image_cleanup_run' => 'Esegui Pulizia',
     'maint_image_cleanup_warning' => ':count immagini potenzialmente inutilizzate sono state trovate. Sei sicuro di voler eliminare queste immagini?',
     'maint_image_cleanup_success' => ':count immagini potenzialmente inutilizzate trovate e eliminate!',
@@ -80,6 +80,27 @@ return [
     'maint_send_test_email_mail_subject' => 'Email di Test',
     'maint_send_test_email_mail_greeting' => 'L\'invio delle email sembra funzionare!',
     'maint_send_test_email_mail_text' => 'Congratulazioni! Siccome hai ricevuto questa notifica email, le tue impostazioni sembrano essere configurate correttamente.',
+    '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' => 'Apri il Cestino',
+
+    // Recycle Bin
+    'recycle_bin' => 'Cestino',
+    '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' => 'Cancellato da',
+    'recycle_bin_deleted_at' => 'Orario Cancellazione',
+    'recycle_bin_permanently_delete' => 'Elimina Definitivamente',
+    'recycle_bin_restore' => 'Ripristina',
+    'recycle_bin_contents_empty' => 'The recycle bin is currently empty',
+    'recycle_bin_empty' => 'Svuota Cestino',
+    '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',
@@ -88,9 +109,9 @@ return [
     'audit_event_filter_no_filter' => 'No Filter',
     'audit_deleted_item' => 'Deleted Item',
     'audit_deleted_item_name' => 'Name: :name',
-    'audit_table_user' => 'User',
-    'audit_table_event' => 'Event',
-    'audit_table_item' => 'Related Item',
+    'audit_table_user' => 'Utente',
+    'audit_table_event' => 'Evento',
+    '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' => 'Profilo Utente',
     'users_add_new' => 'Aggiungi Nuovo Utente',
     'users_search' => 'Cerca Utenti',
+    'users_latest_activity' => 'Latest Activity',
     'users_details' => 'Dettagli Utente',
     'users_details_desc' => 'Imposta un nome e un indirizzo email per questo utente. L\'indirizzo email verrà utilizzato per accedere all\'applicazione.',
     'users_details_desc_no_email' => 'Imposta un nome per questo utente così gli altri possono riconoscerlo.',
@@ -153,7 +175,10 @@ return [
     'users_delete_named' => 'Elimina l\'utente :userName',
     'users_delete_warning' => 'Questo eliminerà completamente l\'utente \':userName\' dal sistema.',
     'users_delete_confirm' => 'Sei sicuro di voler eliminare questo utente?',
-    'users_delete_success' => 'Utenti rimossi correttamente',
+    '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' => 'Nessun utente selezionato',
+    'users_delete_success' => 'Utente rimosso con successo',
     'users_edit' => 'Modifica Utente',
     'users_edit_profile' => 'Modifica Profilo',
     'users_edit_success' => 'Utente aggiornato correttamente',
@@ -214,6 +239,7 @@ return [
         'ja' => '日本語',
         'ko' => '한국어',
         'nl' => 'Nederlands',
+        'nb' => 'Norsk (Bokmål)',
         'pl' => 'Polski',
         'pt_BR' => 'Português do Brasil',
         'ru' => 'Русский',
index 3b85303d2b879477e83f4309db786bfde8b152ed..2e460cdc382715a74492197fe0d31da98364de5b 100755 (executable)
@@ -90,6 +90,7 @@ return [
     'required_without'     => 'Il campo :attribute è richiesto quando :values non è presente.',
     'required_without_all' => 'Il campo :attribute è richiesto quando nessuno dei :values sono presenti.',
     'same'                 => ':attribute e :other devono corrispondere.',
+    'safe_url'             => 'The provided link may not be safe.',
     'size'                 => [
         'numeric' => 'Il campo :attribute deve essere :size.',
         'file'    => 'Il campo :attribute deve essere :size kilobytes.',
index 1cc60eb540e305e2920a8adc4c32a5cb1a37e3ee..b1995a654f5b8c85b8ef39cdbf9c9fb85762fcc5 100644 (file)
@@ -45,4 +45,5 @@ return [
 
     // Other
     'commented_on'                => 'コメントする',
+    'permissions_update'          => 'updated permissions',
 ];
index 2e5bfda75178b02e6bb0609a32eb5864b26705ff..d220d5e38e756915d0fea34a9a64e0820159c05e 100644 (file)
@@ -22,6 +22,7 @@ return [
     'meta_created_name' => '作成: :timeLength (:user)',
     'meta_updated' => '更新: :timeLength',
     'meta_updated_name' => '更新: :timeLength (:user)',
+    'meta_owned_name' => 'Owned by :user',
     'entity_select' => 'エンティティ選択',
     'images' => '画像',
     'my_recent_drafts' => '最近の下書き',
@@ -39,6 +40,7 @@ return [
     'permissions_intro' => 'この設定は各ユーザの役割よりも優先して適用されます。',
     'permissions_enable' => 'カスタム権限設定を有効にする',
     'permissions_save' => '権限を保存',
+    'permissions_owner' => 'Owner',
 
     // Search
     'search_results' => '検索結果',
@@ -146,7 +148,7 @@ return [
     'chapters_create' => 'チャプターを作成',
     'chapters_delete' => 'チャプターを削除',
     'chapters_delete_named' => 'チャプター「:chapterName」を削除',
-    'chapters_delete_explain' => 'チャプター「:chapterName」を削除すると、チャプター内のすべてのページはブック内に直接追加されます。',
+    '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' => 'チャプターを削除してよろしいですか?',
     'chapters_edit' => 'チャプターを編集',
     'chapters_edit_named' => 'チャプター「:chapterName」を編集',
@@ -208,6 +210,7 @@ return [
     'pages_revisions' => '編集履歴',
     'pages_revisions_named' => ':pageName のリビジョン',
     'pages_revision_named' => ':pageName のリビジョン',
+    'pages_revision_restored_from' => 'Restored from #:id; :summary',
     'pages_revisions_created_by' => '作成者',
     'pages_revisions_date' => '日付',
     'pages_revisions_number' => 'リビジョン',
index 303ff57bfa83c73cc1dccffe678844517cfe6e2a..31ab22804ebc02b53041461f566850218a3b2efb 100644 (file)
@@ -68,7 +68,7 @@ return [
     'maint' => 'メンテナンス',
     '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' => 'クリーンアップを実行',
     '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' => 'テストメール',
     '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' => 'ユーザプロフィール',
     'users_add_new' => 'ユーザを追加',
     'users_search' => 'ユーザ検索',
+    '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' => 'ユーザ「:userName」を削除',
     'users_delete_warning' => 'ユーザ「:userName」を完全に削除します。',
     'users_delete_confirm' => '本当にこのユーザを削除してよろしいですか?',
-    'users_delete_success' => 'ユーザを削除しました',
+    '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' => 'ユーザ編集',
     'users_edit_profile' => 'プロフィール編集',
     'users_edit_success' => 'ユーザを更新しました',
@@ -214,6 +239,7 @@ return [
         'ja' => '日本語',
         'ko' => '한국어',
         'nl' => 'Nederlands',
+        'nb' => 'Norsk (Bokmål)',
         'pl' => 'Polski',
         'pt_BR' => 'Português do Brasil',
         'ru' => 'Русский',
index 231bdfa0b8f34f6ac7098f2ba1304d4ce465e6c5..61057f17ee00d6e3aedfddaf453af17d99f0f5a3 100644 (file)
@@ -90,6 +90,7 @@ return [
     'required_without'     => ':valuesが設定されていない場合、:attributeは必須です。',
     'required_without_all' => ':valuesが設定されていない場合、:attributeは必須です。',
     'same'                 => ':attributeと:otherは一致している必要があります。',
+    'safe_url'             => 'The provided link may not be safe.',
     'size'                 => [
         'numeric' => ':attributeは:sizeである必要があります。',
         'file'    => ':attributeは:sizeキロバイトである必要があります。',
index a56316ef82db2f3cd6d49d02949ec7f84122ac1d..fda7e4ef35ebb21aeca7c763841254356f5c75c5 100644 (file)
@@ -45,4 +45,5 @@ return [
 
     // Other
     'commented_on'                => '댓글 쓰기',
+    'permissions_update'          => 'updated permissions',
 ];
index 934c3f607268715593603fd0c372c9856b3b75a0..43156260224a02beaf79a5c12bf81df0540189b1 100644 (file)
@@ -33,7 +33,7 @@ return [
     'copy' => '복사',
     'reply' => '답글',
     'delete' => '삭제',
-    'delete_confirm' => 'Confirm Deletion',
+    'delete_confirm' => '삭제 요청 확인',
     'search' => '검색',
     'search_clear' => '검색 지우기',
     'reset' => '리셋',
index 1eadfccfa891745c122e75ff7cca3bfcd0bfbe1a..9155b9490ee6f531db61753c5a9d7a8363c80926 100644 (file)
@@ -15,7 +15,7 @@ return [
     'image_load_more' => '더 로드하기',
     'image_image_name' => '이미지 이름',
     'image_delete_used' => '이 이미지는 다음 문서들이 쓰고 있습니다.',
-    'image_delete_confirm_text' => 'Are you sure you want to delete this image?',
+    'image_delete_confirm_text' => '이 이미지를 정말 삭제하시겠습니까?',
     'image_select_image' => '이미지 선택',
     'image_dropzone' => '여기에 이미지를 드롭하거나 여기를 클릭하세요. 이미지를 올릴 수 있습니다.',
     'images_deleted' => '이미지 삭제함',
@@ -29,6 +29,6 @@ return [
     'code_editor' => '코드 수정',
     'code_language' => '언어',
     'code_content' => '내용',
-    'code_session_history' => 'Session History',
+    'code_session_history' => '세션 기록',
     'code_save' => '저장',
 ];
index a238b9a8ca08c5a9282524a685a67d79ae6d3751..71815cdd7c3ab544738f8a33fb904eadf2a8786a 100644 (file)
@@ -22,6 +22,7 @@ return [
     'meta_created_name' => '만듦 :timeLength, :user',
     'meta_updated' => '수정함 :timeLength',
     'meta_updated_name' => '수정함 :timeLength, :user',
+    'meta_owned_name' => 'Owned by :user',
     'entity_select' => '항목 선택',
     'images' => '이미지',
     'my_recent_drafts' => '내 최근의 초안 문서',
@@ -39,6 +40,7 @@ return [
     'permissions_intro' => '한번 허용하면 이 설정은 사용자 권한에 우선합니다.',
     'permissions_enable' => '설정 허용',
     'permissions_save' => '권한 저장',
+    'permissions_owner' => 'Owner',
 
     // Search
     'search_results' => '검색 결과',
@@ -47,8 +49,8 @@ return [
     'search_no_pages' => '결과 없음',
     'search_for_term' => ':term 검색',
     'search_more' => '더 많은 결과',
-    'search_advanced' => 'Advanced Search',
-    'search_terms' => 'Search Terms',
+    'search_advanced' => '고급 검색',
+    'search_terms' => '용어 검색',
     'search_content_type' => '형식',
     'search_exact_matches' => '정확히 일치',
     'search_tags' => '꼬리표 일치',
@@ -146,7 +148,7 @@ return [
     'chapters_create' => '챕터 만들기',
     'chapters_delete' => '챕터 삭제하기',
     'chapters_delete_named' => ':chapterName(을)를 지웁니다.',
-    'chapters_delete_explain' => ':chapterName에 있는 모든 문서는 챕터에서 벗어날 뿐 지우지 않습니다.',
+    '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' => '이 챕터를 지울 건가요?',
     'chapters_edit' => '챕터 바꾸기',
     'chapters_edit_named' => ':chapterName 바꾸기',
@@ -208,6 +210,7 @@ return [
     'pages_revisions' => '문서 수정본',
     'pages_revisions_named' => ':pageName 수정본',
     'pages_revision_named' => ':pageName 수정본',
+    'pages_revision_restored_from' => 'Restored from #:id; :summary',
     'pages_revisions_created_by' => '만든 사용자',
     'pages_revisions_date' => '수정한 날짜',
     'pages_revisions_number' => 'No.',
@@ -256,7 +259,7 @@ return [
     'attachments_upload' => '파일 올리기',
     'attachments_link' => '링크로 첨부',
     'attachments_set_link' => '링크 설정',
-    'attachments_delete' => 'Are you sure you want to delete this attachment?',
+    'attachments_delete' => '이 첨부파일을 정말 삭제하시겠습니까?',
     'attachments_dropzone' => '여기에 파일을 드롭하거나 여기를 클릭하세요.',
     'attachments_no_files' => '올린 파일 없음',
     'attachments_explain_link' => '파일을 올리지 않고 링크로 첨부할 수 있습니다.',
@@ -265,7 +268,7 @@ return [
     'attachments_link_url' => '파일로 링크',
     'attachments_link_url_hint' => '파일 주소',
     'attach' => '파일 첨부',
-    'attachments_insert_link' => 'Add Attachment Link to Page',
+    'attachments_insert_link' => '페이지에 첨부파일 링크 추가',
     'attachments_edit_file' => '파일 수정',
     'attachments_edit_file_name' => '파일 이름',
     'attachments_edit_drop_upload' => '여기에 파일을 드롭하거나 여기를 클릭하세요. 파일을 올리거나 덮어쓸 수 있습니다.',
index 7c368543fec7c6f6568cba441c2113b7c26d09e4..9fec26956757fbb45c6d8ddd94545c5a25c38bfd 100755 (executable)
@@ -68,7 +68,7 @@ return [
     'maint' => '데이터',
     'maint_image_cleanup' => '이미지 정리',
     'maint_image_cleanup_desc' => "중복한 이미지를 찾습니다. 실행하기 전에 이미지를 백업하세요.",
-    'maint_image_cleanup_ignore_revisions' => '수정본에 있는 이미지 제외',
+    'maint_delete_images_only_in_revisions' => 'Also delete images that only exist in old page revisions',
     'maint_image_cleanup_run' => '실행',
     'maint_image_cleanup_warning' => '이미지 :count개를 지울 건가요?',
     'maint_image_cleanup_success' => '이미지 :count개 삭제함',
@@ -80,20 +80,41 @@ return [
     'maint_send_test_email_mail_subject' => '테스트 메일',
     'maint_send_test_email_mail_greeting' => '이메일 전송이 성공하였습니다.',
     'maint_send_test_email_mail_text' => '축하합니다! 이 메일을 받음으로 이메일 설정이 정상적으로 되었음을 확인하였습니다.',
+    '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',
+    'audit' => '감사 기록',
     'audit_desc' => 'This audit log displays a list of activities tracked in the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.',
-    'audit_event_filter' => 'Event Filter',
-    'audit_event_filter_no_filter' => 'No Filter',
-    'audit_deleted_item' => 'Deleted Item',
-    'audit_deleted_item_name' => 'Name: :name',
-    'audit_table_user' => 'User',
-    'audit_table_event' => 'Event',
-    'audit_table_item' => 'Related Item',
-    'audit_table_date' => 'Activity Date',
-    'audit_date_from' => 'Date Range From',
-    'audit_date_to' => 'Date Range To',
+    'audit_event_filter' => '이벤트 필터',
+    'audit_event_filter_no_filter' => '필터 없음',
+    'audit_deleted_item' => '삭제된 항목',
+    'audit_deleted_item_name' => '이름: :name',
+    'audit_table_user' => '사용자',
+    'audit_table_event' => '이벤트',
+    'audit_table_related' => 'Related Item or Detail',
+    'audit_table_date' => '활동 날짜',
+    'audit_date_from' => '날짜 범위 시작',
+    'audit_date_to' => '날짜 범위 끝',
 
     // Role Settings
     'roles' => '권한',
@@ -136,6 +157,7 @@ return [
     'user_profile' => '사용자 프로필',
     'users_add_new' => '사용자 만들기',
     'users_search' => '사용자 검색',
+    'users_latest_activity' => 'Latest Activity',
     'users_details' => '사용자 정보',
     'users_details_desc' => '메일 주소로 로그인합니다.',
     'users_details_desc_no_email' => '사용자 이름을 바꿉니다.',
@@ -153,7 +175,10 @@ return [
     'users_delete_named' => ':userName 삭제',
     'users_delete_warning' => ':userName에 관한 데이터를 지웁니다.',
     'users_delete_confirm' => '이 사용자를 지울 건가요?',
-    'users_delete_success' => '사용자 삭제함',
+    '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' => '사용자 수정',
     'users_edit_profile' => '프로필 바꾸기',
     'users_edit_success' => '프로필 바꿈',
@@ -214,6 +239,7 @@ return [
         'ja' => '日本語',
         'ko' => '한국어',
         'nl' => 'Nederlands',
+        'nb' => 'Norsk (Bokmål)',
         'pl' => 'Polski',
         'pt_BR' => 'Português do Brasil',
         'ru' => 'Русский',
index 180faa35f377afa18958033edcdbf51416124f9a..64c4a4345b256d7d1c31f4dc9ea7466a3fc6e647 100644 (file)
@@ -90,6 +90,7 @@ return [
     'required_without'     => ':values(이)가 없을 때 :attribute(을)를 구성해야 합니다.',
     'required_without_all' => ':values(이)가 모두 없을 때 :attribute(을)를 구성해야 합니다.',
     'same'                 => ':attribute(와)과 :other(을)를 똑같이 구성하세요.',
+    'safe_url'             => 'The provided link may not be safe.',
     'size'                 => [
         'numeric' => ':attribute(을)를 :size(으)로 구성하세요.',
         'file'    => ':attribute(을)를 :size킬로바이트로 구성하세요.',
diff --git a/resources/lang/nb/activities.php b/resources/lang/nb/activities.php
new file mode 100644 (file)
index 0000000..e2516ab
--- /dev/null
@@ -0,0 +1,50 @@
+<?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å',
+    'permissions_update'          => 'updated permissions',
+];
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..5fab437
--- /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..dfee93e
--- /dev/null
@@ -0,0 +1,319 @@
+<?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',
+    'meta_owned_name' => 'Owned by :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',
+    'permissions_owner' => 'Owner',
+
+    // 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_revision_restored_from' => 'Restored from #:id; :summary',
+    '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..e952225
--- /dev/null
@@ -0,0 +1,256 @@
+<?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_delete_images_only_in_revisions' => 'Also delete images that only exist in old page revisions',
+    '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.',
+    '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' => '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_related' => 'Related Item or Detail',
+    '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_latest_activity' => 'Latest Activity',
+    '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_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' => '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',
+        'nb' => 'Norsk (Bokmål)',
+        '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..87e6c75
--- /dev/null
@@ -0,0 +1,115 @@
+<?php
+/**
+ * Validation Lines
+ * The following language lines contain the default error messages used by
+ * the validator class. Some of these rules have multiple versions such
+ * as the 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.',
+    'safe_url'             => 'The provided link may not be safe.',
+    '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 76272888119a3127329f3cf986a49514bdb016be..634ea1202f8837eeb83474c888ad0cc53c9fd633 100644 (file)
@@ -45,4 +45,5 @@ return [
 
     // Other
     'commented_on'                => 'reactie op',
+    'permissions_update'          => 'updated permissions',
 ];
index 399d97f2567a1561e8a23ef8d00ed058ff4bb226..0caab99f09afba113fd1804051a9d60bd399f162 100644 (file)
@@ -22,6 +22,7 @@ return [
     'meta_created_name' => 'Aangemaakt: :timeLength door :user',
     'meta_updated' => ':timeLength Aangepast',
     'meta_updated_name' => 'Aangepast: :timeLength door :user',
+    'meta_owned_name' => 'Owned by :user',
     'entity_select' => 'Entiteit Selecteren',
     'images' => 'Afbeeldingen',
     'my_recent_drafts' => 'Mijn Concepten',
@@ -39,6 +40,7 @@ return [
     'permissions_intro' => 'Als je dit aanzet, dan gelden rol-permissies niet meer voor deze pagina.',
     'permissions_enable' => 'Custom Permissies Aanzetten',
     'permissions_save' => 'Permissies Opslaan',
+    'permissions_owner' => 'Owner',
 
     // Search
     'search_results' => 'Zoekresultaten',
@@ -146,8 +148,7 @@ return [
     'chapters_create' => 'Hoofdstuk Toevoegen',
     'chapters_delete' => 'Hoofdstuk Verwijderen',
     'chapters_delete_named' => 'Verwijder Hoofdstuk :chapterName',
-    'chapters_delete_explain' => 'Dit verwijdert het hoofdstuk \':chapterName\', Alle pagina\'s zullen verwijdert worden.
-        en toegevoegd worden aan het bijbehorende boek.',
+    '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' => 'Weet je zeker dat je dit boek wilt verwijderen?',
     'chapters_edit' => 'Hoofdstuk Aanpassen',
     'chapters_edit_named' => 'Hoofdstuk :chapterName Aanpassen',
@@ -209,6 +210,7 @@ return [
     'pages_revisions' => 'Pagina Revisies',
     'pages_revisions_named' => 'Pagina Revisies voor :pageName',
     'pages_revision_named' => 'Pagina Revisie voor :pageName',
+    'pages_revision_restored_from' => 'Restored from #:id; :summary',
     'pages_revisions_created_by' => 'Aangemaakt door',
     'pages_revisions_date' => 'Revisiedatum',
     'pages_revisions_number' => '#',
index 4c87095b4f8ce8304df6958add8b4871b7cda5cf..12f3150a0d2b0e2f06f84896f802ab29ace08f9e 100644 (file)
@@ -68,7 +68,7 @@ return [
     'maint' => 'Onderhoud',
     'maint_image_cleanup' => 'Afbeeldingen opschonen',
     'maint_image_cleanup_desc' => "Scant pagina- en revisie inhoud om te controleren welke afbeeldingen en tekeningen momenteel worden gebruikt en welke afbeeldingen overbodig zijn. Zorg ervoor dat je een volledige database en afbeelding backup maakt voordat je dit uitvoert.",
-    'maint_image_cleanup_ignore_revisions' => 'Afbeeldingen in revisies negeren',
+    'maint_delete_images_only_in_revisions' => 'Also delete images that only exist in old page revisions',
     'maint_image_cleanup_run' => 'Opschonen uitvoeren',
     'maint_image_cleanup_warning' => ':count potentieel ongebruikte afbeeldingen gevonden. Weet u zeker dat u deze afbeeldingen wilt verwijderen?',
     'maint_image_cleanup_success' => ':count potentieel ongebruikte afbeeldingen gevonden en verwijderd!',
@@ -80,6 +80,27 @@ return [
     'maint_send_test_email_mail_subject' => 'Test E-mail',
     'maint_send_test_email_mail_greeting' => 'E-mailbezorging lijkt te werken!',
     'maint_send_test_email_mail_text' => 'Gefeliciteerd! Nu je deze e-mailmelding hebt ontvangen, lijken je e-mailinstellingen correct te zijn geconfigureerd.',
+    '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' => 'Gebruikersprofiel',
     'users_add_new' => 'Gebruiker toevoegen',
     'users_search' => 'Gebruiker zoeken',
+    'users_latest_activity' => 'Latest Activity',
     'users_details' => 'Gebruiker details',
     'users_details_desc' => 'Stel een weergavenaam en e-mailadres in voor deze gebruiker. Het e-mailadres zal worden gebruikt om in te loggen.',
     'users_details_desc_no_email' => 'Stel een weergavenaam in voor deze gebruiker zodat anderen deze kunnen herkennen.',
@@ -153,7 +175,10 @@ return [
     'users_delete_named' => 'Verwijder gebruiker :userName',
     'users_delete_warning' => 'Dit zal de gebruiker \':userName\' volledig uit het systeem verwijderen.',
     'users_delete_confirm' => 'Weet je zeker dat je deze gebruiker wilt verwijderen?',
-    'users_delete_success' => 'Gebruiker succesvol verwijderd',
+    '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' => 'Bewerk Gebruiker',
     'users_edit_profile' => 'Bewerk Profiel',
     'users_edit_success' => 'Gebruiker succesvol bijgewerkt',
@@ -214,6 +239,7 @@ return [
         'ja' => '日本語',
         'ko' => '한국어',
         'nl' => 'Nederlands',
+        'nb' => 'Norsk (Bokmål)',
         'pl' => 'Polski',
         'pt_BR' => 'Português do Brasil',
         'ru' => 'Русский',
index f85e5786f78b0cfc2385873a38928e5de7857e16..2f8ffc9f0531959e4ff2098ad06f7a297890d9dd 100644 (file)
@@ -90,6 +90,7 @@ return [
     'required_without'     => ':attribute veld is verplicht wanneer :values niet ingesteld is.',
     'required_without_all' => ':attribute veld is verplicht wanneer geen van :values ingesteld zijn.',
     'same'                 => ':attribute en :other moeten overeenkomen.',
+    'safe_url'             => 'The provided link may not be safe.',
     'size'                 => [
         'numeric' => ':attribute moet :size zijn.',
         'file'    => ':attribute moet :size kilobytes zijn.',
index f641ca23212d7f450bbd831499c310c163f5533d..c4e8ef0de831b0954d646c4007138a13bf40b450 100644 (file)
@@ -45,4 +45,5 @@ return [
 
     // Other
     'commented_on'                => 'skomentował',
+    'permissions_update'          => 'updated permissions',
 ];
index 89a934291dfa06d0737a8da63fa75bb483be8702..59960bd301e3b9b7564b607a23d15265175f44ac 100644 (file)
@@ -22,6 +22,7 @@ return [
     'meta_created_name' => 'Utworzono :timeLength przez :user',
     'meta_updated' => 'Zaktualizowano :timeLength',
     'meta_updated_name' => 'Zaktualizowano :timeLength przez :user',
+    'meta_owned_name' => 'Owned by :user',
     'entity_select' => 'Wybór obiektu',
     'images' => 'Obrazki',
     'my_recent_drafts' => 'Moje ostatnie wersje robocze',
@@ -39,6 +40,7 @@ return [
     'permissions_intro' => 'Jeśli włączone są indywidualne uprawnienia, to te uprawnienia będą miały priorytet względem pozostałych ustawionych uprawnień ról.',
     'permissions_enable' => 'Włącz własne uprawnienia',
     'permissions_save' => 'Zapisz uprawnienia',
+    'permissions_owner' => 'Owner',
 
     // Search
     'search_results' => 'Wyniki wyszukiwania',
@@ -146,8 +148,7 @@ return [
     'chapters_create' => 'Utwórz nowy rozdział',
     'chapters_delete' => 'Usuń rozdział',
     'chapters_delete_named' => 'Usuń rozdział :chapterName',
-    'chapters_delete_explain' => 'To spowoduje usunięcie rozdziału \':chapterName\', Wszystkie strony zostaną usunięte
-        i dodane bezpośrednio do książki.',
+    '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' => 'Czy na pewno chcesz usunąć ten rozdział?',
     'chapters_edit' => 'Edytuj rozdział',
     'chapters_edit_named' => 'Edytuj rozdział :chapterName',
@@ -209,6 +210,7 @@ return [
     'pages_revisions' => 'Wersje strony',
     'pages_revisions_named' => 'Wersje strony :pageName',
     'pages_revision_named' => 'Wersja strony :pageName',
+    'pages_revision_restored_from' => 'Restored from #:id; :summary',
     'pages_revisions_created_by' => 'Utworzona przez',
     'pages_revisions_date' => 'Data wersji',
     'pages_revisions_number' => '#',
index 4d7936e480876f025575e9c2b95fcc5eb50473f9..abba0ce60d89fc62962f943cecfe14757618321d 100644 (file)
@@ -68,7 +68,7 @@ return [
     'maint' => 'Konserwacja',
     'maint_image_cleanup' => 'Czyszczenie obrazków',
     'maint_image_cleanup_desc' => "Skanuje zawartość strony i poprzednie wersje, aby sprawdzić, które obrazy i rysunki są aktualnie używane, a które obrazy są zbędne. Przed uruchomieniem tej opcji należy utworzyć pełną kopię zapasową bazy danych i obrazków.",
-    'maint_image_cleanup_ignore_revisions' => 'Ignoruje obrazki w poprzednich wersjach',
+    'maint_delete_images_only_in_revisions' => 'Also delete images that only exist in old page revisions',
     'maint_image_cleanup_run' => 'Uruchom czyszczenie',
     'maint_image_cleanup_warning' => 'Znaleziono :count potencjalnie niepotrzebnych obrazków. Czy na pewno chcesz je usunąć?',
     'maint_image_cleanup_success' => ':count potencjalnie nieużywane obrazki zostały znalezione i usunięte!',
@@ -80,6 +80,27 @@ return [
     'maint_send_test_email_mail_subject' => 'E-mail testowy',
     'maint_send_test_email_mail_greeting' => 'Wygląda na to, że wysyłka wiadomości e-mail działa!',
     'maint_send_test_email_mail_text' => 'Gratulacje! Otrzymałeś tego e-maila więc Twoje ustawienia poczty elektronicznej wydają się być prawidłowo skonfigurowane.',
+    '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' => 'Profil użytkownika',
     'users_add_new' => 'Dodaj użytkownika',
     'users_search' => 'Wyszukaj użytkownika',
+    'users_latest_activity' => 'Latest Activity',
     'users_details' => 'Szczegóły użytkownika',
     'users_details_desc' => 'Ustaw wyświetlaną nazwę i adres e-mail dla tego użytkownika. Adres e-mail zostanie wykorzystany do zalogowania się do aplikacji.',
     'users_details_desc_no_email' => 'Ustaw wyświetlaną nazwę dla tego użytkownika, aby inni mogli go rozpoznać.',
@@ -153,7 +175,10 @@ return [
     'users_delete_named' => 'Usuń :userName',
     'users_delete_warning' => 'To usunie użytkownika \':userName\' z systemu.',
     'users_delete_confirm' => 'Czy na pewno chcesz usunąć tego użytkownika?',
-    'users_delete_success' => 'Użytkownik usunięty pomyślnie',
+    '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' => 'Edytuj użytkownika',
     'users_edit_profile' => 'Edytuj profil',
     'users_edit_success' => 'Użytkownik zaktualizowany pomyślnie',
@@ -214,6 +239,7 @@ return [
         'ja' => '日本語',
         'ko' => '한국어',
         'nl' => 'Nederlands',
+        'nb' => 'Norsk (Bokmål)',
         'pl' => 'Polski',
         'pt_BR' => 'Português do Brasil',
         'ru' => 'Русский',
index 249d20ca7384b26407c2c1e7d815061fff05a471..26ac348c1aa955d97af065e3bd538f7eb229df10 100644 (file)
@@ -90,6 +90,7 @@ return [
     'required_without'     => 'Pole :attribute jest wymagane jeśli :values nie zostało wprowadzone.',
     'required_without_all' => 'Pole :attribute jest wymagane jeśli żadna z wartości :values nie została podana.',
     'same'                 => 'Pole :attribute i :other muszą być takie same.',
+    'safe_url'             => 'The provided link may not be safe.',
     'size'                 => [
         'numeric' => ':attribute musi mieć długość :size.',
         'file'    => ':attribute musi mieć :size kilobajtów.',
index 9789fe3bee23e5e6ff3a5d14de0ecbf952314708..2fa3d65717f82856b0ddfe62f33b6cc487c54649 100644 (file)
@@ -45,4 +45,5 @@ return [
 
     // Other
     'commented_on'                => 'comentou em',
+    'permissions_update'          => 'updated permissions',
 ];
index b37d179314a27b261e89f91fa6ba50cb349db6f0..129e1f260876dabb5a63088bc9f2e26ec523ad59 100644 (file)
@@ -22,6 +22,7 @@ return [
     'meta_created_name' => 'Criado :timeLength por :user',
     'meta_updated' => 'Atualizado :timeLength',
     'meta_updated_name' => 'Atualizado :timeLength por :user',
+    'meta_owned_name' => 'Owned by :user',
     'entity_select' => 'Seleção de Entidade',
     'images' => 'Imagens',
     'my_recent_drafts' => 'Meus Rascunhos Recentes',
@@ -39,6 +40,7 @@ return [
     'permissions_intro' => 'Uma vez habilitadas, estas permissões terão prioridade sobre outro conjunto de permissões.',
     'permissions_enable' => 'Habilitar Permissões Customizadas',
     'permissions_save' => 'Salvar Permissões',
+    'permissions_owner' => 'Owner',
 
     // Search
     'search_results' => 'Resultado(s) da Pesquisa',
@@ -146,7 +148,7 @@ return [
     'chapters_create' => 'Criar Novo Capítulo',
     'chapters_delete' => 'Excluir Capítulo',
     'chapters_delete_named' => 'Excluir Capítulo :chapterName',
-    'chapters_delete_explain' => 'A ação vai excluir o capítulo de nome \':chapterName\'. Todas as páginas do capítulo serão removidas e adicionadas diretamente ao livro pai.',
+    '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' => 'Tem certeza que deseja excluir o capítulo?',
     'chapters_edit' => 'Editar Capítulo',
     'chapters_edit_named' => 'Editar Capítulo :chapterName',
@@ -208,6 +210,7 @@ return [
     'pages_revisions' => 'Revisões da Página',
     'pages_revisions_named' => 'Revisões de Página para :pageName',
     'pages_revision_named' => 'Revisão de Página para :pageName',
+    'pages_revision_restored_from' => 'Restored from #:id; :summary',
     'pages_revisions_created_by' => 'Criada por',
     'pages_revisions_date' => 'Data da Revisão',
     'pages_revisions_number' => '#',
index a4da2ad880b6612eed9a163f97b5f17457ce3977..a970e023117ba617662a4407447cbaa0bc699faf 100644 (file)
@@ -68,7 +68,7 @@ return [
     'maint' => 'Manutenção',
     'maint_image_cleanup' => 'Limpeza de Imagens',
     'maint_image_cleanup_desc' => "Examina páginas e revisa seus conteúdos para verificar quais imagens e desenhos estão atualmente em uso e quais são redundantes. Certifique-se de criar um backup completo do banco de dados e imagens antes de executar esta ação.",
-    'maint_image_cleanup_ignore_revisions' => 'Ignorar imagens em revisões',
+    'maint_delete_images_only_in_revisions' => 'Also delete images that only exist in old page revisions',
     'maint_image_cleanup_run' => 'Executar Limpeza',
     'maint_image_cleanup_warning' => ':count imagens potencialmente não utilizadas foram encontradas. Tem certeza de que deseja excluir estas imagens?',
     'maint_image_cleanup_success' => ':count imagens potencialmente não utilizadas foram encontradas e excluídas!',
@@ -80,18 +80,39 @@ return [
     'maint_send_test_email_mail_subject' => 'E-mail de Teste',
     'maint_send_test_email_mail_greeting' => 'O envio de e-mails parece funcionar!',
     'maint_send_test_email_mail_text' => 'Parabéns! Já que você recebeu esta notificação, suas opções de e-mail parecem estar configuradas corretamente.',
+    'maint_recycle_bin_desc' => 'Prateleiras, livros, capítulos e páginas deletados são mandados para a lixeira podendo assim ser restaurados ou excluídos permanentemente. Itens mais antigos da lixeira podem vir a ser automaticamente removidos da lixeira após um tempo dependendo da configuração do sistema.',
+    'maint_recycle_bin_open' => 'Abrir Lixeira',
+
+    // Recycle Bin
+    'recycle_bin' => 'Lixeira',
+    'recycle_bin_desc' => 'Aqui você pode restaurar itens que foram excluídos ou escolher removê-los permanentemente do sistema. Esta lista não é filtrada diferentemente de listas de atividades similares no sistema onde filtros de permissão são aplicados.',
+    'recycle_bin_deleted_item' => 'Item excluído',
+    'recycle_bin_deleted_by' => 'Excluído por',
+    'recycle_bin_deleted_at' => 'Momento de Exclusão',
+    'recycle_bin_permanently_delete' => 'Excluir permanentemente',
+    'recycle_bin_restore' => 'Restaurar',
+    'recycle_bin_contents_empty' => 'A lixeira está vazia',
+    'recycle_bin_empty' => 'Esvaziar Lixeira',
+    'recycle_bin_empty_confirm' => 'Isso irá destruir permanentemente todos os itens na lixeira inclusive o conteúdo de cada item. Tem certeza de que quer esvaziar a lixeira?',
+    'recycle_bin_destroy_confirm' => 'Esta ação irá excluir permanentemente do sistema este item junto com todos os elementos filhos listados abaixo. Você não poderá restaurar esse conteúdo. Tem certeza de que deseja excluir permanentemente este item?',
+    'recycle_bin_destroy_list' => 'Items to be Destroyed',
+    'recycle_bin_restore_list' => 'Items to be Restored',
+    'recycle_bin_restore_confirm' => 'Esta ação irá restaurar o item excluído, inclusive quaisquer elementos filhos, para seu local original. Se a localização original tiver, entretanto, sido eliminada e estiver agora na lixeira, o item pai também precisará ser restaurado.',
+    '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',
+    'audit' => 'Registro de auditoria',
     'audit_desc' => 'This audit log displays a list of activities tracked in the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.',
     'audit_event_filter' => 'Event Filter',
-    'audit_event_filter_no_filter' => 'No Filter',
-    'audit_deleted_item' => 'Deleted Item',
-    'audit_deleted_item_name' => 'Name: :name',
-    'audit_table_user' => 'User',
-    'audit_table_event' => 'Event',
-    'audit_table_item' => 'Related Item',
-    'audit_table_date' => 'Activity Date',
+    'audit_event_filter_no_filter' => 'Sem filtro',
+    'audit_deleted_item' => 'Item excluído',
+    'audit_deleted_item_name' => 'Nome: :name',
+    'audit_table_user' => 'Usuário',
+    'audit_table_event' => 'Evento',
+    'audit_table_related' => 'Related Item or Detail',
+    'audit_table_date' => 'Data da Atividade',
     'audit_date_from' => 'Date Range From',
     'audit_date_to' => 'Date Range To',
 
@@ -136,6 +157,7 @@ return [
     'user_profile' => 'Perfil do Usuário',
     'users_add_new' => 'Adicionar Novo Usuário',
     'users_search' => 'Pesquisar Usuários',
+    'users_latest_activity' => 'Latest Activity',
     'users_details' => 'Detalhes do Usuário',
     'users_details_desc' => 'Defina um nome de exibição e um endereço de e-mail para este usuário. O endereço de e-mail será usado para fazer login na aplicação.',
     'users_details_desc_no_email' => 'Defina um nome de exibição para este usuário para que outros usuários possam reconhecê-lo',
@@ -153,7 +175,10 @@ return [
     'users_delete_named' => 'Excluir :userName',
     'users_delete_warning' => 'A ação vai excluir completamente o usuário de nome \':userName\' do sistema.',
     'users_delete_confirm' => 'Tem certeza que deseja excluir esse usuário?',
-    'users_delete_success' => 'Usuários excluídos com sucesso',
+    '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' => 'Editar Usuário',
     'users_edit_profile' => 'Editar Perfil',
     'users_edit_success' => 'Usuário atualizado com sucesso',
@@ -214,6 +239,7 @@ return [
         'ja' => '日本語',
         'ko' => '한국어',
         'nl' => 'Nederlands',
+        'nb' => 'Norsk (Bokmål)',
         'pl' => 'Polski',
         'pt_BR' => 'Português do Brasil',
         'ru' => 'Русский',
index 718fca6b9323894f0afaf96d8958f1f0fcb58ca1..9777eacac5c5ff756f94a776437fee19ed9a48d2 100644 (file)
@@ -90,6 +90,7 @@ return [
     'required_without'     => 'O campo :attribute é requerido quando os valores :values não estiverem presentes.',
     'required_without_all' => 'O campo :attribute é requerido quando nenhum dos valores :values estiverem presentes.',
     'same'                 => 'O campo :attribute e o campo :other devem ser iguais.',
+    'safe_url'             => 'The provided link may not be safe.',
     'size'                 => [
         'numeric' => 'O tamanho do campo :attribute deve ser :size.',
         'file'    => 'O tamanho do arquivo :attribute deve ser de :size kilobytes.',
index 06b1e354b1e8948e5efe147be7d7180824208ea5..af5a7af1d93fc7e8d8c4a91ccf593c5fc854fa22 100644 (file)
@@ -45,4 +45,5 @@ return [
 
     // Other
     'commented_on'                => 'прокомментировал',
+    'permissions_update'          => 'обновил разрешения',
 ];
index 13747e36e950c24804a89f05ddcc0c676ec2708a..e78ffde6563acbd8cce09ef059a1616656f4a920 100644 (file)
@@ -22,6 +22,7 @@ return [
     'meta_created_name' => ':user создал :timeLength',
     'meta_updated' => 'Обновлено :timeLength',
     'meta_updated_name' => ':user обновил :timeLength',
+    'meta_owned_name' => 'Владелец :user',
     'entity_select' => 'Выбор объекта',
     'images' => 'Изображения',
     'my_recent_drafts' => 'Мои последние черновики',
@@ -39,6 +40,7 @@ return [
     'permissions_intro' => 'После включения опции эти разрешения будут иметь приоритет над любыми установленными разрешениями роли.',
     'permissions_enable' => 'Включение пользовательских разрешений',
     'permissions_save' => 'Сохранить разрешения',
+    'permissions_owner' => 'Владелец',
 
     // Search
     'search_results' => 'Результаты поиска',
@@ -146,7 +148,7 @@ return [
     'chapters_create' => 'Создать новую главу',
     'chapters_delete' => 'Удалить главу',
     'chapters_delete_named' => 'Удалить главу :chapterName',
-    'chapters_delete_explain' => 'Это удалит главу с именем \':chapterName\'. Все страницы главы будут удалены и перемещены напрямую в книгу.',
+    'chapters_delete_explain' => 'Это действие удалит главу с названием \':chapterName\'. Все страницы, которые существуют в этой главе, также будут удалены.',
     'chapters_delete_confirm' => 'Вы действительно хотите удалить эту главу?',
     'chapters_edit' => 'Редактировать главу',
     'chapters_edit_named' => 'Редактировать главу :chapterName',
@@ -208,6 +210,7 @@ return [
     'pages_revisions' => 'Версии страницы',
     'pages_revisions_named' => 'Версии страницы для :pageName',
     'pages_revision_named' => 'Версия страницы для :pageName',
+    'pages_revision_restored_from' => 'Восстановлено из #:id; :summary',
     'pages_revisions_created_by' => 'Создана',
     'pages_revisions_date' => 'Дата версии',
     'pages_revisions_number' => '#',
index 8456f599b4eff532a591c4cc25b2600595f33772..472973ca2a2c1b08c45a9da9579be29ef8084880 100755 (executable)
@@ -68,7 +68,7 @@ return [
     'maint' => 'Обслуживание',
     'maint_image_cleanup' => 'Очистка изображений',
     'maint_image_cleanup_desc' => "Сканирует содержимое страниц и предыдущих версий и определяет изображения, которые не используются. Убедитесь, что у вас есть резервная копия базы данных и папки изображений перед запуском этой функции.",
-    'maint_image_cleanup_ignore_revisions' => 'Пропускать изображения в версиях',
+    'maint_delete_images_only_in_revisions' => 'Также удалять изображения, которые существуют только в старой версии страницы',
     'maint_image_cleanup_run' => 'Выполнить очистку',
     'maint_image_cleanup_warning' => 'Найдено :count возможно бесполезных изображений. Вы уверены, что хотите удалить эти изображения?',
     'maint_image_cleanup_success' => ':count возможно бесполезных изображений было найдено и удалено!',
@@ -80,17 +80,38 @@ return [
     'maint_send_test_email_mail_subject' => 'Проверка электронной почты',
     'maint_send_test_email_mail_greeting' => 'Доставка электронной почты работает!',
     'maint_send_test_email_mail_text' => 'Поздравляем! Поскольку вы получили это письмо, электронная почта настроена правильно.',
+    'maint_recycle_bin_desc' => 'Удаленные полки, книги, главы и страницы отправляются в корзину, чтобы они могли быть восстановлены или удалены навсегда. Более старые элементы в корзине могут быть автоматически удалены через некоторое время в зависимости от системной конфигурации.',
+    'maint_recycle_bin_open' => 'Открыть корзину',
+
+    // Recycle Bin
+    'recycle_bin' => 'Корзина',
+    'recycle_bin_desc' => 'Здесь вы можете восстановить удаленные элементы или навсегда удалить их из системы. Этот список не отфильтрован в отличие от аналогичных списков действий в системе, где применяются фильтры.',
+    'recycle_bin_deleted_item' => 'Удаленный элемент',
+    'recycle_bin_deleted_by' => 'Удалён',
+    'recycle_bin_deleted_at' => 'Время удаления',
+    'recycle_bin_permanently_delete' => 'Удалить навсегда',
+    'recycle_bin_restore' => 'Восстановить',
+    'recycle_bin_contents_empty' => 'На данный момент корзина пуста',
+    'recycle_bin_empty' => 'Очистить корзину',
+    'recycle_bin_empty_confirm' => 'Это действие навсегда уничтожит все элементы в корзине, включая содержимое, содержащееся в каждом элементе. Вы уверены, что хотите очистить корзину?',
+    'recycle_bin_destroy_confirm' => 'Это действие удалит этот элемент навсегда вместе с любыми дочерними элементами, перечисленными ниже, и вы не сможете восстановить этот контент. Вы уверены, что хотите навсегда удалить этот элемент?',
+    'recycle_bin_destroy_list' => 'Элементы для удаления',
+    'recycle_bin_restore_list' => 'Элементы для восстановления',
+    'recycle_bin_restore_confirm' => 'Это действие восстановит удаленный элемент, включая дочерние, в исходное место. Если исходное место было удалено и теперь находится в корзине, родительский элемент также необходимо будет восстановить.',
+    'recycle_bin_restore_deleted_parent' => 'Родитель этого элемента также был удален. Элементы будут удалены до тех пор, пока этот родитель не будет восстановлен.',
+    'recycle_bin_destroy_notification' => 'Удалено :count элементов из корзины.',
+    'recycle_bin_restore_notification' => 'Восстановлено :count элементов из корзины',
 
     // Audit Log
     'audit' => 'Журнал аудита',
-    'audit_desc' => 'Этот журнал аудита отображает список действий, отслеживаемых в системе. Этот список не отфильтрован в отличие от аналогичных списков действий в системе, где применяются фильтры разрешений.',
+    'audit_desc' => 'Этот журнал аудита отображает список действий, отслеживаемых в системе. Этот список не отфильтрован в отличие от аналогичных списков действий в системе, где применяются фильтры.',
     'audit_event_filter' => 'Фильтр событий',
     'audit_event_filter_no_filter' => 'Без фильтра',
     'audit_deleted_item' => 'Удаленный элемент',
     'audit_deleted_item_name' => 'Имя: :name',
     'audit_table_user' => 'Пользователь',
     'audit_table_event' => 'Событие',
-    'audit_table_item' => 'Связанный элемент',
+    'audit_table_related' => 'Связанный элемент',
     'audit_table_date' => 'Дата действия',
     'audit_date_from' => 'Диапазон даты от',
     'audit_date_to' => 'Диапазон даты до',
@@ -136,6 +157,7 @@ return [
     'user_profile' => 'Профиль пользователя',
     'users_add_new' => 'Добавить пользователя',
     'users_search' => 'Поиск пользователей',
+    'users_latest_activity' => 'Последние действия',
     'users_details' => 'Данные пользователя',
     'users_details_desc' => 'Укажите имя и адрес электронной почты для этого пользователя. Адрес электронной почты будет использоваться для входа в приложение.',
     'users_details_desc_no_email' => 'Задайте имя для этого пользователя, чтобы другие могли его узнать.',
@@ -151,9 +173,12 @@ return [
     'users_system_public' => 'Этот пользователь представляет любых гостевых пользователей, которые посещают ваше приложение. Он не может использоваться для входа в систему и назначается автоматически.',
     'users_delete' => 'Удалить пользователя',
     'users_delete_named' => 'Удалить пользователя :userName',
-    'users_delete_warning' => 'Это полностью удалит пользователя с именем \':userName\' из системы.',
+    'users_delete_warning' => 'Это полностью удалит пользователя \':userName\' из системы.',
     'users_delete_confirm' => 'Вы уверены что хотите удалить этого пользователя?',
-    'users_delete_success' => 'Пользователи успешно удалены',
+    'users_migrate_ownership' => 'Наследник контента',
+    'users_migrate_ownership_desc' => 'Выберите пользователя, если вы хотите, чтобы он стал владельцем всех элементов, в настоящее время принадлежащих удаляемому пользователю.',
+    'users_none_selected' => 'Пользователь не выбран',
+    'users_delete_success' => 'Пользователь успешно удален',
     'users_edit' => 'Редактировать пользователя',
     'users_edit_profile' => 'Редактировать профиль',
     'users_edit_success' => 'Пользователь успешно обновлен',
@@ -214,6 +239,7 @@ return [
         'ja' => '日本語',
         'ko' => '한국어',
         'nl' => 'Nederlands',
+        'nb' => 'Norsk (Bokmål)',
         'pl' => 'Polski',
         'pt_BR' => 'Português do Brasil',
         'ru' => 'Русский',
index e5e9d1eec22cfdeba75674adedb76c5fab0cfc34..d16ead5d843e737384416fd8152f9519cf83c337 100644 (file)
@@ -90,6 +90,7 @@ return [
     'required_without'     => ':attribute обязательное поле когда :values не установлены.',
     'required_without_all' => ':attribute обязательное поле когда ни одно из :values не установлены.',
     'same'                 => ':attribute и :other должны совпадать.',
+    'safe_url'             => 'Предоставленная ссылка может быть небезопасной.',
     'size'                 => [
         'numeric' => ':attribute должен быть :size.',
         'file'    => ':attribute должен быть :size килобайт.',
index f07506c2978895e26c865bcdd537842c46d3050d..bbdec3cf7543c2c3d0206096397b41c9e8533336 100644 (file)
@@ -6,43 +6,44 @@
 return [
 
     // Pages
-    'page_create'                 => 'vytvoril stránku',
+    'page_create'                 => 'vytvoril(a) stránku',
     'page_create_notification'    => 'Stránka úspešne vytvorená',
     'page_update'                 => 'aktualizoval stránku',
     'page_update_notification'    => 'Stránka úspešne aktualizovaná',
-    'page_delete'                 => 'odstránil stránku',
+    'page_delete'                 => 'odstránil(a) stránku',
     'page_delete_notification'    => 'Stránka úspešne odstránená',
-    'page_restore'                => 'obnovil stránku',
+    'page_restore'                => 'obnovil(a) stránku',
     'page_restore_notification'   => 'Stránka úspešne obnovená',
-    'page_move'                   => 'presunul stránku',
+    'page_move'                   => 'presunul(a) stránku',
 
     // Chapters
-    'chapter_create'              => 'vytvoril kapitolu',
+    'chapter_create'              => 'vytvoril(a) kapitolu',
     'chapter_create_notification' => 'Kapitola úspešne vytvorená',
-    'chapter_update'              => 'aktualizoval kapitolu',
+    'chapter_update'              => 'aktualizoval(a) kapitolu',
     'chapter_update_notification' => 'Kapitola úspešne aktualizovaná',
-    'chapter_delete'              => 'odstránil kapitolu',
+    'chapter_delete'              => 'odstránil(a) kapitolu',
     'chapter_delete_notification' => 'Kapitola úspešne odstránená',
-    'chapter_move'                => 'presunul kapitolu',
+    'chapter_move'                => 'presunul(a) kapitolu',
 
     // Books
-    'book_create'                 => 'vytvoril knihu',
+    'book_create'                 => 'vytvoril(a) knihu',
     'book_create_notification'    => 'Kniha úspešne vytvorená',
-    'book_update'                 => 'aktualizoval knihu',
+    'book_update'                 => 'aktualizoval(a) knihu',
     'book_update_notification'    => 'Kniha úspešne aktualizovaná',
-    'book_delete'                 => 'odstránil knihu',
+    'book_delete'                 => 'odstránil(a) knihu',
     'book_delete_notification'    => 'Kniha úspešne odstránená',
-    'book_sort'                   => 'zoradil knihu',
+    'book_sort'                   => 'zoradil(a) knihu',
     'book_sort_notification'      => 'Kniha úspešne znovu zoradená',
 
     // Bookshelves
-    'bookshelf_create'            => 'vytvorená knižnica',
+    'bookshelf_create'            => 'vytvoril(a) knižnicu',
     'bookshelf_create_notification'    => 'Knižnica úspešne vytvorená',
-    'bookshelf_update'                 => 'aktualizovaná knižnica',
+    'bookshelf_update'                 => 'aktualizoval(a) knižnicu',
     'bookshelf_update_notification'    => 'Knižnica úspešne aktualizovaná',
-    'bookshelf_delete'                 => 'odstránená knižnica',
+    'bookshelf_delete'                 => 'odstránil(a) knižnicu',
     'bookshelf_delete_notification'    => 'Knižnica úspešne odstránená',
 
     // Other
-    'commented_on'                => 'komentované na',
+    'commented_on'                => 'komentoval(a)',
+    'permissions_update'          => 'aktualizované oprávnenia',
 ];
index ccde1f0405b159052bf84dd5ad73f0da4d7a2759..0d96811a3671aa6bcad8ccf2e80d4ba537c0b464 100644 (file)
@@ -6,7 +6,7 @@
  */
 return [
 
-    'failed' => 'Tieto údaje nesedia s našimi záznamami.',
+    'failed' => 'Tieto údaje sa nezhodujú s našimi záznamami.',
     'throttle' => 'Priveľa pokusov o prihlásenie. Skúste znova o :seconds sekúnd.',
 
     // Login & Register
@@ -24,54 +24,54 @@ return [
     'password_hint' => 'Musí mať viac ako 7 znakov',
     'forgot_password' => 'Zabudli ste heslo?',
     'remember_me' => 'Zapamätať si ma',
-    'ldap_email_hint' => 'Zadajte prosím email, ktorý sa má použiť pre tento účet.',
+    'ldap_email_hint' => 'Zadajte prosím e-mail, ktorý sa má použiť pre tento účet.',
     'create_account' => 'Vytvoriť účet',
     'already_have_account' => 'Už máte svoj ​​účet?',
     'dont_have_account' => 'Nemáte účet?',
     'social_login' => 'Sociálne prihlásenie',
     'social_registration' => 'Sociálna registrácia',
-    'social_registration_text' => 'Registrovať sa a prihlásiť sa použitím inej služby.',
+    'social_registration_text' => 'Registrácia a prihlásenie pomocou inej služby.',
 
-    'register_thanks' => 'Ďakujeme zaregistráciu!',
-    'register_confirm' => 'Skontrolujte prosím svoj email a kliknite na potvrdzujúce tlačidlo pre prístup k :appName.',
+    'register_thanks' => 'Ďakujeme za registráciu!',
+    'register_confirm' => 'Prosím, skontrolujte svoj e-mail a kliknite na potvrdzujúce tlačidlo pre prístup k :appName.',
     'registrations_disabled' => 'Registrácie sú momentálne zablokované',
-    'registration_email_domain_invalid' => 'Táto emailová doména nemá prístup k tejto aplikácii',
+    'registration_email_domain_invalid' => 'Táto e-mailová doména nemá prístup k tejto aplikácii',
     'register_success' => 'Ďakujeme za registráciu! Teraz ste registrovaný a prihlásený.',
 
 
     // Password Reset
-    'reset_password' => 'Reset hesla',
-    'reset_password_send_instructions' => 'Zadajte svoj email nižšie a bude Vám odoslaný email s odkazom pre reset hesla.',
-    'reset_password_send_button' => 'Poslať odkaz na reset hesla',
-    'reset_password_sent' => 'Odkaz na obnovenie hesla bude odoslaný na :email, ak sa táto e-mailová adresa nachádza v systéme.',
+    'reset_password' => 'Resetovanie hesla',
+    'reset_password_send_instructions' => 'Nižšie zadajte svoj e-mail, na ktorý Vám zašleme odkaz pre resetovanie hesla.',
+    'reset_password_send_button' => 'Poslať odkaz na resetovanie hesla',
+    'reset_password_sent' => 'Odkaz na resetovanie hesla bude odoslaný na :email, ak sa táto e-mailová adresa nachádza v systéme.',
     'reset_password_success' => 'Vaše heslo bolo úspešne resetované.',
-    'email_reset_subject' => 'Reset Vášho :appName hesla',
-    'email_reset_text' => 'Tento email Ste dostali pretože sme dostali požiadavku na reset hesla pre Váš účet.',
-    'email_reset_not_requested' => 'Ak ste nepožiadali o reset hesla, nemusíte nič robiť.',
+    'email_reset_subject' => 'Resetovanie Vášho hesla do :appName',
+    'email_reset_text' => 'Tento e-mail ste obdržali, pretože sme dostali požiadavku na resetovanie hesla pre Váš účet.',
+    'email_reset_not_requested' => 'Ak ste nepožiadali o resetovanie hesla, nemusíte robiť nič.',
 
 
     // Email Confirmation
-    'email_confirm_subject' => 'Potvrdiť email na :appName',
-    'email_confirm_greeting' => 'Ďakujeme za pridanie sa k :appName!',
-    'email_confirm_text' => 'Prosím potvrďte Vašu emailovú adresu kliknutím na tlačidlo nižšie:',
-    'email_confirm_action' => 'Potvrdiť email',
-    'email_confirm_send_error' => 'Je požadované overenie emailu, ale systém nemohol odoslať email. Kontaktujte administrátora by ste sa uistili, že email je nastavený správne.',
+    'email_confirm_subject' => 'Potvrdiť e-mail na :appName',
+    'email_confirm_greeting' => 'Ďakujeme, že ste sa pridali k :appName!',
+    'email_confirm_text' => 'Prosím, potvrďte Vašu e-mailovú adresu kliknutím na tlačidlo nižšie:',
+    'email_confirm_action' => 'Potvrdiť e-mail',
+    'email_confirm_send_error' => 'Je požadované overenie e-mailu, ale systém nemohol e-mail odoslať. Kontaktujte administrátora, aby ste sa uistili, že je e-mail nastavený správne.',
     'email_confirm_success' => 'Váš email bol overený!',
-    'email_confirm_resent' => 'Potvrdzujúci email bol poslaný znovu, skontrolujte prosím svoju emailovú schránku.',
+    'email_confirm_resent' => 'Potvrdzujúci e-mail bol poslaný znovu, skontrolujte prosím svoju e-mailovú schránku.',
 
-    'email_not_confirmed' => 'Emailová adresa nebola overená',
-    'email_not_confirmed_text' => 'Vaša emailová adresa nebola zatiaľ overená.',
-    'email_not_confirmed_click_link' => 'Prosím, kliknite na odkaz v emaili, ktorý bol poslaný krátko po Vašej registrácii.',
-    'email_not_confirmed_resend' => 'Ak nemôžete násť email, môžete znova odoslať overovací email odoslaním doleuvedeného formulára.',
-    'email_not_confirmed_resend_button' => 'Znova odoslať overovací email',
+    'email_not_confirmed' => 'E-mailová adresa nebola overená',
+    'email_not_confirmed_text' => 'Vaša e-mailová adresa nebola zatiaľ overená.',
+    'email_not_confirmed_click_link' => 'Prosím, kliknite na odkaz v e-maili, ktorý bol poslaný krátko po Vašej registrácii.',
+    'email_not_confirmed_resend' => 'Ak nemôžete nájsť e-mail, môžete znova odoslať overovací e-mail odoslaním doleuvedeného formulára.',
+    'email_not_confirmed_resend_button' => 'Znova odoslať overovací e-mail',
 
     // User Invite
     'user_invite_email_subject' => 'Dostali ste pozvánku na pripojenie sa k aplikácii :appName!',
     'user_invite_email_greeting' => 'Účet pre :appName bol pre vás vytvorený.',
     'user_invite_email_text' => 'Kliknutím na tlačidlo nižšie nastavíte heslo k účtu a získate prístup:',
-    'user_invite_email_action' => 'Heslo k účtu',
-    'user_invite_page_welcome' => 'Vitajte na stránke :appName!',
+    'user_invite_email_action' => 'Nastaviť heslo k účtu',
+    'user_invite_page_welcome' => 'Vitajte v :appName!',
     'user_invite_page_text' => 'Ak chcete dokončiť svoj účet a získať prístup, musíte nastaviť heslo, ktoré sa použije na prihlásenie do aplikácie :appName pri budúcich návštevách.',
     'user_invite_page_confirm_button' => 'Potvrdiť heslo',
-    'user_invite_success' => 'Nastavené heslo, teraz máte prístup k :appName!'
+    'user_invite_success' => 'Heslo bolo nastavené, teraz máte prístup k :appName!'
 ];
\ No newline at end of file
index 1e6bf4972b4ad41fa29c2acbcef22c09d595d2d6..4b026c506c5a0a0f8c84e649feea1f2ea5968118 100644 (file)
@@ -22,6 +22,7 @@ return [
     'meta_created_name' => 'Vytvorené :timeLength používateľom :user',
     'meta_updated' => 'Aktualizované :timeLength',
     'meta_updated_name' => 'Aktualizované :timeLength používateľom :user',
+    'meta_owned_name' => 'Owned by :user',
     'entity_select' => 'Entita vybraná',
     'images' => 'Obrázky',
     'my_recent_drafts' => 'Moje nedávne koncepty',
@@ -39,6 +40,7 @@ return [
     'permissions_intro' => 'Ak budú tieto oprávnenia povolené, budú mať prioritu pred oprávneniami roly.',
     'permissions_enable' => 'Povoliť vlastné oprávnenia',
     'permissions_save' => 'Uložiť oprávnenia',
+    'permissions_owner' => 'Owner',
 
     // Search
     'search_results' => 'Výsledky hľadania',
@@ -146,8 +148,7 @@ return [
     'chapters_create' => 'Vytvoriť novú kapitolu',
     'chapters_delete' => 'Zmazať kapitolu',
     'chapters_delete_named' => 'Zmazať kapitolu :chapterName',
-    'chapters_delete_explain' => 'Toto zmaže kapitolu menom \':chapterName\', všetky stránky budú ostránené
-        a pridané priamo do rodičovskej knihy.',
+    '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' => 'Ste si istý, že chcete zmazať túto kapitolu?',
     'chapters_edit' => 'Upraviť kapitolu',
     'chapters_edit_named' => 'Upraviť kapitolu :chapterName',
@@ -209,6 +210,7 @@ return [
     'pages_revisions' => 'Revízie stránky',
     'pages_revisions_named' => 'Revízie stránky :pageName',
     'pages_revision_named' => 'Revízia stránky :pageName',
+    'pages_revision_restored_from' => 'Restored from #:id; :summary',
     'pages_revisions_created_by' => 'Vytvoril',
     'pages_revisions_date' => 'Dátum revízie',
     'pages_revisions_number' => '#',
index 8b16157982d9ba60a609085d6ac41840d80c42d4..17e658a3fbe6413e82236840da0e2f5d6d024bab 100644 (file)
@@ -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' => 'Profil používateľa',
     'users_add_new' => 'Pridať nového používateľa',
     'users_search' => 'Hľadať medzi používateľmi',
+    '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' => 'Zmazať používateľa :userName',
     'users_delete_warning' => ' Toto úplne odstráni používateľa menom \':userName\' zo systému.',
     'users_delete_confirm' => 'Ste si istý, že chcete zmazať tohoto používateľa?',
-    'users_delete_success' => 'Používateľ úspešne zmazaný',
+    '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' => 'Upraviť používateľa',
     'users_edit_profile' => 'Upraviť profil',
     'users_edit_success' => 'Používateľ úspešne upravený',
@@ -214,6 +239,7 @@ return [
         'ja' => '日本語',
         'ko' => '한국어',
         'nl' => 'Nederlands',
+        'nb' => 'Norsk (Bokmål)',
         'pl' => 'Polski',
         'pt_BR' => 'Português do Brasil',
         'ru' => 'Русский',
index 8dc26905dc4b5ee8c606385cc40dd876a7ceb992..c127b0623cc24158c006b7219ccf5f97546774c8 100644 (file)
@@ -90,6 +90,7 @@ return [
     'required_without'     => 'Políčko :attribute je povinné aj :values neexistuje.',
     'required_without_all' => 'Políčko :attribute je povinné ak ani jedno z :values neexistuje.',
     'same'                 => ':attribute a :other musia byť rovnaké.',
+    'safe_url'             => 'The provided link may not be safe.',
     'size'                 => [
         'numeric' => ':attribute musí byť :size.',
         'file'    => ':attribute musí mať :size kilobajtov.',
index 01057f7415b17f7c940c8b98de9b82fcaa31c258..4ed1185def5c45acebd58adb81c9655d02eda531 100644 (file)
@@ -7,42 +7,43 @@ return [
 
     // Pages
     'page_create'                 => 'ustvarjena stran',
-    'page_create_notification'    => 'Zapis uspešno ustvarjen',
-    'page_update'                 => 'nadgrajena stran',
-    'page_update_notification'    => 'Uspešno posodobljeno',
+    'page_create_notification'    => 'Stran uspešno ustvarjena',
+    'page_update'                 => 'posodobljena stran',
+    'page_update_notification'    => 'Stran uspešno posodobljena',
     'page_delete'                 => 'izbrisana stran',
-    'page_delete_notification'    => 'Uspešno izbrisano',
+    'page_delete_notification'    => 'Stran uspešno izbrisana',
     'page_restore'                => 'obnovljena stran',
-    'page_restore_notification'   => 'Uspešna obnovitev',
+    'page_restore_notification'   => 'Stran uspešno obnovljena',
     'page_move'                   => 'premaknjena stran',
 
     // Chapters
     'chapter_create'              => 'ustvarjeno poglavje',
-    'chapter_create_notification' => 'Zapis uspešno ustvarjen',
-    'chapter_update'              => 'nadgradi poglavje',
-    'chapter_update_notification' => 'Uspešno posodobljeno',
+    'chapter_create_notification' => 'Poglavje uspešno ustvarjeno',
+    'chapter_update'              => 'posodobljeno poglavje',
+    'chapter_update_notification' => 'Poglavje uspešno posodobljeno',
     'chapter_delete'              => 'izbrisano poglavje',
-    'chapter_delete_notification' => 'Uspešno izbrisano',
+    'chapter_delete_notification' => 'Poglavje uspešno izbrisano',
     'chapter_move'                => 'premaknjeno poglavje',
 
     // Books
     'book_create'                 => 'knjiga ustvarjena',
-    'book_create_notification'    => 'Knjiga Uspešno Usvarjena',
+    'book_create_notification'    => 'Knjiga uspešno usvarjena',
     'book_update'                 => 'knjiga posodobljena',
-    'book_update_notification'    => 'Uspešno posodobljeno',
+    'book_update_notification'    => 'Knjiga uspešno posodobljena',
     'book_delete'                 => 'izbrisana knjiga',
-    'book_delete_notification'    => 'Uspešno izbrisano',
+    'book_delete_notification'    => 'Knjiga uspešno izbrisana',
     'book_sort'                   => 'razvrščena knjiga',
-    'book_sort_notification'      => 'Knjiga Uspešno Razvrščena',
+    'book_sort_notification'      => 'Knjiga uspešno razvrščena',
 
     // Bookshelves
     'bookshelf_create'            => 'knjižna polica izdelana',
-    'bookshelf_create_notification'    => 'Knjižna Polica Izdelana',
+    'bookshelf_create_notification'    => 'Knjižna polica uspešno ustvarjena',
     'bookshelf_update'                 => 'knjižna polica posodobljena',
-    'bookshelf_update_notification'    => 'Knjižna Polica Uspešno Posodobljena',
+    'bookshelf_update_notification'    => 'Knjižna polica uspešno posodobljena',
     'bookshelf_delete'                 => 'knjižna polica izbrisana',
-    'bookshelf_delete_notification'    => 'Knjižna Polica Uspešno Izbrisana',
+    'bookshelf_delete_notification'    => 'Knjižna polica uspešno Izbrisana',
 
     // Other
     'commented_on'                => 'komentar na',
+    'permissions_update'          => 'pravice so posodobljene',
 ];
index 32aa9596e2b54a28bb1d0f725768df3250cf1d23..df6fb4227dc665ce4edbc3866217f8b38a432d37 100644 (file)
@@ -43,7 +43,7 @@ return [
     'reset_password' => 'Ponastavi geslo',
     'reset_password_send_instructions' => 'Spodaj vpišite vaš e-poštni naslov in prejeli boste e-pošto s povezavo za ponastavitev gesla.',
     'reset_password_send_button' => 'Pošlji povezavo za ponastavitev',
-    'reset_password_sent' => 'A password reset link will be sent to :email if that email address is found in the system.',
+    'reset_password_sent' => 'V kolikor e-poštni naslov :email obstaja v sistemu, bo nanj poslana povezava za ponastavitev gesla.',
     'reset_password_success' => 'Vaše geslo je bilo uspešno spremenjeno.',
     'email_reset_subject' => 'Ponastavi svoje :appName geslo',
     'email_reset_text' => 'To e-poštno sporočilo ste prejeli, ker smo prejeli zahtevo za ponastavitev gesla za vaš račun.',
index 527ecace5457215c72d13fb73070a238dc902409..65a42288c4c478520b4d8fb3bdb12e441e143518 100644 (file)
@@ -9,17 +9,17 @@ return [
     'confirm' => 'Potrdi',
     'back' => 'Nazaj',
     'save' => 'Shrani',
-    'continue' => 'Naprej',
+    'continue' => 'Nadaljuj',
     'select' => 'Izberi',
     'toggle_all' => 'Vklopi vse',
     'more' => 'Več',
 
     // Form Labels
-    'name' => 'Ime',
+    'name' => 'Naziv',
     'description' => 'Opis',
     'role' => 'Vloga',
     'cover_image' => 'Naslovna slika',
-    'cover_image_description' => 'Slika naj bo okoli 440x250px velika.',
+    'cover_image_description' => 'Slika naj bo velika približno 440x250px.',
     
     // Actions
     'actions' => 'Dejanja',
@@ -33,11 +33,11 @@ return [
     'copy' => 'Kopiraj',
     'reply' => 'Odgovori',
     'delete' => 'Izbriši',
-    'delete_confirm' => 'Confirm Deletion',
+    'delete_confirm' => 'Potrdi brisanje',
     'search' => 'Išči',
-    'search_clear' => 'Počisti iskanje',
+    'search_clear' => 'Razveljavi iskanje',
     'reset' => 'Ponastavi',
-    'remove' => 'Remove',
+    'remove' => 'Odstrani',
     'add' => 'Dodaj',
     'fullscreen' => 'Celozaslonski način',
 
@@ -53,7 +53,7 @@ return [
     // Misc
     'deleted_user' => 'Izbrisan uporabnik',
     'no_activity' => 'Ni aktivnosti za prikaz',
-    'no_items' => 'Ni na voljo nobenih elementov',
+    'no_items' => 'Na voljo ni nobenega elementa',
     'back_to_top' => 'Nazaj na vrh',
     'toggle_details' => 'Preklopi podrobnosti',
     'toggle_thumbnails' => 'Preklopi sličice',
@@ -67,14 +67,14 @@ return [
     'profile_menu' => 'Meni profila',
     'view_profile' => 'Ogled profila',
     'edit_profile' => 'Uredi profil',
-    'dark_mode' => 'Dark Mode',
-    'light_mode' => 'Light Mode',
+    'dark_mode' => 'Način temnega zaslona',
+    'light_mode' => 'Način svetlega zaslona',
 
     // Layout tabs
     'tab_info' => 'Informacije',
     'tab_content' => 'Vsebina',
 
     // Email Content
-    'email_action_help' => 'Če imate težave s klikom na ":actionText" gumb, kopirajte im prilepite spodnjo povezavo v vaš brskalnik:',
+    'email_action_help' => 'V kolikor imate težave s klikom na gumb ":actionText", kopirajte in prilepite spodnjo povezavo v vaš brskalnik:',
     'email_rights' => 'Vse pravice pridržane',
 ];
index 658859e7ef97b4ae104bb4eba8798ae3fa8884b3..d1b454e6568b1531e3b34bdeb1c9d61769e5bb4d 100644 (file)
@@ -8,27 +8,27 @@ return [
     'image_select' => 'Izberi slike',
     'image_all' => 'Vse',
     'image_all_title' => 'Prikaži vse slike',
-    'image_book_title' => 'Preglej slike naložene v to knjigo',
+    'image_book_title' => 'Prikaži slike naložene v to knjigo',
     'image_page_title' => 'Preglej slike naložene na to stran',
     'image_search_hint' => 'Iskanje po nazivu slike',
     'image_uploaded' => 'Naloženo :uploadedDate',
-    'image_load_more' => 'Naloži več',
+    'image_load_more' => 'Dodatno naloži',
     'image_image_name' => 'Ime slike',
     'image_delete_used' => 'Ta slika je uporabljena na spodnjih straneh.',
-    'image_delete_confirm_text' => 'Are you sure you want to delete this image?',
+    'image_delete_confirm_text' => 'Ste prepričani, da želite izbrisati to sliko?',
     'image_select_image' => 'Izberite sliko',
     'image_dropzone' => 'Povlecite slike ali kliknite tukaj za nalaganje',
     'images_deleted' => 'Slike so bile izbrisane',
     'image_preview' => 'Predogled slike',
     'image_upload_success' => 'Slika uspešno naložena',
     'image_update_success' => 'Podatki slike uspešno posodobljeni',
-    'image_delete_success' => 'Uspešno izbrisano',
+    'image_delete_success' => 'Slika uspešno izbrisana',
     'image_upload_remove' => 'Odstrani',
 
     // Code Editor
     'code_editor' => 'Uredi kodo',
     'code_language' => 'Koda jezika',
     'code_content' => 'Koda vsebine',
-    'code_session_history' => 'Session History',
+    'code_session_history' => 'Zgodovina seje',
     'code_save' => 'Shrani kodo',
 ];
index 40d891f878b74c1b340f546cc073313863bbef8d..9e400bbec584e63ad927937591dc0732455d40f4 100644 (file)
@@ -8,20 +8,21 @@ return [
     // Shared
     'recently_created' => 'Nazadnje objavljeno',
     'recently_created_pages' => 'Nazadnje objavljene strani',
-    'recently_updated_pages' => 'Nedavno posodobljene strani',
+    'recently_updated_pages' => 'Nazadnje posodobljene strani',
     'recently_created_chapters' => 'Nazadnje objavljena poglavja',
     'recently_created_books' => 'Nazadnje objavljene knjige',
-    'recently_created_shelves' => 'Nazadnje objavljene police',
-    'recently_update' => 'Nedavno posodobljeno',
-    'recently_viewed' => 'Nedavno prikazano',
+    'recently_created_shelves' => 'Nazadnje ustvarjene police',
+    'recently_update' => 'Nazadnje posodobljeno',
+    'recently_viewed' => 'Nazadnje prikazano',
     'recent_activity' => 'Nedavna dejavnost',
     'create_now' => 'Ustvarite eno sedaj',
     'revisions' => 'Revizije',
-    'meta_revision' => 'Revizije #:revisionCount',
+    'meta_revision' => 'Številka revizije #:revisionCount',
     'meta_created' => 'Ustvarjeno :timeLength',
-    'meta_created_name' => 'Created :timeLength by :user',
+    'meta_created_name' => 'Ustvaril :timeLength uporabnik :user',
     'meta_updated' => 'Posodobljeno :timeLength',
-    'meta_updated_name' => 'Posodobljeno :timeLength by :user',
+    'meta_updated_name' => 'Posodobil :timeLength uporabnik :user',
+    'meta_owned_name' => 'Owned by :user',
     'entity_select' => 'Izbira entitete',
     'images' => 'Slike',
     'my_recent_drafts' => 'Moji nedavni osnutki',
@@ -31,14 +32,15 @@ return [
     'no_pages_recently_updated' => 'Nedavno ni bila posodobljena nobena stran',
     'export' => 'Izvozi',
     'export_html' => 'Vsebuje spletno datoteko',
-    'export_pdf' => 'Datoteka PDF',
+    'export_pdf' => 'PDF datoteka (.pdf)',
     'export_text' => 'Navadna besedilna datoteka',
 
     // Permissions and restrictions
     'permissions' => 'Dovoljenja',
-    'permissions_intro' => 'Ko so enkrat omogočena, bodo ta dovoljenja imela prednost pred dovoljenji za določanje vlog.',
+    'permissions_intro' => 'V trenutku, ko bodo omogočena, bodo imela ta dovoljenja prednost pred dovoljenji za določanje vlog.',
     'permissions_enable' => 'Omogoči dovoljenja po meri',
     'permissions_save' => 'Shrani dovoljenja',
+    'permissions_owner' => 'Owner',
 
     // Search
     'search_results' => 'Rezultati iskanja',
@@ -47,11 +49,11 @@ return [
     'search_no_pages' => 'Nobena stran se ne ujema z vašim iskanjem',
     'search_for_term' => 'Išči :term',
     'search_more' => 'Prikaži več rezultatov',
-    'search_advanced' => 'Advanced Search',
-    'search_terms' => 'Search Terms',
+    'search_advanced' => 'Napredno iskanje',
+    'search_terms' => 'Iskalni izrazi',
     'search_content_type' => 'Vrsta vsebine',
     'search_exact_matches' => 'Natančno ujemanje',
-    'search_tags' => 'Iskanje oznak',
+    'search_tags' => 'Iskanje po oznakah',
     'search_options' => 'Možnosti',
     'search_viewed_by_me' => 'Ogledano',
     'search_not_viewed_by_me' => 'Neogledano',
@@ -72,30 +74,30 @@ return [
     'x_shelves' => ':count Polica|:count Police',
     'shelves_long' => 'Knjižne police',
     'shelves_empty' => 'Ustvarjena ni bila nobena polica',
-    'shelves_create' => 'Izdelaj novo polico',
+    'shelves_create' => 'Ustvari novo polico',
     'shelves_popular' => 'Priljubljene police',
     'shelves_new' => 'Nove police',
     'shelves_new_action' => 'Nova polica',
     'shelves_popular_empty' => 'Najbolj priljubljene police se bodo pojavile tukaj.',
-    'shelves_new_empty' => 'Zadnje ustvarjene police se bodo pojavile tukaj.',
+    'shelves_new_empty' => 'Nazadnje ustvarjene police se bodo pojavile tukaj.',
     'shelves_save' => 'Shrani polico',
     'shelves_books' => 'Knjige na tej polici',
     'shelves_add_books' => 'Dodaj knjige na to polico',
-    'shelves_drag_books' => 'Povleci knjige sem za jih dodati na to polico',
-    'shelves_empty_contents' => 'Ta polica nima dodeljenih knjig',
-    'shelves_edit_and_assign' => 'Uredi polico za dodajanje knjig',
+    'shelves_drag_books' => 'Povlecite knjige sem, da jih dodate na to polico',
+    'shelves_empty_contents' => 'Na tej polici ni nobene knjige',
+    'shelves_edit_and_assign' => 'Uredi knjižno polico za dodajanje knjig',
     'shelves_edit_named' => 'Uredi knjižno polico :name',
     'shelves_edit' => 'Uredi knjižno polico',
     'shelves_delete' => 'Izbriši knjižno polico',
     'shelves_delete_named' => 'Izbriši knjižno polico :name',
-    'shelves_delete_explain' => "To bo izbrisalo knjižno polico z imenom ':name'. Vsebovane knjige ne bodo izbrisane.",
+    'shelves_delete_explain' => "S tem boste izbrisali knjižno polico z nazivom ':name'. Vsebovane knjige ne bodo izbrisane.",
     'shelves_delete_confirmation' => 'Ali ste prepričani, da želite izbrisati ta knjižno polico?',
     'shelves_permissions' => 'Dovoljenja knjižnih polic',
     'shelves_permissions_updated' => 'Posodobljena dovoljenja knjižnih polic',
     'shelves_permissions_active' => 'Aktivna dovoljenja knjižnih polic',
     'shelves_copy_permissions_to_books' => 'Kopiraj dovoljenja na knjige',
-    'shelves_copy_permissions' => 'Kopiraj dovoljenja',
-    'shelves_copy_permissions_explain' => 'To bo uporabilo trenutne nastavitve dovoljenj te knjižne police za vse knjige, ki jih vsebuje. Pred aktiviranjem zagotovite, da so shranjene vse spremembe dovoljenj te knjižne police.',
+    'shelves_copy_permissions' => 'Dovoljenja kopiranja',
+    'shelves_copy_permissions_explain' => 'To bo uveljavilo trenutne nastavitve dovoljenj na knjižni polici za vse knjige, ki jih vsebuje ta polica. Pred aktiviranjem zagotovite, da so shranjene vse spremembe dovoljenj te knjižne police.',
     'shelves_copy_permission_success' => 'Dovoljenja knjižne police kopirana na :count knjig',
 
     // Books
@@ -107,12 +109,12 @@ return [
     'books_recent' => 'Zadnje knjige',
     'books_new' => 'Nove knjige',
     'books_new_action' => 'Nova knjiga',
-    'books_popular_empty' => 'Najbolj priljubljene knjige se bodo pojavile tukaj.',
-    'books_new_empty' => 'Zadnje ustvarjene knjige se bodo pojavile tukaj.',
-    'books_create' => 'Izdelaj novo knjigo',
+    'books_popular_empty' => 'Tukaj bodo prikazane najbolj priljubljene knjige.',
+    'books_new_empty' => 'Tukaj bodo prikazane nazadnje ustvarjene knjige.',
+    'books_create' => 'Ustvari novo knjigo',
     'books_delete' => 'Izbriši knjigo',
     'books_delete_named' => 'Izbriši knjigo :bookName',
-    'books_delete_explain' => 'To bo izbrisalo knjigo z imenom \':bookName\'. Vse strani in poglavja bodo odstranjena.',
+    'books_delete_explain' => 'S tem boste izbrisali knjigo z nazivom \':bookName\'. Vse strani in poglavja bodo odstranjena.',
     'books_delete_confirmation' => 'Ali ste prepričani, da želite izbrisati to knjigo?',
     'books_edit' => 'Uredi knjigo',
     'books_edit_named' => 'Uredi knjigo :bookName',
@@ -120,14 +122,14 @@ return [
     'books_save' => 'Shrani knjigo',
     'books_permissions' => 'Dovoljenja knjige',
     'books_permissions_updated' => 'Posodobljena dovoljenja knjige',
-    'books_empty_contents' => 'Nobena stran ali poglavje ni bilo ustvarjeno za to knjigo.',
+    'books_empty_contents' => 'V tej knjigi ni bila ustvarjena še nobena stran ali poglavje.',
     'books_empty_create_page' => 'Ustvari novo stran',
     'books_empty_sort_current_book' => 'Razvrsti trenutno knjigo',
     'books_empty_add_chapter' => 'Dodaj poglavje',
     'books_permissions_active' => 'Aktivna dovoljenja knjige',
-    'books_search_this' => 'Išči to knjigo',
-    'books_navigation' => 'Navigacija knjige',
-    'books_sort' => 'Razvrsti vsebine knjige',
+    'books_search_this' => 'Išči v tej knjigi',
+    'books_navigation' => 'Navigacija po knjigi',
+    'books_sort' => 'Razvrsti vsebino knjige',
     'books_sort_named' => 'Razvrsti knjigo :bookName',
     'books_sort_name' => 'Razvrsti po imenu',
     'books_sort_created' => 'Razvrsti po datumu nastanka',
@@ -135,7 +137,7 @@ return [
     'books_sort_chapters_first' => 'Najprej poglavja',
     'books_sort_chapters_last' => 'Nazadnje poglavja',
     'books_sort_show_other' => 'Prikaži druge knjige',
-    'books_sort_save' => 'Shrani novo naročilo',
+    'books_sort_save' => 'Shrani novo razvrstitev',
 
     // Chapters
     'chapter' => 'Poglavje',
@@ -146,8 +148,8 @@ return [
     'chapters_create' => 'Ustvari novo poglavje',
     'chapters_delete' => 'Izbriši poglavje',
     'chapters_delete_named' => 'Izbriši poglavje :chapterName',
-    'chapters_delete_explain' => 'To bo izbrisalo poglavje z \':chapterName\'. Vse strani bodo odstranjene in dodane neposredno v matično knjigo.',
-    'chapters_delete_confirm' => 'Si prepričan, da želiš izbrisati to poglavje?',
+    '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' => 'Ste prepričani, da želite izbrisati to poglavje?',
     'chapters_edit' => 'Uredi poglavje',
     'chapters_edit_named' => 'Uredi poglavje :chapterName',
     'chapters_save' => 'Shrani poglavje',
@@ -156,7 +158,7 @@ return [
     'chapter_move_success' => 'Poglavje premaknjeno v :bookName',
     'chapters_permissions' => 'Dovoljenja poglavij',
     'chapters_empty' => 'V tem poglavju trenutno ni strani.',
-    'chapters_permissions_active' => 'Aktivna dovoljenja poglavij',
+    'chapters_permissions_active' => 'Dovoljenja poglavij so aktivirana',
     'chapters_permissions_success' => 'Posodobljena dovoljenja poglavij',
     'chapters_search_this' => 'Išči v tem poglavju',
 
@@ -166,7 +168,7 @@ return [
     'x_pages' => ':count Stran|:count Strani',
     'pages_popular' => 'Priljubjene strani',
     'pages_new' => 'Nova stran',
-    'pages_attachments' => 'Priloge',
+    'pages_attachments' => 'Priponke',
     'pages_navigation' => 'Navigacija po strani',
     'pages_delete' => 'Izbriši stran',
     'pages_delete_named' => 'Izbriši stran :pageName',
@@ -185,16 +187,16 @@ return [
     'pages_edit_draft_save_at' => 'Osnutek shranjen ob ',
     'pages_edit_delete_draft' => 'Izbriši osnutek',
     'pages_edit_discard_draft' => 'Zavrzi osnutek',
-    'pages_edit_set_changelog' => 'Nastavi zgodovino sprememb',
+    'pages_edit_set_changelog' => 'Opiši spremembe na dokumentu',
     'pages_edit_enter_changelog_desc' => 'Vnesite kratek opis sprememb, ki ste jih naredili',
-    'pages_edit_enter_changelog' => 'Vnesite zgodovino sprememb',
+    'pages_edit_enter_changelog' => 'Vpišite vsebino sprememb',
     'pages_save' => 'Shrani stran',
     'pages_title' => 'Naslov strani',
     'pages_name' => 'Ime strani',
     'pages_md_editor' => 'Urejevalnik',
     'pages_md_preview' => 'Predogled',
     'pages_md_insert_image' => 'Vstavi sliko',
-    'pages_md_insert_link' => 'Vnesi povezavo entitete',
+    'pages_md_insert_link' => 'Vnesi povezavo do objekta',
     'pages_md_insert_drawing' => 'Vstavi risbo',
     'pages_not_in_chapter' => 'Stran ni v poglavju',
     'pages_move' => 'Premakni stran',
@@ -208,28 +210,29 @@ return [
     'pages_revisions' => 'Pregled strani',
     'pages_revisions_named' => 'Pregledi strani za :pageName',
     'pages_revision_named' => 'Pregled strani za :pageName',
+    'pages_revision_restored_from' => 'Restored from #:id; :summary',
     'pages_revisions_created_by' => 'Ustvaril',
     'pages_revisions_date' => 'Datum revizije',
     'pages_revisions_number' => '#',
-    'pages_revisions_numbered' => 'Revizija #:id',
-    'pages_revisions_numbered_changes' => 'Revizija #:id Changes',
+    'pages_revisions_numbered' => 'Revizija št. :id',
+    'pages_revisions_numbered_changes' => 'Revizija št. #:id Changes',
     'pages_revisions_changelog' => 'Dnevnik sprememb',
     'pages_revisions_changes' => 'Spremembe',
     'pages_revisions_current' => 'Trenutna različica',
     'pages_revisions_preview' => 'Predogled',
     'pages_revisions_restore' => 'Obnovi',
-    'pages_revisions_none' => 'Ta stran nima revizije',
+    'pages_revisions_none' => 'Ta stran nima popravkov',
     'pages_copy_link' => 'Kopiraj povezavo',
     'pages_edit_content_link' => 'Uredi vsebino',
     'pages_permissions_active' => 'Aktivna dovoljenja strani',
     'pages_initial_revision' => 'Prvotno objavljeno',
     'pages_initial_name' => 'Nova stran',
-    'pages_editing_draft_notification' => 'Trenutno urejate osnutek ku je bil nazadnje shranjen :timeDiff.',
-    'pages_draft_edited_notification' => 'Ta stran je od takrat posodobljena. Priporočamo, da zavržete ta osnutek.',
+    'pages_editing_draft_notification' => 'Trenutno urejate osnutek, ki je bil nazadnje shranjen :timeDiff.',
+    'pages_draft_edited_notification' => 'Ta stran je odtlej posodobljena. Priporočamo, da zavržete ta osnutek.',
     'pages_draft_edit_active' => [
         'start_a' => ':count uporabnikov je začelo urejati to stran',
         'start_b' => ':userName je začel urejati to stran',
-        'time_a' => 'od kar je bila stran nazandnje posodobljena',
+        'time_a' => 'odkar je bila stran nazandnje posodobljena',
         'time_b' => 'v zadnjih :minCount minutah',
         'message' => ':start :time. Pazite, da ne boste prepisali posodobitev drug drugega!',
     ],
@@ -246,48 +249,48 @@ return [
     'tags' =>  'Oznake',
     'tag_name' =>  'Ime oznake',
     'tag_value' => 'Vrednost oznake (opcijsko)',
-    'tags_explain' => "Dodajte nekaj oznak za boljšo kategorizacijo vaše vsebine.\nDodelite lahko vrednost oznake za boljšo poglobljeno organizacijo.",
+    'tags_explain' => "Dodajte nekaj oznak za boljšo kategorizacijo vaše vsebine.\nZ dodelitvijo oznake lahko poskrbite za bolj poglobljeno organizacijo.",
     'tags_add' => 'Dodaj drugo oznako',
     'tags_remove' => 'Odstrani to oznako',
-    'attachments' => 'Priloge',
-    'attachments_explain' => 'Naložite nekaj datotek ali pripnite nekaj povezav, da bo prikazano na vaši strani. Te so vidne v stranski vrstici strani.',
+    'attachments' => 'Priponke',
+    'attachments_explain' => 'Naložite nekaj datotek ali pripnite nekaj povezav, da jih prikažete na vaši strani. Vidne so v stranski orodni vrstici.',
     'attachments_explain_instant_save' => 'Spremembe tukaj so takoj shranjene.',
-    'attachments_items' => 'Priloženi element',
+    'attachments_items' => 'Priloženi elementi',
     'attachments_upload' => 'Naloži datoteko',
     'attachments_link' => 'Pripni povezavo',
     'attachments_set_link' => 'Nastavi povezavo',
-    'attachments_delete' => 'Are you sure you want to delete this attachment?',
+    'attachments_delete' => 'Ali ste prepričani, da želite izbrisati to priponko?',
     'attachments_dropzone' => 'Spustite datoteke ali kliknite tukaj, če želite priložiti datoteko',
     'attachments_no_files' => 'Nobena datoteka ni bila naložena',
     'attachments_explain_link' => 'Lahko pripnete povezavo, če ne želite naložiti datoteke. Lahko je povezava na drugo stran ali povezava do dateteke v oblaku.',
     'attachments_link_name' => 'Ime povezave',
     'attachment_link' => 'Povezava priponke',
     'attachments_link_url' => 'Povezava do datoteke',
-    'attachments_link_url_hint' => 'Url mesta ali datoteke',
+    'attachments_link_url_hint' => 'Url spletnega mesta ali datoteke',
     'attach' => 'Pripni',
-    'attachments_insert_link' => 'Add Attachment Link to Page',
+    'attachments_insert_link' => 'Dodaj povezavo na priponko na stran',
     'attachments_edit_file' => 'Uredi datoteko',
     'attachments_edit_file_name' => 'Ime datoteke',
     'attachments_edit_drop_upload' => 'Spustite datoteke ali kliknite tukaj, če želite naložiti in prepisati',
-    'attachments_order_updated' => 'Priloga posodobljena',
+    'attachments_order_updated' => 'Razvrščanje priponk posodobljeno',
     'attachments_updated_success' => 'Podrobnosti priloge posodobljene',
-    'attachments_deleted' => 'Priloga izbirsana',
+    'attachments_deleted' => 'Priponka izbirsana',
     'attachments_file_uploaded' => 'Datoteka uspešno naložena',
     'attachments_file_updated' => 'Datoteka uspešno posodobljena',
     'attachments_link_attached' => 'Povezava uspešno dodana na stran',
     'templates' => 'Predloge',
     'templates_set_as_template' => 'Stran je predloga',
-    'templates_explain_set_as_template' => 'To stran lahko nastavite kot predlogo tako bo njena vsebina uporabljena pri izdelavi drugih strani. Ostali uporabniki bodo lahko uporabljali to predlogo, če imajo dovoljenja za to stran.',
+    'templates_explain_set_as_template' => 'To stran lahko nastavite kot predlogo in njeno vsebino uporabite pri izdelavi drugih strani. Ostali uporabniki bodo lahko uporabljali to predlogo, če imajo dovoljenja za to stran.',
     'templates_replace_content' => 'Zamenjaj vsebino strani',
-    'templates_append_content' => 'Dodajte vsebini strani',
-    'templates_prepend_content' => 'Dodaj k vsebini strani',
+    'templates_append_content' => 'Dodajte vsebini strani',
+    'templates_prepend_content' => 'Dodaj predpono k vsebini strani',
 
     // Profile View
     'profile_user_for_x' => 'Uporabnik že :time',
     'profile_created_content' => 'Ustvarjena vsebina',
     'profile_not_created_pages' => ':userName ni izdelal nobene strani',
     'profile_not_created_chapters' => ':userName ni izdelal nobenega poglavja',
-    'profile_not_created_books' => ':userName ni izdelal nobene knjige',
+    'profile_not_created_books' => ':userName ni objavil nobene knjige',
     'profile_not_created_shelves' => ':userName ni izdelal nobene knjižne police',
 
     // Comments
index e8ea99ab70e5aad12701c279e499d1610edcb06d..e291222c3f6be5f22cdbcbb61908c492d4619327 100644 (file)
@@ -12,16 +12,16 @@ return [
     'error_user_exists_different_creds' => 'Uporabnik z e-pošto :email že obstaja, vendar z drugačnimi poverilnicami.',
     'email_already_confirmed' => 'E-naslov je že bil potrjen, poskusite se prijaviti.',
     'email_confirmation_invalid' => 'Ta potrditveni žeton ni veljaven ali je že bil uporabljen. Poizkusite znova.',
-    'email_confirmation_expired' => 'Potrditveni žeton je pretečen. Nova potrditvena e-pošta je bila poslana.',
+    'email_confirmation_expired' => 'Potrditveni žeton je potekel. Nova potrditvena e-pošta je bila poslana.',
     'email_confirmation_awaiting' => 'Potrebno je potrditi e-naslov',
     'ldap_fail_anonymous' => 'Dostop do LDAP ni uspel z anonimno povezavo',
     'ldap_fail_authed' => 'Neuspešen LDAP dostop z danimi podrobnostimi dn & gesla',
-    'ldap_extension_not_installed' => 'PHP razširitev za LDAP ni nameščen',
+    'ldap_extension_not_installed' => 'PHP razširitev za LDAP ni nameščena',
     'ldap_cannot_connect' => 'Ne morem se povezati na LDAP strežnik, neuspešna začetna povezava',
     'saml_already_logged_in' => 'Že prijavljen',
     'saml_user_not_registered' => 'Uporabniško ime :name ni registrirano in avtomatska registracija je onemogočena',
     'saml_no_email_address' => 'Nisem našel e-naslova za tega uporabnika v podatkih iz zunanjega sistema za preverjanje pristnosti',
-    'saml_invalid_response_id' => 'Zahteva iz zunanjega sistema za preverjanje pristnosti ni prepoznana s strani procesa zagnanega s strani te aplikacije. Pomik nazaj po prijavi je lahko povzročil te težave.',
+    'saml_invalid_response_id' => 'Zahteva iz zunanjega sistema za preverjanje pristnosti ni prepoznana s strani procesa zagnanega s strani te aplikacije. Pomik nazaj po prijavi je lahko vzrok teh težav.',
     'saml_fail_authed' => 'Prijava z uporabo :system ni uspela, sistem ni zagotovil uspešne avtorizacije',
     'social_no_action_defined' => 'Akcija ni določena',
     'social_login_bad_response' => "Napaka pri :socialAccount prijavi:\n:error",
@@ -33,14 +33,14 @@ return [
     'social_account_register_instructions' => 'Če še nimate računa, se lahko registrirate z uporabo :socialAccount.',
     'social_driver_not_found' => 'Socialni vtičnik ni najden',
     'social_driver_not_configured' => 'Vaše nastavitve :socialAccount niso pravilo nastavljene.',
-    'invite_token_expired' => 'Ta link je pretečen. Namesto tega lahko ponastavite vaše geslo računa.',
+    'invite_token_expired' => 'Ta povezava je potekla. Namesto tega lahko ponastavite vaše geslo računa.',
 
     // System
-    'path_not_writable' => 'Poti :filePath ni bilo mogoče naložiti. Prepričajte se da je zapisljiva na strežnik.',
+    'path_not_writable' => 'Poti :filePath ni bilo mogoče naložiti. Prepričajte se, da je zapisljiva na strežnik.',
     'cannot_get_image_from_url' => 'Ne morem pridobiti slike z :url',
     'cannot_create_thumbs' => 'Strežnik ne more izdelati sličice. Prosimo preverite če imate GD PHP razširitev nameščeno.',
-    'server_upload_limit' => 'Strežnik ne dovoli nalaganj take velikosti. Prosimo poskusite manjšo velikost datoteke.',
-    'uploaded'  => 'Strežnik ne dovoli nalaganj take velikosti. Prosimo poskusite manjšo velikost datoteke.',
+    'server_upload_limit' => 'Strežnik ne dovoli nalaganj take velikosti. Prosimo poskusite z manjšo velikostjo datoteke.',
+    'uploaded'  => 'Strežnik ne dovoli nalaganj take velikosti. Prosimo poskusite zmanjšati velikost datoteke.',
     'image_upload_error' => 'Prišlo je do napake med nalaganjem slike',
     'image_upload_type_error' => 'Napačen tip (format) slike',
     'file_upload_timeout' => 'Čas nalaganjanja datoteke je potekel.',
@@ -50,10 +50,10 @@ return [
 
     // Pages
     'page_draft_autosave_fail' => 'Osnutka ni bilo mogoče shraniti. Pred shranjevanjem te strani se prepričajte, da imate internetno povezavo',
-    'page_custom_home_deletion' => 'Ne morem izbrisati strani dokler je nastavljena kot domača stran',
+    'page_custom_home_deletion' => 'Ne morem izbrisati strani, ki je nastavljena kot domača stran',
 
     // Entities
-    'entity_not_found' => 'Entiteta ni najdena',
+    'entity_not_found' => 'Ne najdem tega objekta',
     'bookshelf_not_found' => 'Knjižna polica ni najdena',
     'book_not_found' => 'Knjiga ni najdena',
     'page_not_found' => 'Stran ni najdena',
@@ -69,20 +69,20 @@ return [
     // Roles
     'role_cannot_be_edited' => 'Te vloge mi možno urejati',
     'role_system_cannot_be_deleted' => 'Ta vloga je sistemska in je ni možno brisati',
-    'role_registration_default_cannot_delete' => 'Te vloge ni možno brisati dokler je nastavljena kot privzeta',
-    'role_cannot_remove_only_admin' => 'Ta uporabnik je edini administrator. Dodelite vlogo administratorja drugemu uporabniku preden ga poskusite brisati.',
+    'role_registration_default_cannot_delete' => 'Te vloge ni možno brisati, dokler je nastavljena kot privzeta',
+    'role_cannot_remove_only_admin' => 'Ta uporabnik je edini administrator. Dodelite vlogo administratorja drugemu uporabniku, preden ga poskusite brisati.',
 
     // Comments
     'comment_list' => 'Napaka se je pojavila pri pridobivanju komentarjev.',
-    'cannot_add_comment_to_draft' => 'Ni mogoče dodajanje komentarjev v osnutek.',
-    'comment_add' => 'Napaka se je pojavila pri dodajanju / posodobitev komentarjev.',
+    'cannot_add_comment_to_draft' => 'V osnutek ni možno dodajati komentarjev.',
+    'comment_add' => 'Napaka se je pojavila pri dodajanju / posodobitvi komentarjev.',
     'comment_delete' => 'Napaka se je pojavila pri brisanju komentarja.',
     'empty_comment' => 'Praznega komentarja ne morete objaviti.',
 
     // Error pages
     '404_page_not_found' => 'Strani ni mogoče najti',
-    'sorry_page_not_found' => 'Oprostite, strani ki jo iščete ni mogoče najti.',
-    'sorry_page_not_found_permission_warning' => 'Če pričakujete, da ta stran obstaja, mogoče nimate pravic, da jo vidite.',
+    'sorry_page_not_found' => 'Oprostite, strani ki jo iščete, ni mogoče najti.',
+    'sorry_page_not_found_permission_warning' => 'Če pričakujete, da ta stran obstaja, mogoče nimate pravic ogleda zanjo.',
     'return_home' => 'Vrni se domov',
     'error_occurred' => 'Prišlo je do napake',
     'app_down' => ':appName trenutno ni dosegljiva',
index b7df08291306afcc822f99aadbd9cc5d66259b32..e04a98d89b5283f606d1575e9783918661f6d753 100644 (file)
@@ -6,7 +6,7 @@
  */
 return [
 
-    'previous' => '&laquo; Prejšnje',
+    'previous' => '&laquo; Predhodno',
     'next'     => 'Naslednje &raquo;',
 
 ];
index 774cf4cbf48c1a0858bd67ab697de030c31f2651..e9e195fe98ee7a1345b6abcb09edee0abbe667e5 100644 (file)
@@ -6,9 +6,9 @@
  */
 return [
 
-    'password' => 'Gesla morajo biti najmanj osem znakov in se morajo ujemati s potrditvijo.',
+    'password' => 'Gesla morajo biti najmanj osem znakov dolga in se morajo ujemati s potrditvijo.',
     'user' => "Ne moremo najti uporabnika s tem e-poštnim naslovom.",
-    'token' => 'The password reset token is invalid for this email address.',
+    'token' => 'Žeton za ponastavitev gesla ni veljaven za ta e-poštni naslov.',
     'sent' => 'Poslali smo vam povezavo za ponastavitev gesla!',
     'reset' => 'Vaše geslo je bilo ponastavljeno!',
 
index 5d0c0ba7427eeb643d954250d5321eed3e069f7c..feae93bd516a4406bb9cca266e610624e54857bb 100644 (file)
@@ -9,10 +9,10 @@ return [
     // Common Messages
     'settings' => 'Nastavitve',
     'settings_save' => 'Shrani nastavitve',
-    'settings_save_success' => 'Nastavitve, shranjene',
+    'settings_save_success' => 'Nastavitve shranjene',
 
     // App Settings
-    'app_customization' => 'Prilagajanje',
+    'app_customization' => 'Prilagoditev',
     'app_features_security' => 'Lastnosti & Varnost',
     'app_name' => 'Ime aplikacije',
     'app_name_desc' => 'To ime je prikazano v glavi in vsaki sistemski e-pošti.',
@@ -66,110 +66,132 @@ return [
 
     // Maintenance settings
     'maint' => 'Vzdrževanje',
-    'maint_image_cleanup' => 'odstrani /počisti slike',
-    '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_image_cleanup_run' => 'zaženite čiščenje',
-    'maint_image_cleanup_warning' => ':zaznano je bilo število neuporabljenih slik. Ali si prepričan, da želiš odstraniti izbrane slike?',
-    'maint_image_cleanup_success' => ':najdeno in izbrisano je bilo število neuporabljenih slik!',
-    'maint_image_cleanup_nothing_found' => 'Najdenih ni bilo nobenih neuporabljenih slik!',
+    'maint_image_cleanup' => 'Odstrani /počisti slike',
+    'maint_image_cleanup_desc' => "Pregleda vsebino strani in revizij ter ugotovi, katere slike in risbe so v uporabi in katere so odvečne. Preden to poženeš, naredi popolno varnostno kopijo podatkovne zbirke in slik.",
+    'maint_delete_images_only_in_revisions' => 'Also delete images that only exist in old page revisions',
+    'maint_image_cleanup_run' => 'Zaženi čiščenje',
+    'maint_image_cleanup_warning' => 'Najdenih je bilo :count verjetno neuporabljenih slik. Ali si prepričan, da želiš odstraniti izbrane slike?',
+    'maint_image_cleanup_success' => ':count verjetno neuporavljenih slik je bilo najdenih in izbrisanih!',
+    'maint_image_cleanup_nothing_found' => 'Ni bilo najdenih neuporabljenih slik, nič ni izbrisano!',
     'maint_send_test_email' => 'Pošlji testno e-pismo',
     'maint_send_test_email_desc' => 'To pošlje testno e-pošto na vaš e-poštni naslov, naveden v vašem profilu.',
-    'maint_send_test_email_run' => 'Pošlji preizkusno sporočilo',
+    'maint_send_test_email_run' => 'Pošlji testno sporočilo',
     'maint_send_test_email_success' => 'e-pošta poslana na :naslov',
-    'maint_send_test_email_mail_subject' => 'Preizkusno sporočilo',
+    'maint_send_test_email_mail_subject' => 'Testno e-sporočilo',
     'maint_send_test_email_mail_greeting' => 'Zdi se, da dostava e-pošte deluje!',
-    'maint_send_test_email_mail_text' => 'Čestitke! Če ste prejeli e.poštno obvestilo so bile vaše e-poštne nastavitve pravilno konfigurirane.',
+    'maint_send_test_email_mail_text' => 'Čestitke! Če ste prejeli e-poštno obvestilo so bile vaše e-poštne nastavitve pravilno konfigurirane.',
+    'maint_recycle_bin_desc' => 'Izbrisane police, knjige, poglavja in strani se pošljejo v koš, da jih je mogoče obnoviti ali trajno izbrisati. Starejše predmete v košu lahko čez nekaj časa samodejno odstranite, odvisno od konfiguracije sistema.',
+    'maint_recycle_bin_open' => 'Odpri koš',
+
+    // Recycle Bin
+    'recycle_bin' => 'Koš',
+    'recycle_bin_desc' => 'Tu lahko obnovite predmete, ki so bili izbrisani, ali pa jih trajno odstranite s sistema. Ta seznam je nefiltriran, za razliko od podobnih seznamov dejavnosti v sistemu, kjer se uporabljajo filtri dovoljenj.',
+    'recycle_bin_deleted_item' => 'Izbrisan element',
+    'recycle_bin_deleted_by' => 'Izbrisal uporabnik',
+    'recycle_bin_deleted_at' => 'Čas izbrisa',
+    'recycle_bin_permanently_delete' => 'Trajno izbrišem?',
+    'recycle_bin_restore' => 'Obnovi',
+    'recycle_bin_contents_empty' => 'Koš je prazen',
+    'recycle_bin_empty' => 'Izprazni koš',
+    'recycle_bin_empty_confirm' => 'S tem boste trajno uničili vse predmete v košu, vključno z vsebino vsakega predmeta. Ali ste prepričani, da želite izprazniti koš?',
+    'recycle_bin_destroy_confirm' => 'S tem dejanjem boste ta element skupaj s spodaj navedenimi podrejenimi elementi trajno izbrisali iz sistema in te vsebine ne boste mogli obnoviti. Ali ste prepričani, da želite trajno izbrisati ta element?',
+    'recycle_bin_destroy_list' => 'Predmeti, ki naj bodo trajno izbrisani',
+    'recycle_bin_restore_list' => 'Predmeti, ki naj bodo obnovljeni',
+    'recycle_bin_restore_confirm' => 'S tem dejanjem boste izbrisani element, vključno z vsemi podrejenimi elementi, obnovili na prvotno mesto. Če je bilo prvotno mesto od takrat izbrisano in je zdaj v košu, bo treba obnoviti tudi nadrejeni element.',
+    'recycle_bin_restore_deleted_parent' => 'Nadrejeni element je bil prav tako izbrisan. Dokler se ne obnovi nadrejenega elementa, ni mogoče obnoviti njemu podrejenih elementov.',
+    'recycle_bin_destroy_notification' => 'Izbrisano :count skupno število elementov iz koša.',
+    'recycle_bin_restore_notification' => 'Obnovljeno :count skupno število elementov iz koša.',
 
     // Audit Log
-    'audit' => 'Audit Log',
-    'audit_desc' => 'This audit log displays a list of activities tracked in the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.',
-    'audit_event_filter' => 'Event Filter',
-    'audit_event_filter_no_filter' => 'No Filter',
-    'audit_deleted_item' => 'Deleted Item',
-    'audit_deleted_item_name' => 'Name: :name',
-    'audit_table_user' => 'User',
-    'audit_table_event' => 'Event',
-    'audit_table_item' => 'Related Item',
-    'audit_table_date' => 'Activity Date',
-    'audit_date_from' => 'Date Range From',
-    'audit_date_to' => 'Date Range To',
+    'audit' => 'Dnevnik dogodkov',
+    'audit_desc' => 'Ta dnevnik dogodkov prikazuje seznam dejavnosti, ki jim sledi sistem. Seznam je nefiltriran, za razliko od podobnih seznamov dejavnosti v sistemu, kjer se uporabljajo filtri dovoljenj.',
+    'audit_event_filter' => 'Filter dogodkov',
+    'audit_event_filter_no_filter' => 'Ni filtra',
+    'audit_deleted_item' => 'Izbrisan element',
+    'audit_deleted_item_name' => 'Naziv: :name',
+    'audit_table_user' => 'Uporabnik',
+    'audit_table_event' => 'Dogodek',
+    'audit_table_related' => 'Povezani predmet ali podrobnost',
+    'audit_table_date' => 'Datum zadnje dejavnosti',
+    'audit_date_from' => 'Časovno obdobje od',
+    'audit_date_to' => 'Časovno obdobje do',
 
     // Role Settings
     'roles' => 'Vloge',
-    'role_user_roles' => 'Pravilo uporabnika',
-    'role_create' => 'Izdelaj novo polico',
-    'role_create_success' => 'Zapis uspešno ustvarjen',
+    'role_user_roles' => 'Vloge uporabnika',
+    'role_create' => 'Ustvari novo vlogo',
+    'role_create_success' => 'Vloga uspešno ustvarjena',
     'role_delete' => 'Brisanje vloge',
-    'role_delete_confirm' => 'Izbrisana bo vloga z imenom \':vlogaImena\'.',
-    'role_delete_users_assigned' => 'This role has :userCount users assigned to it. If you would like to migrate the users from this role select a new role below.',
-    'role_delete_no_migration' => "Uporabniki niso prenosljivi",
-    'role_delete_sure' => 'Ali ste prepričani, da želite izbrisati to element?',
-    'role_delete_success' => 'Uspešno izbrisano',
-    'role_edit' => 'Uredi zapis',
-    'role_details' => 'Podrobnosti zapisa',
-    'role_name' => 'Ime zapisa',
-    'role_desc' => 'Kratki opis ',
+    'role_delete_confirm' => 'Izbrisana bo vloga z imenom \':roleName\'.',
+    'role_delete_users_assigned' => 'Ta vloga ima dodeljenih :userCount uporabnikov. V kolikor želite uporabnike preseliti iz te vloge, spodaj izberite novo vlogo.',
+    'role_delete_no_migration' => "Ne prenašaj uporabnikov",
+    'role_delete_sure' => 'Ali ste prepričani, da želite izbrisati to vlogo?',
+    'role_delete_success' => 'Vloga uspešno izbrisana',
+    'role_edit' => 'Uredi vlogo',
+    'role_details' => 'Podrobnosti vloge',
+    'role_name' => 'Naziv vloge',
+    'role_desc' => 'Kratki opis vloge',
     'role_external_auth_id' => 'Zunanje dokazilo ID',
     'role_system' => 'Sistemska dovoljenja',
     'role_manage_users' => 'Upravljanje uporabnikov',
-    'role_manage_roles' => 'Vloga upravljanja & vloga dovoljenj',
-    'role_manage_entity_permissions' => 'Upravljanje vseh knjig, poglavij & dovoljenj',
-    'role_manage_own_entity_permissions' => 'Manage permissions on own book, chapter & pages',
-    'role_manage_page_templates' => 'Manage page templates',
-    'role_access_api' => 'Access system API',
+    'role_manage_roles' => 'Upravljanje vlog in dovoljenja vlog',
+    'role_manage_entity_permissions' => 'Upravljanje dovoljenj vseh knjig, poglavij in strani',
+    'role_manage_own_entity_permissions' => 'Upravljanje dovoljenj za svojo knjigo, poglavje in strani',
+    'role_manage_page_templates' => 'Uredi predloge',
+    'role_access_api' => 'API za dostop do sistema',
     'role_manage_settings' => 'Nastavitve za upravljanje',
     'role_asset' => 'Sistemska dovoljenja',
-    'roles_system_warning' => 'Be aware that access to any of the above three permissions can allow a user to alter their own privileges or the privileges of others in the system. Only assign roles with these permissions to trusted users.',
-    'role_asset_desc' => 'These permissions control default access to the assets within the system. Permissions on Books, Chapters and Pages will override these permissions.',
-    'role_asset_admins' => 'Admins are automatically given access to all content but these options may show or hide UI options.',
+    'roles_system_warning' => 'Zavedajte se, da lahko dostop do kateregakoli od zgornjih treh dovoljenj uporabniku omogoči, da spremeni lastne privilegije ali privilegije drugih v sistemu. Vloge s temi dovoljenji dodelite samo zaupanja vrednim uporabnikom.',
+    'role_asset_desc' => 'Ta dovoljenja nadzorujejo privzeti dostop do sredstev v sistemu. Dovoljenja za knjige, poglavja in strani bodo razveljavila ta dovoljenja.',
+    'role_asset_admins' => 'Skrbniki samodejno pridobijo dostop do vseh vsebin, vendar lahko te možnosti prikažejo ali pa skrijejo možnosti uporabniškega vmesnika.',
     'role_all' => 'Vse',
-    'role_own' => 'Own',
-    'role_controlled_by_asset' => '
-46/5000
-Nadzira ga sredstvo, v katerega so naloženi',
+    'role_own' => 'Lasten',
+    'role_controlled_by_asset' => 'Nadzira ga sredstvo, v katerega so naloženi',
     'role_save' => 'Shrani vlogo',
     'role_update_success' => 'Vloga uspešno posodobljena',
     'role_users' => 'Uporabniki v tej vlogi',
-    'role_users_none' => '
-V tej vlogi trenutno ni dodeljen noben uporabnik',
+    'role_users_none' => 'Tej vlogi trenutno ni dodeljen noben uporabnik',
 
     // Users
     'users' => 'Uporabniki',
     'user_profile' => 'Uporabniški profil',
     'users_add_new' => 'Dodaj novega uporabnika',
     'users_search' => 'Išči uporabnike',
+    'users_latest_activity' => 'Zadnja dejavnost',
     'users_details' => 'Podatki o uporabniku',
     'users_details_desc' => 'Nastavite prikazno ime in e-poštni naslov za tega uporabnika. E-poštni naslov bo uporabljen za prijavo v aplikacijo.',
     'users_details_desc_no_email' => ' Nastavite prikazno ime za tega uporabnika, da ga bodo drugi lahko prepoznali.',
     'users_role' => 'Vloge uporabnika',
-    'users_role_desc' => 'Izberi katere vloge bodo dodeljene uporabniku. Če je uporabniku dodeljenih več vlog, se dovoljenja združijo in prejmenjo vse sposobnosti dodeljenih vlog.',
+    'users_role_desc' => 'Izberi vloge, ki bodo dodeljene uporabniku. Če je uporabniku dodeljenih več vlog, se dovoljenja združijo in prejmenjo vsa dovoljenja dodeljenih vlog.',
     'users_password' => 'Uporabniško geslo',
-    'users_password_desc' => 'Nastavite geslo, ki se uporablja za prijavo v aplikacijo. Dolg mora biti vsaj 6 znakov.',
-    'users_send_invite_text' => 'Uporabniku lahko pošljete e-poštno sporočilo s povabilom, ki mu omogoča, da nastavi svoje geslo, ali ga nastavite kar sami.',
-    'users_send_invite_option' => 'Pošlji uporabniku e-povabilo',
+    'users_password_desc' => 'Nastavite geslo, ki se uporablja za prijavo v aplikacijo. Dolgo mora biti vsaj 6 znakov.',
+    'users_send_invite_text' => 'Uporabniku lahko pošljete e-poštno sporočilo s povabilom, ki mu omogoča, da nastavi svoje geslo, ali pa ga nastavite kar sami.',
+    'users_send_invite_option' => 'Pošlji uporabniku e-poštno povabilo',
     'users_external_auth_id' => 'Zunanje dokazilo ID',
     'users_external_auth_id_desc' => 'To je ID, s katerim se ta uporabnik ujema pri komunikaciji z vašim zunanjim sistemom za preverjanje pristnosti.',
     'users_password_warning' => 'Spodaj izpolni le, če želiš spremeniti geslo.',
-    'users_system_public' => 'Ta uporabnik predstavlja vse gostujoče uporabnike, ki obiščejo vaš primer. Za prijavo je ni mogoče uporabiti, ampak je dodeljena samodejno.',
+    'users_system_public' => 'Ta uporabnik predstavlja vse gostujoče uporabnike, ki obiščejo vašo wiki stran. Za prijavo je ni mogoče uporabiti, ampak je dodeljena samodejno.',
     'users_delete' => 'Brisanje uporabnika',
     'users_delete_named' => 'Brisanje uporabnika :userName',
     'users_delete_warning' => 'Iz sistema se bo popolnoma  izbrisal uporabnik z imenom \':userName\'',
-    'users_delete_confirm' => 'Ste prepričani, da želite izbrisati izbranega uporabnika?',
-    'users_delete_success' => 'Uporabniki uspešno odstranjeni.',
+    'users_delete_confirm' => 'Ste prepričani, da želite izbrisati tega uporabnika?',
+    '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' => 'Uredi uporabnika',
     'users_edit_profile' => 'Uredi profil',
     'users_edit_success' => 'Uporabnik uspešno posodobljen',
-    'users_avatar' => 'uporabnikov avatar',
+    'users_avatar' => 'Uporabnikov avatar',
     'users_avatar_desc' => 'Izberi sliko, ki predstavlja uporabnika. Velikost mora biti približno 256px.',
     'users_preferred_language' => 'Izbrani jezik',
     'users_preferred_language_desc' => 'Ta možnost bo spremenila jezik, ki se uporablja za uporabniški vmesnik aplikacije. To ne bo vplivalo na nobeno vsebino, ki jo ustvari uporabnik.',
     'users_social_accounts' => 'Družbene ikone / računi',
-    'users_social_accounts_info' => 'Here you can connect your other accounts for quicker and easier login. Disconnecting an account here does not revoke previously authorized access. Revoke access from your profile settings on the connected social account.',
+    'users_social_accounts_info' => 'Tu lahko za hitrejšo in lažjo prijavo povežete druge račune. Prekinitev povezave računa tukaj ne prekliče predhodno odobrenega dostopa. Prekličite dostop iz nastavitev profila v povezanem družabnem računu.',
     'users_social_connect' => 'Povežite račun',
     'users_social_disconnect' => 'Odklop računa',
-    'users_social_connected' => ':socialAccount  račun je bil uspešno dodan na vašem profilu',
-    'users_social_disconnected' => ':socialAccount račun je bil uspešno odstranjen iz vašega profila',
+    'users_social_connected' => ':socialAccount račun je bil uspešno dodan vašemu profilu.',
+    'users_social_disconnected' => ':socialAccount račun je bil uspešno odstranjen iz vašega profila.',
     'users_api_tokens' => 'API žeton',
     'users_api_tokens_none' => 'Nič API žetonov ni bilo ustvarjenih za uporabnika',
     'users_api_tokens_create' => 'Ustvari žeton',
@@ -179,7 +201,7 @@ V tej vlogi trenutno ni dodeljen noben uporabnik',
     // API Tokens
     'user_api_token_create' => 'Ustvari žeton',
     'user_api_token_name' => 'Ime',
-    'user_api_token_name_desc' => 'Give your token a readable name as a future reminder of its intended purpose.',
+    'user_api_token_name_desc' => 'Dajte žetonu berljivo ime kot prihodnji opomnik o predvidenem namenu.',
     'user_api_token_expiry' => 'Datum poteka',
     'user_api_token_expiry_desc' => 'Določi datum izteka uporabnosti žetona. Po tem datumu, zahteve poslane s tem žetonom, ne bodo več delovale. 
 Če pustite to polje prazno, bo iztek uporabnosti 100.let .',
@@ -188,9 +210,9 @@ V tej vlogi trenutno ni dodeljen noben uporabnik',
     'user_api_token_update_success' => 'API žeton uspešno posodobljen',
     'user_api_token' => 'API žeton',
     'user_api_token_id' => 'Žeton ID',
-    'user_api_token_id_desc' => 'This is a non-editable system generated identifier for this token which will need to be provided in API requests.',
+    'user_api_token_id_desc' => 'To je sistemski identifikator, ki ga ni mogoče urejati za ta žeton in ga je treba navesti v zahtevah za API.',
     'user_api_token_secret' => 'Skrivnost žetona',
-    'user_api_token_secret_desc' => 'This is a system generated secret for this token which will need to be provided in API requests. This will only be displayed this one time so copy this value to somewhere safe and secure.',
+    'user_api_token_secret_desc' => 'To je sistemsko ustvarjena skrivnost za ta žeton, ki jo bo treba navesti v zahtevah za API. To bo prikazano samo enkrat, zato kopirajte to vrednost na varno mesto.',
     'user_api_token_created' => 'Žeton ustvarjen :timeAgo',
     'user_api_token_updated' => 'Žeton posodobljen :timeAgo',
     'user_api_token_delete' => 'Briši žeton',
@@ -218,6 +240,7 @@ V tej vlogi trenutno ni dodeljen noben uporabnik',
         'ja' => '日本語',
         'ko' => '한국어',
         'nl' => 'Nederlands',
+        'nb' => 'Norsk (Bokmål)',
         'pl' => 'Polski',
         'pt_BR' => 'Português do Brasil',
         'ru' => 'Русский',
index fec6d56027d41517204cb587261b22402475993b..bc7242bac35d4e1ab13861f568a14e4ffb216ac1 100644 (file)
@@ -90,6 +90,7 @@ return [
     'required_without'     => 'Polje atributa je obvezno, če: vrednosti niso prisotne.',
     'required_without_all' => 'Polje atributa je obvezno, če nobena od: vrednosti ni prisotna.',
     'same'                 => 'Atribut in: drugi se morajo ujemati.',
+    'safe_url'             => 'Podana povezava morda ni varna.',
     'size'                 => [
         'numeric' => ':attribute mora biti :velikost.',
         'file'    => ':attribute mora biti :velikost KB.',
index 1cb8051b93920e18599396129598c7c0b3ef154c..e3fa051555f92bd5f24edf72342f110d0b385f84 100644 (file)
@@ -45,4 +45,5 @@ return [
 
     // Other
     'commented_on'                => 'kommenterade',
+    'permissions_update'          => 'uppdaterade behörigheter',
 ];
index 155e4b0025316bc4c933015370b81b50cff34157..8669055351722eb127ac674a2a57978f66777ac5 100644 (file)
@@ -43,7 +43,7 @@ return [
     'reset_password' => 'Återställ lösenord',
     'reset_password_send_instructions' => 'Ange din e-postadress nedan så skickar vi ett mail med en länk för att återställa ditt lösenord.',
     'reset_password_send_button' => 'Skicka återställningslänk',
-    'reset_password_sent' => 'A password reset link will be sent to :email if that email address is found in the system.',
+    'reset_password_sent' => 'En länk för återställning av lösenord kommer att skickas till :email om den e-postadressen finns i systemet.',
     'reset_password_success' => 'Ditt lösenord har återställts.',
     'email_reset_subject' => 'Återställ ditt lösenord till :appName',
     'email_reset_text' => 'Du får detta mail eftersom vi fått en begäran om att återställa lösenordet till ditt konto.',
index f6acd332fbc91dabc9351af1d1a715529c6a648a..7ad2712ccf40bbd00a18fb79fd1e764e64a8d7f1 100644 (file)
@@ -33,7 +33,7 @@ return [
     'copy' => 'Kopiera',
     'reply' => 'Svara',
     'delete' => 'Ta bort',
-    'delete_confirm' => 'Confirm Deletion',
+    'delete_confirm' => 'Bekräfta radering',
     'search' => 'Sök',
     'search_clear' => 'Rensa sökning',
     'reset' => 'Återställ',
@@ -67,8 +67,8 @@ return [
     'profile_menu' => 'Profilmeny',
     'view_profile' => 'Visa profil',
     'edit_profile' => 'Redigera profil',
-    'dark_mode' => 'Dark Mode',
-    'light_mode' => 'Light Mode',
+    'dark_mode' => 'Mörkt läge',
+    'light_mode' => 'Ljust Läge',
 
     // Layout tabs
     'tab_info' => 'Information',
index aef0400d26b97f9ce96ae37bb177e25f7d10eb60..68b141945e1d30d130762981d1aa2789941d152a 100644 (file)
@@ -15,7 +15,7 @@ return [
     'image_load_more' => 'Ladda fler',
     'image_image_name' => 'Bildnamn',
     'image_delete_used' => 'Den här bilden används på nedanstående sidor.',
-    'image_delete_confirm_text' => 'Are you sure you want to delete this image?',
+    'image_delete_confirm_text' => 'Är du säker på att du vill radera denna bild?',
     'image_select_image' => 'Välj bild',
     'image_dropzone' => 'Släpp bilder här eller klicka för att ladda upp',
     'images_deleted' => 'Bilder borttagna',
@@ -29,6 +29,6 @@ return [
     'code_editor' => 'Redigera kod',
     'code_language' => 'Språk',
     'code_content' => 'Kod',
-    'code_session_history' => 'Session History',
+    'code_session_history' => 'Sessionshistorik',
     'code_save' => 'Spara',
 ];
index b71a27ccfbf8fd5c5c254b1b98ec46b939971619..d31bef6f00dc593bed331c3326270cbc140b58ef 100644 (file)
@@ -22,6 +22,7 @@ return [
     'meta_created_name' => 'Skapad :timeLength av :user',
     'meta_updated' => 'Uppdaterad :timeLength',
     'meta_updated_name' => 'Uppdaterad :timeLength av :user',
+    'meta_owned_name' => 'Owned by :user',
     'entity_select' => 'Välj enhet',
     'images' => 'Bilder',
     'my_recent_drafts' => 'Mina nyaste utkast',
@@ -39,6 +40,7 @@ return [
     'permissions_intro' => 'Dessa rättigheter kommer att överskrida eventuella rollbaserade rättigheter.',
     'permissions_enable' => 'Aktivera anpassade rättigheter',
     'permissions_save' => 'Spara rättigheter',
+    'permissions_owner' => 'Owner',
 
     // Search
     'search_results' => 'Sökresultat',
@@ -47,8 +49,8 @@ return [
     'search_no_pages' => 'Inga sidor matchade sökningen',
     'search_for_term' => 'Sök efter :term',
     'search_more' => 'Fler resultat',
-    'search_advanced' => 'Advanced Search',
-    'search_terms' => 'Search Terms',
+    'search_advanced' => 'Avancerad sök',
+    'search_terms' => 'Söktermer',
     'search_content_type' => 'Innehållstyp',
     'search_exact_matches' => 'Exakta matchningar',
     'search_tags' => 'Taggar',
@@ -146,7 +148,7 @@ return [
     'chapters_create' => 'Skapa nytt kapitel',
     'chapters_delete' => 'Radera kapitel',
     'chapters_delete_named' => 'Radera kapitlet :chapterName',
-    'chapters_delete_explain' => 'Du håller på att ta bort kapitlet \':chapterName\'. Alla sidor kommer att flyttas direkt in i den aktuella boken istället.',
+    'chapters_delete_explain' => 'Detta kommer att ta bort kapitlet med namnet \':chapterName\'. Alla sidor som finns inom detta kapitel kommer också att raderas.',
     'chapters_delete_confirm' => 'Är du säker på att du vill ta bort det här kapitlet?',
     'chapters_edit' => 'Redigera kapitel',
     'chapters_edit_named' => 'Redigera kapitel :chapterName',
@@ -208,6 +210,7 @@ return [
     'pages_revisions' => 'Sidrevisioner',
     'pages_revisions_named' => 'Sidrevisioner för :pageName',
     'pages_revision_named' => 'Sidrevision för :pageName',
+    'pages_revision_restored_from' => 'Restored from #:id; :summary',
     'pages_revisions_created_by' => 'Skapad av',
     'pages_revisions_date' => 'Revisionsdatum',
     'pages_revisions_number' => '#',
@@ -256,7 +259,7 @@ return [
     'attachments_upload' => 'Ladda upp fil',
     'attachments_link' => 'Bifoga länk',
     'attachments_set_link' => 'Ange länk',
-    'attachments_delete' => 'Are you sure you want to delete this attachment?',
+    'attachments_delete' => 'Är du säker på att du vill ta bort bilagan?',
     'attachments_dropzone' => 'Släpp filer här eller klicka för att ladda upp',
     'attachments_no_files' => 'Inga filer har laddats upp',
     'attachments_explain_link' => 'Du kan bifoga en länk om du inte vill ladda upp en fil. Detta kan vara en länk till en annan sida eller till en fil i molnet.',
@@ -265,7 +268,7 @@ return [
     'attachments_link_url' => 'Länk till fil',
     'attachments_link_url_hint' => 'URL till sida eller fil',
     'attach' => 'Bifoga',
-    'attachments_insert_link' => 'Add Attachment Link to Page',
+    'attachments_insert_link' => 'Lägg till bilagelänk till sida',
     'attachments_edit_file' => 'Redigera fil',
     'attachments_edit_file_name' => 'Filnamn',
     'attachments_edit_drop_upload' => 'Släpp filer här eller klicka för att ladda upp och skriva över',
index 9f5545f36d96df981358b9c4e73277def5c32772..640309b8848fe4396f3463079f4463887c59c05f 100644 (file)
@@ -8,7 +8,7 @@ return [
 
     'password' => 'Lösenord måste vara minst sex tecken långa och anges likadant två gånger.',
     'user' => "Det finns ingen användare med den e-postadressen.",
-    'token' => 'The password reset token is invalid for this email address.',
+    'token' => 'Lösenordsåterställningstoken är ogiltig för denna e-postadress.',
     'sent' => 'Vi har mailat dig en länk för att återställa ditt lösenord!',
     'reset' => 'Ditt lösenord har blivit återställt!',
 
index 1b2f33bdecde75a73191df467479a3a86630e357..8c6caaf4a9e6d1096e6423f30fa89382fb5533fc 100644 (file)
@@ -68,7 +68,7 @@ return [
     'maint' => 'Underhåll',
     'maint_image_cleanup' => 'Rensa bilder',
     'maint_image_cleanup_desc' => "Söker igenom innehåll i sidor & revisioner för att se vilka bilder och teckningar som är i bruk och vilka som är överflödiga. Se till att ta en komplett backup av databas och bilder innan du kör detta.",
-    'maint_image_cleanup_ignore_revisions' => 'Ignorera bilder i revisioner',
+    'maint_delete_images_only_in_revisions' => 'Ta också bort bilder som bara finns i gamla sidrevideringar',
     'maint_image_cleanup_run' => 'Kör rensning',
     'maint_image_cleanup_warning' => 'Hittade :count bilder som potentiellt inte används. Vill du verkligen ta bort dessa bilder?',
     'maint_image_cleanup_success' => 'Hittade och raderade :count bilder som potentiellt inte används!',
@@ -80,20 +80,41 @@ return [
     'maint_send_test_email_mail_subject' => 'Testmejl',
     'maint_send_test_email_mail_greeting' => 'E-postleverans verkar fungera!',
     'maint_send_test_email_mail_text' => 'Grattis! Eftersom du fick detta e-postmeddelande verkar dina e-postinställningar vara korrekt konfigurerade.',
+    'maint_recycle_bin_desc' => 'Borttagna hyllor, böcker, kapitel & sidor skickas till papperskorgen så att de kan återställas eller raderas permanent. Äldre objekt i papperskorgen kan automatiskt tas bort efter ett tag beroende på systemkonfiguration.',
+    'maint_recycle_bin_open' => 'Öppna papperskorgen',
+
+    // Recycle Bin
+    'recycle_bin' => 'Papperskorgen',
+    'recycle_bin_desc' => 'Här kan du återställa objekt som har tagits bort eller välja att permanent ta bort dem från systemet. Denna lista är ofiltrerad till skillnad från liknande aktivitetslistor i systemet där behörighetsfilter tillämpas.',
+    'recycle_bin_deleted_item' => 'Raderat objekt',
+    'recycle_bin_deleted_by' => 'Borttagen av',
+    'recycle_bin_deleted_at' => 'Tid för borttagning',
+    'recycle_bin_permanently_delete' => 'Radera permanent',
+    'recycle_bin_restore' => 'Återställ',
+    'recycle_bin_contents_empty' => 'Papperskorgen är för närvarande tom',
+    'recycle_bin_empty' => 'Töm papperskorgen',
+    'recycle_bin_empty_confirm' => 'Detta kommer permanent att förstöra alla objekt i papperskorgen inklusive innehåll som finns i varje objekt. Är du säker du vill tömma papperskorgen?',
+    'recycle_bin_destroy_confirm' => 'Denna åtgärd kommer att permanent ta bort detta objekt, tillsammans med alla underordnade element som anges nedan, från systemet och du kommer inte att kunna återställa detta innehåll. Är du säker på att du vill ta bort objektet permanent?',
+    'recycle_bin_destroy_list' => 'Objekt som ska förstöras',
+    'recycle_bin_restore_list' => 'Objekt som ska återställas',
+    'recycle_bin_restore_confirm' => 'Denna åtgärd kommer att återställa det raderade objektet, inklusive alla underordnade element, till deras ursprungliga plats. Om den ursprungliga platsen har tagits bort sedan dess, och är nu i papperskorgen, kommer det överordnade objektet också att behöva återställas.',
+    'recycle_bin_restore_deleted_parent' => 'Föräldern till det här objektet har också tagits bort. Dessa kommer att förbli raderade tills den förälder är återställd.',
+    'recycle_bin_destroy_notification' => 'Raderade :count totala objekt från papperskorgen.',
+    'recycle_bin_restore_notification' => 'Återställt :count totala objekt från papperskorgen.',
 
     // Audit Log
-    'audit' => 'Audit Log',
-    'audit_desc' => 'This audit log displays a list of activities tracked in the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.',
-    'audit_event_filter' => 'Event Filter',
-    'audit_event_filter_no_filter' => 'No Filter',
-    'audit_deleted_item' => 'Deleted Item',
-    'audit_deleted_item_name' => 'Name: :name',
-    'audit_table_user' => 'User',
-    'audit_table_event' => 'Event',
-    'audit_table_item' => 'Related Item',
-    'audit_table_date' => 'Activity Date',
-    'audit_date_from' => 'Date Range From',
-    'audit_date_to' => 'Date Range To',
+    'audit' => 'Auditlogg',
+    'audit_desc' => 'Denna granskningslogg visar en lista över aktiviteter som spåras i systemet. Denna lista är ofiltrerad till skillnad från liknande aktivitetslistor i systemet där behörighetsfilter tillämpas.',
+    'audit_event_filter' => 'Händelse Filter',
+    'audit_event_filter_no_filter' => 'Inget filter',
+    'audit_deleted_item' => 'Raderat objekt',
+    'audit_deleted_item_name' => 'Namn: :name',
+    'audit_table_user' => 'Användare',
+    'audit_table_event' => 'Händelse',
+    'audit_table_related' => 'Relaterat objekt eller detalj',
+    'audit_table_date' => 'Datum för senaste aktiviteten',
+    'audit_date_from' => 'Datumintervall från',
+    'audit_date_to' => 'Datumintervall till',
 
     // Role Settings
     'roles' => 'Roller',
@@ -120,7 +141,7 @@ return [
     'role_access_api' => 'Åtkomst till systemets API',
     'role_manage_settings' => 'Hantera appinställningar',
     'role_asset' => 'Tillgång till innehåll',
-    'roles_system_warning' => 'Be aware that access to any of the above three permissions can allow a user to alter their own privileges or the privileges of others in the system. Only assign roles with these permissions to trusted users.',
+    'roles_system_warning' => 'Var medveten om att åtkomst till någon av ovanstående tre behörigheter kan tillåta en användare att ändra sina egna rättigheter eller andras rättigheter i systemet. Tilldela endast roller med dessa behörigheter till betrodda användare.',
     'role_asset_desc' => 'Det här är standardinställningarna för allt innehåll i systemet. Eventuella anpassade rättigheter på böcker, kapitel och sidor skriver över dessa inställningar.',
     'role_asset_admins' => 'Administratörer har automatisk tillgång till allt innehåll men dessa alternativ kan visa och dölja vissa gränssnittselement',
     'role_all' => 'Alla',
@@ -136,6 +157,7 @@ return [
     'user_profile' => 'Användarprofil',
     'users_add_new' => 'Lägg till användare',
     'users_search' => 'Sök användare',
+    'users_latest_activity' => 'Senaste aktivitet',
     'users_details' => 'Användarinformation',
     'users_details_desc' => 'Ange ett visningsnamn och en e-postadress för den här användaren. E-postadressen kommer att användas vid inloggningen.',
     'users_details_desc_no_email' => 'Ange ett visningsnamn för den här användaren så att andra kan känna igen den.',
@@ -153,7 +175,10 @@ return [
     'users_delete_named' => 'Ta bort användaren :userName',
     'users_delete_warning' => 'Detta kommer att ta bort användaren \':userName\' från systemet helt och hållet.',
     'users_delete_confirm' => 'Är du säker på att du vill ta bort användaren?',
-    'users_delete_success' => 'Användaren har tagits bort',
+    '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' => 'Redigera användare',
     'users_edit_profile' => 'Redigera profil',
     'users_edit_success' => 'Användaren har uppdaterats',
@@ -214,6 +239,7 @@ return [
         'ja' => '日本語',
         'ko' => '한국어',
         'nl' => 'Nederlands',
+        'nb' => 'Norsk (Bokmål)',
         'pl' => 'Polski',
         'pt_BR' => 'Português do Brasil',
         'ru' => 'Русский',
index 5da78b2457f14fe13f48c476aee929650e0a8cdd..3190318559d1642c960bc6b2c612a27b874fb757 100644 (file)
@@ -90,6 +90,7 @@ return [
     'required_without'     => ':attribute är obligatoriskt när :values inte finns.',
     'required_without_all' => ':attribute är obligatirskt när ingen av :values finns.',
     'same'                 => ':attribute och :other måste stämma överens.',
+    'safe_url'             => 'Den angivna länken kanske inte är säker.',
     'size'                 => [
         'numeric' => ':attribute måste vara :size.',
         'file'    => ':attribute måste vara :size kilobyte.',
index cb8101461e8801bae98fb6e6078c4e235df90e98..67f9653f432c1975a7164bf509cce8d6b1611f0f 100644 (file)
@@ -45,4 +45,5 @@ return [
 
     // Other
     'commented_on'                => 'yorum yaptı',
+    'permissions_update'          => 'güncellenmiş izinler',
 ];
index cf2612640d14489b0f9b6efbecd0971fc268f9ed..2f447e7953a51a7a6cb3bfb257c57904eac855cb 100644 (file)
@@ -22,6 +22,7 @@ return [
     'meta_created_name' => ':user tarafından :timeLength oluşturuldu',
     'meta_updated' => ':timeLength güncellendi',
     'meta_updated_name' => ':user tarafından :timeLength güncellendi',
+    'meta_owned_name' => 'Owned by :user',
     'entity_select' => 'Öge Seçimi',
     'images' => 'Görseller',
     'my_recent_drafts' => 'Son Taslaklarım',
@@ -39,6 +40,7 @@ return [
     'permissions_intro' => 'Etkinleştirildikten sonra bu izinler, diğer bütün izinlerden öncelikli olacaktır.',
     'permissions_enable' => 'Özelleştirilmiş Yetkileri Etkinleştir',
     'permissions_save' => 'İzinleri Kaydet',
+    'permissions_owner' => 'Sahip',
 
     // Search
     'search_results' => 'Arama Sonuçları',
@@ -146,7 +148,7 @@ return [
     'chapters_create' => 'Yeni Bölüm Oluştur',
     'chapters_delete' => 'Bölümü Sil',
     'chapters_delete_named' => ':chapterName Bölümünü Sil',
-    'chapters_delete_explain' => 'Bu işlem sonunda \':chapterName\' bölümü silinecek ve bu bölüme ait bütün sayfalar direkt olarak ana kitaba aktarılacaktır.',
+    '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' => 'Bölümü silmek istediğinize emin misiniz?',
     'chapters_edit' => 'Bölümü Düzenle',
     'chapters_edit_named' => ':chapterName Bölümünü Düzenle',
@@ -208,6 +210,7 @@ return [
     'pages_revisions' => 'Sayfa Revizyonları',
     'pages_revisions_named' => ':pageName için Sayfa Revizyonları',
     'pages_revision_named' => ':pageName için Sayfa Revizyonu',
+    'pages_revision_restored_from' => 'Restored from #:id; :summary',
     'pages_revisions_created_by' => 'Revize Eden',
     'pages_revisions_date' => 'Revizyon Tarihi',
     'pages_revisions_number' => '#',
@@ -265,7 +268,7 @@ return [
     'attachments_link_url' => 'Dosya bağlantısı',
     'attachments_link_url_hint' => 'Dosyanın veya sitenin url adresi',
     'attach' => 'Ekle',
-    'attachments_insert_link' => 'Add Attachment Link to Page',
+    'attachments_insert_link' => 'Sayfaya Bağlantı Ekle',
     'attachments_edit_file' => 'Dosyayı Düzenle',
     'attachments_edit_file_name' => 'Dosya Adı',
     'attachments_edit_drop_upload' => 'Üzerine yazılacak dosyaları sürükleyin veya seçin',
index 14da79438c0b6fabbbf9f7e998b744f1eec78c13..fd861c17011c23573a1de8f2e60444f6dc640bc9 100755 (executable)
@@ -68,7 +68,7 @@ return [
     'maint' => 'Bakım',
     'maint_image_cleanup' => 'Görselleri Temizle',
     'maint_image_cleanup_desc' => "Sayfaları ve revizyon içeriklerini tarayarak hangi görsellerin ve çizimlerin kullanımda olduğunu ve hangilerinin gereksiz olduğunu tespit eder. Bunu başlatmadan önce veritabanının ve görsellerin tam bir yedeğinin alındığından emin olun.",
-    'maint_image_cleanup_ignore_revisions' => 'Revizyonlardaki görselleri yoksay',
+    'maint_delete_images_only_in_revisions' => 'Eski sayfa revizyonlarındaki görselleri de sil',
     'maint_image_cleanup_run' => 'Temizliği Başlat',
     'maint_image_cleanup_warning' => 'Muhtemelen kullanılmayan :count adet görsel bulundu. Bu görselleri silmek istediğinize emin misiniz?',
     'maint_image_cleanup_success' => 'Muhtemelen kullanılmayan :count adet görsel bulundu ve silindi!',
@@ -80,20 +80,41 @@ return [
     'maint_send_test_email_mail_subject' => 'Deneme E-postası',
     'maint_send_test_email_mail_greeting' => 'E-posta iletimi çalışıyor gibi görünüyor!',
     'maint_send_test_email_mail_text' => 'Tebrikler! Eğer bu e-posta bildirimini alıyorsanız, e-posta ayarlarınız doğru bir şekilde ayarlanmış demektir.',
+    'maint_recycle_bin_desc' => 'Silinen raflar, kitaplar, bölümler ve sayfalar geri dönüşüm kutusuna gönderilir, böylece geri yüklenebilir veya kalıcı olarak silinebilir. Geri dönüşüm kutusundaki daha eski öğeler, sistem yapılandırmasına bağlı olarak bir süre sonra otomatik olarak kaldırılabilir.',
+    'maint_recycle_bin_open' => 'Geri Dönüşüm Kutusunu Aç',
+
+    // Recycle Bin
+    'recycle_bin' => 'Geri Dönüşüm Kutusu',
+    'recycle_bin_desc' => 'Burada silinen öğeleri geri yükleyebilir veya bunları sistemden kalıcı olarak kaldırmayı seçebilirsiniz. Bu liste, izin filtrelerinin uygulandığı sistemdeki benzer etkinlik listelerinden farklı olarak filtrelenmez.',
+    'recycle_bin_deleted_item' => 'Silinen öge',
+    'recycle_bin_deleted_by' => 'Tarafından silindi',
+    'recycle_bin_deleted_at' => 'Silinme Zamanı',
+    'recycle_bin_permanently_delete' => 'Kalıcı Olarak Sil',
+    'recycle_bin_restore' => 'Geri Yükle',
+    'recycle_bin_contents_empty' => 'Geri dönüşüm kutusu boş',
+    'recycle_bin_empty' => 'Geri Dönüşüm Kutusunu Boşalt',
+    'recycle_bin_empty_confirm' => 'Bu işlem, her bir öğenin içinde bulunan içerik de dahil olmak üzere geri dönüşüm kutusundaki tüm öğeleri kalıcı olarak imha edecektir. Geri dönüşüm kutusunu boşaltmak istediğinizden emin misiniz?',
+    'recycle_bin_destroy_confirm' => 'Bu işlem, bu öğeyi kalıcı olarak ve aşağıda listelenen alt öğelerle birlikte sistemden silecek ve bu içeriği geri yükleyemeyeceksiniz. Bu öğeyi kalıcı olarak silmek istediğinizden emin misiniz?',
+    'recycle_bin_destroy_list' => 'Kalıcı Olarak Silinecek Öğeler',
+    'recycle_bin_restore_list' => 'Geri Yüklenecek Öğeler',
+    'recycle_bin_restore_confirm' => 'Bu eylem, tüm alt öğeler dahil olmak üzere silinen öğeyi orijinal konumlarına geri yükleyecektir. Orijinal konum o zamandan beri silinmişse ve şimdi geri dönüşüm kutusunda bulunuyorsa, üst öğenin de geri yüklenmesi gerekecektir.',
+    'recycle_bin_restore_deleted_parent' => 'Bu öğenin üst öğesi de silindi. Bunlar, üst öğe de geri yüklenene kadar silinmiş olarak kalacaktır.',
+    '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',
-    'audit_desc' => 'This audit log displays a list of activities tracked in the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.',
-    'audit_event_filter' => 'Event Filter',
-    'audit_event_filter_no_filter' => 'No Filter',
-    'audit_deleted_item' => 'Deleted Item',
-    'audit_deleted_item_name' => 'Name: :name',
-    'audit_table_user' => 'User',
-    'audit_table_event' => 'Event',
-    'audit_table_item' => 'Related Item',
-    'audit_table_date' => 'Activity Date',
-    'audit_date_from' => 'Date Range From',
-    'audit_date_to' => 'Date Range To',
+    'audit' => 'Denetim Kaydı',
+    'audit_desc' => 'Bu denetim günlüğü, sistemde izlenen etkinliklerin bir listesini görüntüler. Bu liste, izin filtrelerinin uygulandığı sistemdeki benzer etkinlik listelerinden farklı olarak filtrelenmez.',
+    'audit_event_filter' => 'Etkinlik Filtresi',
+    'audit_event_filter_no_filter' => 'Filtre Yok',
+    'audit_deleted_item' => 'Silinen Öge',
+    'audit_deleted_item_name' => 'Isim: :name',
+    'audit_table_user' => 'Kullanıcı',
+    'audit_table_event' => 'Etkinlik',
+    'audit_table_related' => 'İlgili Öğe veya Detay',
+    'audit_table_date' => 'Aktivite Tarihi',
+    'audit_date_from' => 'Tarih Aralığından',
+    'audit_date_to' => 'Tarih Aralığına',
 
     // Role Settings
     'roles' => 'Roller',
@@ -136,6 +157,7 @@ return [
     'user_profile' => 'Kullanıcı Profili',
     'users_add_new' => 'Yeni Kullanıcı Ekle',
     'users_search' => 'Kullanıcı Ara',
+    'users_latest_activity' => 'Son Etkinlik',
     'users_details' => 'Kullanıcı Detayları',
     'users_details_desc' => 'Bu kullanıcı için gösterilecek bir isim ve e-posta adresi belirleyin. Buraya yazacağınız e-posta adresi, uygulamaya giriş yaparken kullanılacaktır.',
     'users_details_desc_no_email' => 'Diğer kullanıcılar tarafından tanınabilmesi için bir isim belirleyin.',
@@ -153,7 +175,10 @@ return [
     'users_delete_named' => ':userName kullanıcısını sil ',
     'users_delete_warning' => 'Bu işlem \':userName\' kullanıcısını sistemden tamamen silecektir.',
     'users_delete_confirm' => 'Bu kullanıcıyı tamamen silmek istediğinize emin misiniz?',
-    'users_delete_success' => 'Kullanıcılar başarıyla silindi.',
+    'users_migrate_ownership' => 'Sahipliği Taşıyın',
+    'users_migrate_ownership_desc' => 'Başka bir kullanıcının şu anda bu kullanıcıya ait olan tüm öğelerin sahibi olmasını istiyorsanız buradan bir kullanıcı seçin.',
+    'users_none_selected' => 'Hiçbir kullanıcı seçilmedi',
+    'users_delete_success' => 'Kullanıcı başarıyla kaldırıldı',
     'users_edit' => 'Kullanıcıyı Düzenle',
     'users_edit_profile' => 'Profili Düzenle',
     'users_edit_success' => 'Kullanıcı başarıyla güncellendi',
@@ -214,6 +239,7 @@ return [
         'ja' => '日本語',
         'ko' => '한국어',
         'nl' => 'Nederlands',
+        'nb' => 'Norsk (Bokmål)',
         'pl' => 'Polski',
         'pt_BR' => 'Português do Brasil',
         'ru' => 'Русский',
index 801ec89da46acb9d2cc040835f7be9762d89fb48..45b7189d7dca89a2d3cc945f8be4d87f9b1052c8 100644 (file)
@@ -90,6 +90,7 @@ return [
     'required_without'     => ':values değerinin bulunmuyor olması, :attribute alanını zorunlu kılar.',
     'required_without_all' => ':values değerlerinden hiçbirinin bulunmuyor olması, :attribute alanını zorunlu kılar.',
     'same'                 => ':attribute ve :other eşleşmelidir.',
+    'safe_url'             => 'Sağlanan bağlantı güvenli olmayabilir.',
     'size'                 => [
         'numeric' => ':attribute, :size boyutunda olmalıdır.',
         'file'    => ':attribute, :size kilobayt olmalıdır.',
index 900ccd2b84fca49e450dad2c695aa128aa10e595..51cf9db19058b4f8a980b23884bb1fee9d3caa44 100644 (file)
@@ -45,4 +45,5 @@ return [
 
     // Other
     'commented_on'                => 'прокоментував',
+    'permissions_update'          => 'updated permissions',
 ];
index 1af8502cd7f8d37919d4ec53b6f2352131ae0756..79fe3c24bf86c16bdfcf521d2e65bed2c435bd68 100644 (file)
@@ -33,7 +33,7 @@ return [
     'copy' => 'Копіювати',
     'reply' => 'Відповісти',
     'delete' => 'Видалити',
-    'delete_confirm' => 'Confirm Deletion',
+    'delete_confirm' => 'Підтвердити видалення',
     'search' => 'Шукати',
     'search_clear' => 'Очистити пошук',
     'reset' => 'Скинути',
index bf4622c2b33b621e50b6370f1687e2dbd230417e..a37d196eb522c7e2afa6af82a75feba12b637f1e 100644 (file)
@@ -15,7 +15,7 @@ return [
     'image_load_more' => 'Завантажити ще',
     'image_image_name' => 'Назва зображення',
     'image_delete_used' => 'Це зображення використовується на наступних сторінках.',
-    'image_delete_confirm_text' => 'Are you sure you want to delete this image?',
+    'image_delete_confirm_text' => 'Ви дійсно хочете видалити це зображення?',
     'image_select_image' => 'Вибрати зображення',
     'image_dropzone' => 'Перетягніть зображення, або натисніть тут для завантаження',
     'images_deleted' => 'Зображень видалено',
@@ -29,6 +29,6 @@ return [
     'code_editor' => 'Редагувати код',
     'code_language' => 'Мова коду',
     'code_content' => 'Вміст коду',
-    'code_session_history' => 'Session History',
+    'code_session_history' => 'Історія сесії',
     'code_save' => 'Зберегти Код',
 ];
index 45da0e21cd0f6b4390a10ae734e111a210d78cc0..3ca3915f40a376049d319deb75ca495ca4d2eed4 100644 (file)
@@ -22,6 +22,7 @@ return [
     'meta_created_name' => ':user створив :timeLength',
     'meta_updated' => 'Оновлено :timeLength',
     'meta_updated_name' => ':user оновив :timeLength',
+    'meta_owned_name' => 'Owned by :user',
     'entity_select' => 'Вибір об\'єкта',
     'images' => 'Зображення',
     'my_recent_drafts' => 'Мої останні чернетки',
@@ -39,6 +40,7 @@ return [
     'permissions_intro' => 'Після ввімкнення ці дозволи будуть мати вищий пріоритет ніж інші дозволи ролей.',
     'permissions_enable' => 'Увімкнути спеціальні дозволи',
     'permissions_save' => 'Зберегти дозволи',
+    'permissions_owner' => 'Owner',
 
     // Search
     'search_results' => 'Результати пошуку',
@@ -47,8 +49,8 @@ return [
     'search_no_pages' => 'Немає сторінок, які відповідають цьому пошуку',
     'search_for_term' => 'Шукати :term',
     'search_more' => 'Більше результатів',
-    'search_advanced' => 'Advanced Search',
-    'search_terms' => 'Search Terms',
+    'search_advanced' => 'Розширений пошук',
+    'search_terms' => 'Пошукові фрази',
     'search_content_type' => 'Тип вмісту',
     'search_exact_matches' => 'Точна відповідність',
     'search_tags' => 'Пошукові теги',
@@ -146,7 +148,7 @@ return [
     'chapters_create' => 'Створити новий розділ',
     'chapters_delete' => 'Видалити розділ',
     'chapters_delete_named' => 'Видалити розділ :chapterName',
-    'chapters_delete_explain' => 'Ця дія видалить розділ з назвою \':chapterName\'. Всі сторінки будуть вилучені, та додані безпосередньо до батьківської книги.',
+    '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' => 'Ви впевнені, що хочете видалити цей розділ?',
     'chapters_edit' => 'Редагувати розділ',
     'chapters_edit_named' => 'Редагувати розділ :chapterName',
@@ -208,6 +210,7 @@ return [
     'pages_revisions' => 'Версія сторінки',
     'pages_revisions_named' => 'Версії сторінки для :pageName',
     'pages_revision_named' => 'Версія сторінки для :pageName',
+    'pages_revision_restored_from' => 'Restored from #:id; :summary',
     'pages_revisions_created_by' => 'Створена',
     'pages_revisions_date' => 'Дата версії',
     'pages_revisions_number' => '#',
@@ -256,7 +259,7 @@ return [
     'attachments_upload' => 'Завантажити файл',
     'attachments_link' => 'Приєднати посилання',
     'attachments_set_link' => 'Встановити посилання',
-    'attachments_delete' => 'Are you sure you want to delete this attachment?',
+    'attachments_delete' => 'Дійсно хочете видалити це вкладення?',
     'attachments_dropzone' => 'Перетягніть файли, або натисніть тут щоб прикріпити файл',
     'attachments_no_files' => 'Файли не завантажені',
     'attachments_explain_link' => 'Ви можете приєднати посилання, якщо не бажаєте завантажувати файл. Це може бути посилання на іншу сторінку або посилання на файл у хмарі.',
@@ -265,7 +268,7 @@ return [
     'attachments_link_url' => 'Посилання на файл',
     'attachments_link_url_hint' => 'URL-адреса сайту або файлу',
     'attach' => 'Приєднати',
-    'attachments_insert_link' => 'Add Attachment Link to Page',
+    'attachments_insert_link' => 'Додати посилання на вкладення',
     'attachments_edit_file' => 'Редагувати файл',
     'attachments_edit_file_name' => 'Назва файлу',
     'attachments_edit_drop_upload' => 'Перетягніть файли, або натисніть тут щоб завантажити та перезаписати',
index bf9a46ecb2c21de2db3abd989f701df8d85dbc7a..eb20bae74bf57e921caf9cbce25e2448d228e049 100644 (file)
@@ -92,8 +92,8 @@ return [
     'api_no_authorization_found' => 'У запиті не знайдено токен авторизації',
     'api_bad_authorization_format' => 'У запиті знайдено токен авторизації, але формат недійсний',
     'api_user_token_not_found' => 'Не знайдено відповідного API-токена для наданого токена авторизації',
-    'api_incorrect_token_secret' => 'The secret provided for the given used API token is incorrect',
-    'api_user_no_api_permission' => 'The owner of the used API token does not have permission to make API calls',
+    'api_incorrect_token_secret' => 'Секрет, наданий для даного використовуваного токена API є неправильним',
+    'api_user_no_api_permission' => 'Власник використовуваного токена API не має дозволу здійснювати виклики API',
     'api_user_token_expired' => 'Термін дії токена авторизації закінчився',
 
     // Settings & Maintenance
index 1caf94701229cc30688f2e440ce59022cbc0a552..b94ae4e8c03d64af50902105d8c06c2984d2bad7 100644 (file)
@@ -68,7 +68,7 @@ return [
     'maint' => 'Обслуговування',
     'maint_image_cleanup' => 'Очищення зображень',
     'maint_image_cleanup_desc' => "Сканує вміст сторінки та версій, щоб перевірити, які зображення та малюнки в даний час використовуються, а також які зображення зайві. Переконайтеся, що ви створили повну резервну копію бази даних та зображення, перш ніж запускати це.",
-    'maint_image_cleanup_ignore_revisions' => 'Ігнорувати зображення в версіях',
+    'maint_delete_images_only_in_revisions' => 'Also delete images that only exist in old page revisions',
     'maint_image_cleanup_run' => 'Запустити очищення',
     'maint_image_cleanup_warning' => ':count потенційно невикористаних зображень було знайдено. Ви впевнені, що хочете видалити ці зображення?',
     'maint_image_cleanup_success' => ':count потенційно невикористані зображення знайдено і видалено!',
@@ -80,20 +80,41 @@ return [
     'maint_send_test_email_mail_subject' => 'Перевірка електронної пошти',
     'maint_send_test_email_mail_greeting' => 'Доставляння електронної пошти працює!',
     'maint_send_test_email_mail_text' => 'Вітаємо! Оскільки ви отримали цього листа, поштова скринька налаштована правильно.',
+    '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',
-    'audit_desc' => 'This audit log displays a list of activities tracked in the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.',
-    'audit_event_filter' => 'Event Filter',
-    'audit_event_filter_no_filter' => 'No Filter',
-    'audit_deleted_item' => 'Deleted Item',
-    'audit_deleted_item_name' => 'Name: :name',
-    'audit_table_user' => 'User',
-    'audit_table_event' => 'Event',
-    'audit_table_item' => 'Related Item',
-    'audit_table_date' => 'Activity Date',
-    'audit_date_from' => 'Date Range From',
-    'audit_date_to' => 'Date Range To',
+    'audit' => 'Журнал аудиту',
+    'audit_desc' => 'Цей журнал аудиту показує список відстежуваних у системі дій. Цей список нефільтрований, на відміну від подібних списків активності в системі, де застосовуються фільтри дозволів.',
+    'audit_event_filter' => 'Фільтр подій',
+    'audit_event_filter_no_filter' => 'Без фільтра',
+    'audit_deleted_item' => 'Видалений елемент',
+    'audit_deleted_item_name' => 'Назва: :name',
+    'audit_table_user' => 'Користувач',
+    'audit_table_event' => 'Подія',
+    'audit_table_related' => 'Related Item or Detail',
+    'audit_table_date' => 'Дата активності',
+    'audit_date_from' => 'Діапазон дат від',
+    'audit_date_to' => 'Діапазон дат до',
 
     // Role Settings
     'roles' => 'Ролі',
@@ -117,10 +138,10 @@ return [
     'role_manage_entity_permissions' => 'Керування всіма правами на книги, розділи та сторінки',
     'role_manage_own_entity_permissions' => 'Керування дозволами на власну книгу, розділ та сторінки',
     'role_manage_page_templates' => 'Управління шаблонами сторінок',
-    'role_access_api' => 'Access system API',
+    'role_access_api' => 'Доступ до системного API',
     'role_manage_settings' => 'Керування налаштуваннями програми',
     'role_asset' => 'Дозволи',
-    'roles_system_warning' => 'Be aware that access to any of the above three permissions can allow a user to alter their own privileges or the privileges of others in the system. Only assign roles with these permissions to trusted users.',
+    'roles_system_warning' => 'Майте на увазі, що доступ до будь-якого з вищезазначених трьох дозволів може дозволити користувачеві змінювати власні привілеї або привілеї інших в системі. Ролі з цими дозволами призначайте лише довіреним користувачам.',
     'role_asset_desc' => 'Ці дозволи контролюють стандартні доступи всередині системи. Права на книги, розділи та сторінки перевизначать ці дозволи.',
     'role_asset_admins' => 'Адміністратори автоматично отримують доступ до всього вмісту, але ці параметри можуть відображати або приховувати параметри інтерфейсу користувача.',
     'role_all' => 'Все',
@@ -136,6 +157,7 @@ return [
     'user_profile' => 'Профіль користувача',
     'users_add_new' => 'Додати нового користувача',
     'users_search' => 'Пошук користувачів',
+    'users_latest_activity' => 'Latest Activity',
     'users_details' => 'Відомості про користувача',
     'users_details_desc' => 'Встановіть ім\'я та електронну адресу для цього користувача. Адреса електронної пошти буде використана для входу до програми.',
     'users_details_desc_no_email' => 'Встановіть ім\'я для цього користувача, щоб інші могли його розпізнати.',
@@ -146,14 +168,17 @@ return [
     'users_send_invite_text' => 'Ви можете надіслати цьому користувачеві лист із запрошенням, що дозволить йому встановити пароль власноруч, або ви можете встановити йому пароль самостійно.',
     'users_send_invite_option' => 'Надіслати листа із запрошенням користувачу',
     'users_external_auth_id' => 'Зовнішній ID автентифікації',
-    'users_external_auth_id_desc' => 'This is the ID used to match this user when communicating with your external authentication system.',
+    'users_external_auth_id_desc' => 'Цей ідентифікатор використовується для ідентифікації цього користувача під час взаємодії із зовнішньою системою автентифікації.',
     'users_password_warning' => 'Тільки якщо ви хочете змінити свій пароль, заповніть поля нижче:',
     'users_system_public' => 'Цей користувач представляє будь-яких гостьових користувачів, які відвідують ваш екземпляр. Його не можна використовувати для входу, але він призначається автоматично.',
     'users_delete' => 'Видалити користувача',
     'users_delete_named' => 'Видалити користувача :userName',
     'users_delete_warning' => 'Це повне видалення цього користувача з ім\'ям \':userName\' з системи.',
     'users_delete_confirm' => 'Ви впевнені, що хочете видалити цього користувача?',
-    'users_delete_success' => 'Користувачі успішно видалені',
+    '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' => 'Редагувати користувача',
     'users_edit_profile' => 'Редагувати профіль',
     'users_edit_success' => 'Користувача успішно оновлено',
@@ -167,32 +192,32 @@ return [
     'users_social_disconnect' => 'Від\'єднати обліковий запис',
     'users_social_connected' => 'Обліковий запис :socialAccount успішно додано до вашого профілю.',
     'users_social_disconnected' => 'Обліковий запис :socialAccount був успішно відключений від вашого профілю.',
-    'users_api_tokens' => 'API Tokens',
-    'users_api_tokens_none' => 'No API tokens have been created for this user',
-    'users_api_tokens_create' => 'Create Token',
-    'users_api_tokens_expires' => 'Expires',
-    'users_api_tokens_docs' => 'API Documentation',
+    'users_api_tokens' => 'API токени',
+    'users_api_tokens_none' => 'Жодного токена API не створено для цього користувача',
+    'users_api_tokens_create' => 'Створити токен',
+    'users_api_tokens_expires' => 'Закінчується',
+    'users_api_tokens_docs' => 'Документація API',
 
     // API Tokens
-    'user_api_token_create' => 'Create API Token',
-    'user_api_token_name' => 'Name',
-    'user_api_token_name_desc' => 'Give your token a readable name as a future reminder of its intended purpose.',
-    'user_api_token_expiry' => 'Expiry Date',
-    'user_api_token_expiry_desc' => 'Set a date at which this token expires. After this date, requests made using this token will no longer work. Leaving this field blank will set an expiry 100 years into the future.',
-    'user_api_token_create_secret_message' => 'Immediately after creating this token a "Token ID" & "Token Secret" will be generated and displayed. The secret will only be shown a single time so be sure to copy the value to somewhere safe and secure before proceeding.',
-    'user_api_token_create_success' => 'API token successfully created',
-    'user_api_token_update_success' => 'API token successfully updated',
-    'user_api_token' => 'API Token',
-    'user_api_token_id' => 'Token ID',
-    'user_api_token_id_desc' => 'This is a non-editable system generated identifier for this token which will need to be provided in API requests.',
-    'user_api_token_secret' => 'Token Secret',
-    'user_api_token_secret_desc' => 'This is a system generated secret for this token which will need to be provided in API requests. This will only be displayed this one time so copy this value to somewhere safe and secure.',
-    'user_api_token_created' => 'Token created :timeAgo',
-    'user_api_token_updated' => 'Token updated :timeAgo',
-    'user_api_token_delete' => 'Delete Token',
-    'user_api_token_delete_warning' => 'This will fully delete this API token with the name \':tokenName\' from the system.',
-    'user_api_token_delete_confirm' => 'Are you sure you want to delete this API token?',
-    'user_api_token_delete_success' => 'API token successfully deleted',
+    'user_api_token_create' => 'Створити токен API',
+    'user_api_token_name' => 'Назва',
+    'user_api_token_name_desc' => 'Дайте своєму токену читабельну назву як майбутнє нагадування про його пряме призначення.',
+    'user_api_token_expiry' => 'Дата закінчення',
+    'user_api_token_expiry_desc' => 'Встановіть дату закінчення терміну дії цього токена. Після цієї дати запити, зроблені за допомогою цього токена, більше не працюватимуть. Якщо залишити це поле порожнім, термін дії токена закінчиться через 100 років.',
+    'user_api_token_create_secret_message' => 'Відразу після створення цього токена буде створено та показано «Ідентифікатор токена» та «Ключ токена». Ключ буде показано лише один раз, тому перед тим, як продовжити, не забудьте скопіювати значення ключа в надійне та безпечне місце.',
+    'user_api_token_create_success' => 'Токен API успішно створено',
+    'user_api_token_update_success' => 'Токен API успішно оновлено',
+    'user_api_token' => 'Токен API',
+    'user_api_token_id' => 'Ідентифікатор (ID) токена',
+    'user_api_token_id_desc' => 'Системний ідентифікатор цього токена, який потрібно буде вказати в запитах API. Його редагування неможливе.',
+    'user_api_token_secret' => 'Ключ токена',
+    'user_api_token_secret_desc' => 'Це ключ, згенерований системою для цього токена, його потрібно буде надати в запитах API. Він буде видимий лише цього разу, тому скопіюйте це значення в безпечне та надійне місце.',
+    'user_api_token_created' => 'Токен створено :timeAgo',
+    'user_api_token_updated' => 'Токен оновлено :timeAgo',
+    'user_api_token_delete' => 'Видалити токен',
+    'user_api_token_delete_warning' => 'Ця дія повністю видалить цей токен API із назвою \':tokenName\' з системи.',
+    'user_api_token_delete_confirm' => 'Дійсно хочете видалити цей токен API?',
+    'user_api_token_delete_success' => 'Токен API успішно видалено',
 
     //! If editing translations files directly please ignore this in all
     //! languages apart from en. Content will be auto-copied from en.
@@ -214,6 +239,7 @@ return [
         'ja' => '日本語',
         'ko' => '한국어',
         'nl' => 'Nederlands',
+        'nb' => 'Norsk (Bokmål)',
         'pl' => 'Polski',
         'pt_BR' => 'Português do Brasil',
         'ru' => 'Русский',
index 47f15fe7fcbdb5a63be8889665695b9574662683..51b9a09990ff024bf83d2dac2edab6ccce92f05d 100644 (file)
@@ -90,6 +90,7 @@ return [
     'required_without'     => 'Поле :attribute є обов\'язковим для заповнення, коли :values не вказано.',
     'required_without_all' => 'Поле :attribute є обов\'язковим для заповнення, коли :values не вказано.',
     'same'                 => 'Поля :attribute та :other мають збігатися.',
+    'safe_url'             => 'The provided link may not be safe.',
     'size'                 => [
         'numeric' => 'Поле :attribute має бути довжини :size.',
         'file'    => 'Файл в полі :attribute має бути розміром :size кілобайт.',
index 19fae850a4ffd73aa36f8086be79167bb5ee0d21..42b34f38379f3dc98dcee3c9c64c1d9b57e317b0 100644 (file)
@@ -45,4 +45,5 @@ return [
 
     // Other
     'commented_on'                => 'đã bình luận về',
+    'permissions_update'          => 'updated permissions',
 ];
index bd13572cb884d7d5570a62f40ca24a87800f41dd..5ba2db390f6280cc791f345003109c33968a8313 100644 (file)
@@ -43,7 +43,7 @@ return [
     'reset_password' => 'Đặt lại mật khẩu',
     'reset_password_send_instructions' => 'Nhập email vào ô dưới đây và bạn sẽ nhận được một email với liên kết để đặt lại mật khẩu.',
     'reset_password_send_button' => 'Gửi liên kết đặt lại mật khẩu',
-    'reset_password_sent' => 'A password reset link will be sent to :email if that email address is found in the system.',
+    'reset_password_sent' => 'Một đường dẫn đặt lại mật khẩu sẽ được gửi tới :email nếu địa chỉ email đó tồn tại trong hệ thống.',
     'reset_password_success' => 'Mật khẩu đã được đặt lại thành công.',
     'email_reset_subject' => 'Đặt lại mật khẩu của :appName',
     'email_reset_text' => 'Bạn nhận được email này bởi vì chúng tôi nhận được một yêu cầu đặt lại mật khẩu cho tài khoản của bạn.',
index 5106e276970b48fa6875f0c8931da8fa2fb1c20c..d1dc8a0489fa3cd425d44f183ba4ef6587c2e11d 100644 (file)
@@ -33,7 +33,7 @@ return [
     'copy' => 'Sao chép',
     'reply' => 'Trả lời',
     'delete' => 'Xóa',
-    'delete_confirm' => 'Confirm Deletion',
+    'delete_confirm' => 'Xác nhận Xóa',
     'search' => 'Tìm kiếm',
     'search_clear' => 'Xoá tìm kiếm',
     'reset' => 'Thiết lập lại',
@@ -67,8 +67,8 @@ return [
     'profile_menu' => 'Menu Hồ sơ',
     'view_profile' => 'Xem Hồ sơ',
     'edit_profile' => 'Sửa Hồ sơ',
-    'dark_mode' => 'Dark Mode',
-    'light_mode' => 'Light Mode',
+    'dark_mode' => 'Chế độ Tối',
+    'light_mode' => 'Chế độ Sáng',
 
     // Layout tabs
     'tab_info' => 'Thông tin',
index cdcce776d07192f2a47865a44cd0ac88fe9b890c..8a061625cd6128ad48e34c76439b368b4c4b2de5 100644 (file)
@@ -15,7 +15,7 @@ return [
     'image_load_more' => 'Hiện thêm',
     'image_image_name' => 'Tên Ảnh',
     'image_delete_used' => 'Ảnh này được sử dụng trong các trang dưới đây.',
-    'image_delete_confirm_text' => 'Are you sure you want to delete this image?',
+    'image_delete_confirm_text' => 'Bạn có chắc chắn muốn xóa hình ảnh này?',
     'image_select_image' => 'Chọn Ảnh',
     'image_dropzone' => 'Thả các ảnh hoặc bấm vào đây để tải lên',
     'images_deleted' => 'Các ảnh đã được xóa',
@@ -29,6 +29,6 @@ return [
     'code_editor' => 'Sửa Mã',
     'code_language' => 'Ngôn ngữ Mã',
     'code_content' => 'Nội dung Mã',
-    'code_session_history' => 'Session History',
+    'code_session_history' => 'Lịch sử Phiên',
     'code_save' => 'Lưu Mã',
 ];
index be16dd72b92ae4361663b2a46c6dbf81e3eb0483..158bdad18efc44e1a136603a0421805786a79238 100644 (file)
@@ -22,6 +22,7 @@ return [
     'meta_created_name' => 'Được tạo :timeLength bởi :user',
     'meta_updated' => 'Được cập nhật :timeLength',
     'meta_updated_name' => 'Được cập nhật :timeLength bởi :user',
+    'meta_owned_name' => 'Owned by :user',
     'entity_select' => 'Chọn thực thể',
     'images' => 'Ảnh',
     'my_recent_drafts' => 'Bản nháp gần đây của tôi',
@@ -39,6 +40,7 @@ return [
     'permissions_intro' => 'Một khi được bật, các quyền này sẽ được ưu tiên trên hết tất cả các quyền hạn khác.',
     'permissions_enable' => 'Bật quyền hạn tùy chỉnh',
     'permissions_save' => 'Lưu quyền hạn',
+    'permissions_owner' => 'Owner',
 
     // Search
     'search_results' => 'Kết quả Tìm kiếm',
@@ -47,8 +49,8 @@ return [
     'search_no_pages' => 'Không trang nào khớp với tìm kiếm này',
     'search_for_term' => 'Tìm kiếm cho :term',
     'search_more' => 'Thêm kết quả',
-    'search_advanced' => 'Advanced Search',
-    'search_terms' => 'Search Terms',
+    'search_advanced' => 'Tìm kiếm Nâng cao',
+    'search_terms' => 'Cụm từ Tìm kiếm',
     'search_content_type' => 'Kiểu Nội dung',
     'search_exact_matches' => 'Hoàn toàn trùng khớp',
     'search_tags' => 'Tìm kiếm Tag',
@@ -146,7 +148,7 @@ return [
     'chapters_create' => 'Tạo Chương mới',
     'chapters_delete' => 'Xóa Chương',
     'chapters_delete_named' => 'Xóa Chương :chapterName',
-    'chapters_delete_explain' => 'Chức năng này sẽ xóa chương với tên \':chapterName\'. Tất cả các trang sẽ bị loại bỏ và thêm trực tiếp vào sách chứa nó.',
+    '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' => 'Bạn có chắc chắn muốn xóa chương này?',
     'chapters_edit' => 'Sửa Chương',
     'chapters_edit_named' => 'Sửa chương :chapterName',
@@ -208,6 +210,7 @@ return [
     'pages_revisions' => 'Phiên bản Trang',
     'pages_revisions_named' => 'Phiên bản Trang cho :pageName',
     'pages_revision_named' => 'Phiên bản Trang cho :pageName',
+    'pages_revision_restored_from' => 'Restored from #:id; :summary',
     'pages_revisions_created_by' => 'Tạo bởi',
     'pages_revisions_date' => 'Ngày của Phiên bản',
     'pages_revisions_number' => '#',
@@ -256,7 +259,7 @@ return [
     'attachments_upload' => 'Tải lên Tập tin',
     'attachments_link' => 'Đính kèm Liên kết',
     'attachments_set_link' => 'Đặt Liên kết',
-    'attachments_delete' => 'Are you sure you want to delete this attachment?',
+    'attachments_delete' => 'Bạn có chắc chắn muốn xóa tập tin đính kèm này?',
     'attachments_dropzone' => 'Thả các tập tin hoặc bấm vào đây để đính kèm một tập tin',
     'attachments_no_files' => 'Không có tập tin nào được tải lên',
     'attachments_explain_link' => 'Bạn có thể đính kèm một liên kết nếu bạn lựa chọn không tải lên tập tin. Liên kết này có thể trỏ đến một trang khác hoặc một tập tin ở trên mạng (đám mây).',
@@ -265,7 +268,7 @@ return [
     'attachments_link_url' => 'Liên kết đến tập tin',
     'attachments_link_url_hint' => 'URL của trang hoặc tập tin',
     'attach' => 'Đính kèm',
-    'attachments_insert_link' => 'Add Attachment Link to Page',
+    'attachments_insert_link' => 'Thêm Đường dẫn Tập tin đính kèm vào Trang',
     'attachments_edit_file' => 'Sửa tập tin',
     'attachments_edit_file_name' => 'Tên tệp tin',
     'attachments_edit_drop_upload' => 'Thả tập tin hoặc bấm vào đây để tải lên và ghi đè',
index 6caf0e9904ef244cd6f536b9ec29f3268660f391..6410fb97092748f0b91064b4b91d49d5d270e714 100644 (file)
@@ -82,7 +82,7 @@ return [
     // Error pages
     '404_page_not_found' => 'Không Tìm Thấy Trang',
     'sorry_page_not_found' => 'Xin lỗi, Không tìm thấy trang bạn đang tìm kiếm.',
-    'sorry_page_not_found_permission_warning' => 'If you expected this page to exist, you might not have permission to view it.',
+    'sorry_page_not_found_permission_warning' => 'Nếu trang bạn tìm kiếm tồn tại, có thể bạn đang không có quyền truy cập.',
     'return_home' => 'Quay lại trang chủ',
     'error_occurred' => 'Đã xảy ra lỗi',
     'app_down' => ':appName hiện đang ngoại tuyến',
index 21242e003a3ec3dbe7b75aab91f58d581b323222..65b42b4d80fdd300169c5d0a940d7830e3399b85 100644 (file)
@@ -8,7 +8,7 @@ return [
 
     'password' => 'Mật khẩu phải có tối thiểu 8 ký tự và và phải trùng với mật khẩu xác nhận.',
     'user' => "Chúng tôi không tìm thấy người dùng với địa chỉ email đó.",
-    'token' => 'The password reset token is invalid for this email address.',
+    'token' => 'Mã token đặt lại mật khẩu cho địa chỉ email này không hợp lệ.',
     'sent' => 'Chúng tôi đã gửi email chứa liên kết đặt lại mật khẩu cho bạn!',
     'reset' => 'Mật khẩu của bạn đã được đặt lại!',
 
index 5d35ff4399cf10c1748c17d8aa1c4c90a85db200..67fcb1285bbf47f49716f771aa17e113a055d216 100644 (file)
@@ -68,7 +68,7 @@ return [
     'maint' => 'Bảo trì',
     'maint_image_cleanup' => 'Dọn dẹp ảnh',
     'maint_image_cleanup_desc' => "Quét nội dung trang và phiên bản để kiểm tra xem các ảnh và hình vẽ nào đang được sử dụng và ảnh nào dư thừa. Đảm bảo rằng bạn đã tạo bản sao lưu toàn dữ liệu và ảnh trước khi chạy chức năng này.",
-    'maint_image_cleanup_ignore_revisions' => 'Bỏ qua ảnh trong phiên bản chỉnh sửa',
+    'maint_delete_images_only_in_revisions' => 'Also delete images that only exist in old page revisions',
     'maint_image_cleanup_run' => 'Chạy Dọn dẹp',
     'maint_image_cleanup_warning' => 'Đã tìm thấy :count ảnh có thể không được sử dụng. Bạn muốn chắc rằng muốn xóa các ảnh này?',
     'maint_image_cleanup_success' => ':count ảnh có thể không được sử dụng đã được tìm thấy và xóa!',
@@ -80,6 +80,27 @@ return [
     'maint_send_test_email_mail_subject' => 'Thử Email',
     'maint_send_test_email_mail_greeting' => 'Chức năng gửi email có vẻ đã hoạt động!',
     'maint_send_test_email_mail_text' => 'Chúc mừng! Khi bạn nhận được email thông báo này, cài đặt email của bạn có vẻ đã được cấu hình đúng.',
+    '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' => 'Mở Thùng Rác',
+
+    // Recycle Bin
+    'recycle_bin' => 'Thùng Rác',
+    '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' => 'Xóa Bởi',
+    'recycle_bin_deleted_at' => 'Thời điểm Xóa',
+    'recycle_bin_permanently_delete' => 'Xóa Vĩnh viễn',
+    'recycle_bin_restore' => 'Khôi phục',
+    'recycle_bin_contents_empty' => 'The recycle bin is currently empty',
+    'recycle_bin_empty' => 'Dọn dẹp Thùng Rác',
+    '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',
@@ -88,9 +109,9 @@ return [
     'audit_event_filter_no_filter' => 'No Filter',
     'audit_deleted_item' => 'Deleted Item',
     'audit_deleted_item_name' => 'Name: :name',
-    'audit_table_user' => 'User',
+    'audit_table_user' => 'Người dùng',
     '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' => 'Hồ sơ người dùng',
     'users_add_new' => 'Thêm người dùng mới',
     'users_search' => 'Tìm kiếm người dùng',
+    'users_latest_activity' => 'Latest Activity',
     'users_details' => 'Chi tiết người dùng',
     'users_details_desc' => 'Hiển thị tên và địa chỉ email cho người dùng này. Địa chỉ email sẽ được sử dụng để đăng nhập vào ứng dụng.',
     'users_details_desc_no_email' => 'Đặt tên cho người dùng này để giúp người dùng khác nhận ra họ.',
@@ -153,7 +175,10 @@ return [
     'users_delete_named' => 'Xóa người dùng :userName',
     'users_delete_warning' => 'Chức năng này sẽ hoàn toàn xóa người dùng với tên \':userName\' từ hệ thống.',
     'users_delete_confirm' => 'Bạn có chắc muốn xóa người dùng này không?',
-    'users_delete_success' => 'Người dùng đã được xóa thành công',
+    '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' => 'Sửa người dùng',
     'users_edit_profile' => 'Sửa Hồ sơ',
     'users_edit_success' => 'Người dùng được cập nhật thành công',
@@ -214,6 +239,7 @@ return [
         'ja' => '日本語',
         'ko' => '한국어',
         'nl' => 'Nederlands',
+        'nb' => 'Norsk (Bokmål)',
         'pl' => 'Polski',
         'pt_BR' => 'Português do Brasil',
         'ru' => 'Русский',
index 030885408be647e4cf5004edbdf798c196f74da4..c0020717506345e699dc887ea2d99d32e522a945 100644 (file)
@@ -90,6 +90,7 @@ return [
     'required_without'     => 'Trường :attribute là bắt buộc khi :values không tồn tại.',
     'required_without_all' => 'Trường :attribute là bắt buộc khi không có bất cứ :values nào tồn tại.',
     'same'                 => ':attribute và :other phải trùng khớp với nhau.',
+    'safe_url'             => 'Đường dẫn cung cấp có thể không an toàn.',
     'size'                 => [
         'numeric' => ':attribute phải có cỡ :size.',
         'file'    => ':attribute phải có cỡ :size KB.',
index 676a1dd92db0c0c2556ae858b6a0ada0c236b472..717c7dfdf98a7bbd4b292201a4274bf49cd746ea 100644 (file)
@@ -45,4 +45,5 @@ return [
 
     // Other
     'commented_on'                => '评论',
+    'permissions_update'          => '权限已更新',
 ];
index 387a5b2902469873e8dad8ccdb400f3907a84148..986476119fd863bfdf17e847b8310ba78593b460 100644 (file)
@@ -22,6 +22,7 @@ return [
     'meta_created_name' => '由 :user 创建于 :timeLength',
     'meta_updated' => '更新于 :timeLength',
     'meta_updated_name' => '由 :user 更新于 :timeLength',
+    'meta_owned_name' => '拥有者 :user',
     'entity_select' => '实体选择',
     'images' => '图片',
     'my_recent_drafts' => '我最近的草稿',
@@ -39,6 +40,7 @@ return [
     'permissions_intro' => '本设置优先于每个用户角色本身所具有的权限。',
     'permissions_enable' => '启用自定义权限',
     'permissions_save' => '保存权限',
+    'permissions_owner' => '拥有者',
 
     // Search
     'search_results' => '搜索结果',
@@ -146,7 +148,7 @@ return [
     'chapters_create' => '创建章节',
     'chapters_delete' => '删除章节',
     'chapters_delete_named' => '删除章节「:chapterName」',
-    'chapters_delete_explain' => '这将删除章节「:chapterName」。所有的页面将被删除并添加到其所在的书籍。',
+    'chapters_delete_explain' => '这将删除名为“:chapterName”的章节。本章节中存在的所有页面也将被删除。',
     'chapters_delete_confirm' => '您确定要删除此章节吗?',
     'chapters_edit' => '编辑章节',
     'chapters_edit_named' => '编辑章节「:chapterName」',
@@ -208,6 +210,7 @@ return [
     'pages_revisions' => '页面修订',
     'pages_revisions_named' => '“:pageName”页面修订',
     'pages_revision_named' => '“:pageName”页面修订',
+    'pages_revision_restored_from' => '从 #:id; :summary 恢复',
     'pages_revisions_created_by' => '创建者',
     'pages_revisions_date' => '修订日期',
     'pages_revisions_number' => '#',
@@ -305,7 +308,7 @@ return [
     'comment_deleted_success' => '评论已删除',
     'comment_created_success' => '评论已添加',
     'comment_updated_success' => '评论已更新',
-    'comment_delete_confirm' => '确定要删除这条评论?',
+    'comment_delete_confirm' => '确定要删除这条评论?',
     'comment_in_reply_to' => '回复 :commentId',
 
     // Revision
index 3467b49a4267a82706d75e6bf9add60279cc1397..5d58ace2c6c9a33d26313934fbda4a45f4fd4b08 100755 (executable)
@@ -68,7 +68,7 @@ return [
     'maint' => '维护',
     'maint_image_cleanup' => '清理图像',
     'maint_image_cleanup_desc' => "扫描页面和修订内容以检查哪些图像是正在使用的以及哪些图像是多余的。确保在运行前创建完整的数据库和映像备份。",
-    'maint_image_cleanup_ignore_revisions' => '忽略修订记录中的图像',
+    'maint_delete_images_only_in_revisions' => '同时删除只存在于旧页面版本中的图片',
     'maint_image_cleanup_run' => '运行清理',
     'maint_image_cleanup_warning' => '发现了 :count 张可能未使用的图像。您确定要删除这些图像吗?',
     'maint_image_cleanup_success' => '找到并删除了 :count 张可能未使用的图像!',
@@ -79,18 +79,39 @@ return [
     'maint_send_test_email_success' => '电子邮件已发送至 :address',
     'maint_send_test_email_mail_subject' => '测试电子邮件',
     'maint_send_test_email_mail_greeting' => '邮件发送功能看起来工作正常!',
-    'maint_send_test_email_mail_text' => '恭喜!您收到了此邮件通知,你的电子邮件设置看起来配置正确。',
+    'maint_send_test_email_mail_text' => '恭喜!您收到了此邮件通知,您的电子邮件设置看起来已配置正确。',
+    'maint_recycle_bin_desc' => '被删除的书架、书籍、章节和页面会被存入回收站,您可以还原或永久删除它们。回收站中较旧的项目可能会在系统设置的一段时间后被自动删除。',
+    'maint_recycle_bin_open' => '打开回收站',
+
+    // Recycle Bin
+    'recycle_bin' => '回收站',
+    'recycle_bin_desc' => '在这里,您可以还原已删除的项目,或选择将其从系统中永久删除。与系统中过滤过的类似的活动记录不同,这个表会显示所有操作。',
+    'recycle_bin_deleted_item' => '被删除的项目',
+    'recycle_bin_deleted_by' => '删除者',
+    'recycle_bin_deleted_at' => '删除时间',
+    'recycle_bin_permanently_delete' => '永久删除',
+    'recycle_bin_restore' => '恢复',
+    'recycle_bin_contents_empty' => '回收站当前为空',
+    'recycle_bin_empty' => '清空回收站',
+    'recycle_bin_empty_confirm' => '这将永久性销毁回收站中的所有项目(包括每个项目中包含的内容,例如图片)。您确定要清空回收站吗?',
+    'recycle_bin_destroy_confirm' => '此操作将从系统中永久删除此项目以及下面列出的所有子元素,并且您将无法还原此内容。您确定要永久删除该项目吗?',
+    'recycle_bin_destroy_list' => '要销毁的项目',
+    'recycle_bin_restore_list' => '要恢复的项目',
+    'recycle_bin_restore_confirm' => '此操作会将已删除的项目及其所有子元素恢复到原始位置。如果项目的原始位置已被删除,并且现在位于回收站中,则要恢复项目的上级项目也需要恢复。',
+    'recycle_bin_restore_deleted_parent' => '该项目的上级项目也已被删除。这些项目将保持被删除状态,直到上级项目被恢复。',
+    'recycle_bin_destroy_notification' => '从回收站中删除了 :count 个项目。',
+    'recycle_bin_restore_notification' => '从回收站中恢复了 :count 个项目。',
 
     // Audit Log
     'audit' => '审核日志',
-    'audit_desc' => '该审核æ\97¥å¿\97æ\98¾ç¤ºç³»ç»\9f中è·\9f踪ç\9a\84æ´»å\8a¨å\88\97表ã\80\82ä¸\8eç³»ç»\9f中åº\94ç\94¨äº\86æ\9d\83é\99\90è¿\87滤å\99¨ç\9a\84类似活å\8a¨å\88\97表ä¸\8då\90\8cï¼\8cè¿\99个表æ\98¯æ\9cªç»\8fè¿\87滤ç\9a\84。',
+    'audit_desc' => 'è¿\99份审核æ\97¥å¿\97æ\98¾ç¤ºæ\89\80æ\9c\89被系ç»\9fè·\9f踪ç\9a\84æ´»å\8a¨ã\80\82ä¸\8eç³»ç»\9f中è¿\87滤è¿\87ç\9a\84类似ç\9a\84æ´»å\8a¨è®°å½\95ä¸\8då\90\8cï¼\8cè¿\99个表ä¼\9aæ\98¾ç¤ºæ\89\80æ\9c\89æ\93\8dä½\9c。',
     'audit_event_filter' => '事件过滤器',
     'audit_event_filter_no_filter' => '无过滤器',
     'audit_deleted_item' => '被删除的项目',
-    'audit_deleted_item_name' => 'å§\93å\90\8d: :name',
+    'audit_deleted_item_name' => 'å\90\8dç§°: :name',
     'audit_table_user' => '用户',
     'audit_table_event' => '事件',
-    'audit_table_item' => '相关项目',
+    'audit_table_related' => '相关项目或详细信息',
     'audit_table_date' => '活动日期',
     'audit_date_from' => '日期范围从',
     'audit_date_to' => '日期范围至',
@@ -136,6 +157,7 @@ return [
     'user_profile' => '用户资料',
     'users_add_new' => '添加用户',
     'users_search' => '搜索用户',
+    'users_latest_activity' => '最新活动',
     'users_details' => '用户详细资料',
     'users_details_desc' => '设置该用户的显示名称和电子邮件地址。 该电子邮件地址将用于登录本站。',
     'users_details_desc_no_email' => '设置此用户的昵称,以便其他人识别。',
@@ -153,7 +175,10 @@ return [
     'users_delete_named' => '删除用户 :userName',
     'users_delete_warning' => '这将从系统中完全删除名为 \':userName\' 的用户。',
     'users_delete_confirm' => '您确定要删除这个用户?',
-    'users_delete_success' => '用户删除成功。',
+    'users_migrate_ownership' => '迁移拥有权',
+    'users_migrate_ownership_desc' => '如果您想要当前用户拥有的全部项目转移到另一个用户(更改拥有者),请在此处选择一个用户。',
+    'users_none_selected' => '没有选中用户',
+    'users_delete_success' => '已成功移除用户',
     'users_edit' => '编辑用户',
     'users_edit_profile' => '编辑资料',
     'users_edit_success' => '用户更新成功',
@@ -214,6 +239,7 @@ return [
         'ja' => '日本語',
         'ko' => '한국어',
         'nl' => 'Nederlands',
+        'nb' => '挪威语 (Bokmål)',
         'pl' => 'Polski',
         'pt_BR' => 'Português do Brasil',
         'ru' => 'Русский',
index e0bc55523883715bec8cddee26907f364979445d..8bb8a207a00099893bf52e37d37ab3ffc4da83b9 100644 (file)
@@ -90,6 +90,7 @@ return [
     'required_without'     => '当:values不存在时,:attribute 字段是必需的。',
     'required_without_all' => '当:values均不存在时,:attribute 字段是必需的。',
     'same'                 => ':attribute 与 :other 必须匹配。',
+    'safe_url'             => '提供的链接可能不安全。',
     'size'                 => [
         'numeric' => ':attribute 必须为:size。',
         'file'    => ':attribute 必须为:size KB。',
index fab4dd20e62b2706793c643f335e8f99c5d630fd..dedcdc0cf0cd469a6ea977b2d7f53c400e3527cc 100644 (file)
@@ -45,4 +45,5 @@ return [
 
     // Other
     'commented_on'                => '評論',
+    'permissions_update'          => '更新權限',
 ];
index bc08ee32a8be82573bfc91baf10fe9fabcef9acd..ca98df0d4d321734edc9d96ca6eb186fcc514df4 100644 (file)
@@ -22,6 +22,7 @@ return [
     'meta_created_name' => '由 :user 建立於 :timeLength',
     'meta_updated' => '更新於 :timeLength',
     'meta_updated_name' => '由 :user 更新於 :timeLength',
+    'meta_owned_name' => ':user 所擁有',
     'entity_select' => '選擇項目',
     'images' => '圖片',
     'my_recent_drafts' => '我最近的草稿',
@@ -39,6 +40,7 @@ return [
     'permissions_intro' => '本設定優先權高於每個使用者角色本身所具有的權限。',
     'permissions_enable' => '啟用自訂權限',
     'permissions_save' => '儲存權限',
+    'permissions_owner' => '擁有者',
 
     // Search
     'search_results' => '搜尋結果',
@@ -146,7 +148,7 @@ return [
     'chapters_create' => '建立章節',
     'chapters_delete' => '刪除章節',
     'chapters_delete_named' => '刪除章節「:chapterName」',
-    'chapters_delete_explain' => '這將刪除章節「:chapterName」。所有的頁面將被刪除並加入到其所在的書籍。',
+    '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' => '您確定要刪除此章節嗎?',
     'chapters_edit' => '編輯章節',
     'chapters_edit_named' => '編輯章節「:chapterName」',
@@ -208,13 +210,14 @@ return [
     'pages_revisions' => '頁面修訂',
     'pages_revisions_named' => '“:pageName”頁面修訂',
     'pages_revision_named' => '“:pageName”頁面修訂',
+    'pages_revision_restored_from' => 'Restored from #:id; :summary',
     'pages_revisions_created_by' => '建立者',
     'pages_revisions_date' => '修訂日期',
     'pages_revisions_number' => '#',
     'pages_revisions_numbered' => '修訂編號:id',
     'pages_revisions_numbered_changes' => '修訂編號:id 更改',
     'pages_revisions_changelog' => '更新說明',
-    'pages_revisions_changes' => '說明',
+    'pages_revisions_changes' => '更新紀錄',
     'pages_revisions_current' => '目前版本',
     'pages_revisions_preview' => '預覽',
     'pages_revisions_restore' => '恢複',
@@ -265,7 +268,7 @@ return [
     'attachments_link_url' => '連結到檔案',
     'attachments_link_url_hint' => '網站或檔案的網址',
     'attach' => '附加',
-    'attachments_insert_link' => 'Add Attachment Link to Page',
+    'attachments_insert_link' => '將附件連結增加到頁面',
     'attachments_edit_file' => '編輯檔案',
     'attachments_edit_file_name' => '檔案名稱',
     'attachments_edit_drop_upload' => '刪除檔案或點選這裡上傳並覆蓋',
index b135cb08623dc876be645dba91bb4dc4bd4e10bc..50ef79cbb55b25647d51563e8e46cea0d62b85b0 100644 (file)
@@ -68,7 +68,7 @@ return [
     'maint' => '維護',
     'maint_image_cleanup' => '清理圖像',
     'maint_image_cleanup_desc' => "掃描頁面和修訂內容以檢查哪些圖像是正在使用的以及哪些圖像是多余的。確保在運行前創建完整的數據庫和映像備份。",
-    'maint_image_cleanup_ignore_revisions' => '忽略修訂記錄中的圖像',
+    'maint_delete_images_only_in_revisions' => '包含刪除僅在舊頁面修訂版中存在的圖像',
     'maint_image_cleanup_run' => '運行清理',
     'maint_image_cleanup_warning' => '發現了:count 張可能未使用的圖像。您確定要刪除這些圖像嗎?',
     'maint_image_cleanup_success' => '找到並刪除了:count 張可能未使用的圖像!',
@@ -80,18 +80,39 @@ return [
     'maint_send_test_email_mail_subject' => '測試郵件',
     'maint_send_test_email_mail_greeting' => '電子郵件傳遞似乎有效!',
     'maint_send_test_email_mail_text' => '恭喜你! 收到此電子郵件通知時,您的電子郵件設置已經認證成功。',
+    'maint_recycle_bin_desc' => '刪除的書架,書籍,章節和頁面將發送到回收站,以便可以還原或永久刪除它們。 回收站中的較舊項目可能會在一段時間後自動刪除,具體取決於系統配置。',
+    'maint_recycle_bin_open' => 'Open 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',
+    'audit' => '稽核記錄',
     'audit_desc' => 'This audit log displays a list of activities tracked in the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.',
     'audit_event_filter' => 'Event Filter',
     'audit_event_filter_no_filter' => 'No Filter',
     'audit_deleted_item' => 'Deleted Item',
     'audit_deleted_item_name' => 'Name: :name',
-    'audit_table_user' => 'User',
-    'audit_table_event' => 'Event',
-    'audit_table_item' => 'Related Item',
-    'audit_table_date' => 'Activity Date',
+    'audit_table_user' => '使用者',
+    'audit_table_event' => '活動',
+    'audit_table_related' => 'Related Item or Detail',
+    'audit_table_date' => '最後活動日期',
     'audit_date_from' => 'Date Range From',
     'audit_date_to' => 'Date Range To',
 
@@ -136,6 +157,7 @@ return [
     'user_profile' => '使用者資料',
     'users_add_new' => '加入使用者',
     'users_search' => '搜尋使用者',
+    'users_latest_activity' => '最新活動',
     'users_details' => '用戶詳情',
     'users_details_desc' => '請設置用戶的顯示名稱和電子郵件地址, 該電子郵件地址將用於登錄該應用。',
     'users_details_desc_no_email' => '設置一個用戶的顯示名稱,以便其他人可以認出你。',
@@ -153,7 +175,10 @@ return [
     'users_delete_named' => '刪除使用者 :userName',
     'users_delete_warning' => '這將從系統中完全刪除名為 \':userName\' 的使用者。',
     'users_delete_confirm' => '您確定要刪除這個使用者?',
-    'users_delete_success' => '使用者刪除成功。',
+    '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' => '沒有選定的使用者',
+    'users_delete_success' => 'User successfully removed',
     'users_edit' => '編輯使用者',
     'users_edit_profile' => '編輯資料',
     'users_edit_success' => '使用者更新成功',
@@ -214,6 +239,7 @@ return [
         'ja' => '日本語',
         'ko' => '한국어',
         'nl' => 'Nederlands',
+        'nb' => 'Norsk (Bokmål)',
         'pl' => 'Polski',
         'pt_BR' => 'Português do Brasil',
         'ru' => 'Русский',
index 82d74e525a506d096d7682b900d6c41c3cd83b35..7a46288ad2664c3e9cd848817d1a169215b5ac0d 100644 (file)
@@ -90,6 +90,7 @@ return [
     'required_without'     => '當:values不存在時,:attribute 字段是必需的。',
     'required_without_all' => '當:values均不存在時,:attribute 字段是必需的。',
     'same'                 => ':attribute 與 :other 必須匹配。',
+    'safe_url'             => 'The provided link may not be safe.',
     'size'                 => [
         'numeric' => ':attribute 必須為:size。',
         'file'    => ':attribute 必須為:size KB。',
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
diff --git a/resources/sass/_footer.scss b/resources/sass/_footer.scss
new file mode 100644 (file)
index 0000000..1c58bcc
--- /dev/null
@@ -0,0 +1,17 @@
+/**
+ * Includes the footer links.
+ */
+
+ footer {
+    flex-shrink: 0;
+    padding: 1rem 1rem 2rem 1rem;
+    text-align: center;
+  }
+  
+  footer a {
+    margin: 0 .5em;
+  }
+  
+  body.flexbox footer {
+    display: none;
+  }
\ No newline at end of file
index e19bb4f612f373461545ff97c4b7078bc5fd31ae..3e8c676fdb08bd2d3e838cb9ef7c86f4a77abcfd 100644 (file)
@@ -3,7 +3,7 @@
  */
 
 header .grid {
-  grid-template-columns: auto min-content auto;
+  grid-template-columns: minmax(max-content, 2fr) 1fr minmax(max-content, 2fr);
 }
 
 @include smaller-than($l) {
@@ -77,9 +77,6 @@ header {
 }
 
 
-.header-search {
-  display: inline-block;
-}
 header .search-box {
   display: inline-block;
   margin-top: 10px;
@@ -269,9 +266,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 +281,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 57869d6520b04aa6a90ea5405c36d8a95de0927f..0262956691a0f4fc978b72f6ac6d56a25637cf15 100644 (file)
@@ -25,4 +25,8 @@ body {
   line-height: 1.6;
   @include lightDark(color, #444, #AAA);
   -webkit-font-smoothing: antialiased;
-}
\ No newline at end of file
+  background-color: #F2F2F2;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+}
index c4e412f0e67a982492b61363c05dab4bf27f0471..c12cae256e6d424dcd3e590c115ecfceca3c20ad 100644 (file)
   }
 }
 
+#content {
+  flex: 1 0 auto;
+}
+
 /**
  * Flexbox layout system
  */
@@ -153,6 +157,9 @@ body.flexbox {
 .justify-center {
   justify-content: center;
 }
+.items-center {
+  align-items: center;
+}
 
 
 /**
@@ -175,6 +182,10 @@ body.flexbox {
   display: none !important;
 }
 
+.fill-height {
+  height: 100%;
+}
+
 .float {
   float: left;
   &.right {
@@ -272,6 +283,7 @@ body.flexbox {
     min-height: 50vh;
     overflow-y: scroll;
     overflow-x: hidden;
+    height: 100%;
     scrollbar-width: none;
     -ms-overflow-style: none;
     &::-webkit-scrollbar {
@@ -350,4 +362,4 @@ body.flexbox {
     margin-inline-start: 0;
     margin-inline-end: 0;
   }
-}
\ No newline at end of file
+}
index f41ebd0c4bf734c5392652f8aa983eae37025bf0..315f979f3efdb2c215d8de56ab66609ca8e11e48 100644 (file)
@@ -28,6 +28,7 @@ table.table {
     padding: $-s $-s;
     vertical-align: middle;
     margin: 0;
+    overflow: visible;
   }
   th {
     font-weight: bold;
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 78d94f977f8d0043679557148fc00af97a8cd276..614b7f295a5b22e6adb8eaf428207267ed5ec3c1 100644 (file)
@@ -15,6 +15,7 @@
 @import "codemirror";
 @import "components";
 @import "header";
+@import "footer";
 @import "lists";
 @import "pages";
 
index a5404a36506ae9d70225034a6eeef749eeb86d28..29e4acee7bf8355f7bbda9b17f240e1483cf001f 100644 (file)
@@ -35,6 +35,8 @@
         @yield('content')
     </div>
 
+    @include('common.footer')
+
     <div back-to-top class="primary-background print-hidden">
         <div class="inner">
             @icon('chevron-up') <span>{{ trans('common.back_to_top') }}</span>
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 f3c3ee34b1ca2e6b16f869346e936c8d9843995a..81fb66cfcd18148a796cd478ed53b43110a4817c 100644 (file)
@@ -36,7 +36,7 @@
     <div class="actions mb-xl">
         <h5>{{ trans('common.actions') }}</h5>
         <div class="icon-list text-primary">
-            @if($currentUser->can('book-create-all'))
+            @if(user()->can('book-create-all'))
                 <a href="{{ url("/create-book") }}" class="icon-list-item">
                     <span>@icon('add')</span>
                     <span>{{ trans('entities.books_create') }}</span>
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>
diff --git a/resources/views/common/footer.blade.php b/resources/views/common/footer.blade.php
new file mode 100644 (file)
index 0000000..67b52a6
--- /dev/null
@@ -0,0 +1,7 @@
+@if(count(setting('app-footer-links', [])) > 0)
+<footer>
+    @foreach(setting('app-footer-links', []) as $link)
+        <a href="{{ $link['url'] }}" target="_blank">{{ strpos($link['label'], 'trans::') === 0 ? trans(str_replace('trans::', '', $link['label'])) : $link['label'] }}</a>
+    @endforeach
+</footer>
+@endif
\ No newline at end of file
index 827abcac601d8d6e4276880085a1a27aadb7a960..43ac273efb648bce16e451e9634bb3dcc260faa7 100644 (file)
@@ -13,7 +13,7 @@
             <div class="mobile-menu-toggle hide-over-l">@icon('more')</div>
         </div>
 
-        <div class="header-search hide-under-l">
+        <div class="flex-container-row justify-center hide-under-l">
             @if (hasAppAccess())
             <form action="{{ url('/search') }}" method="GET" class="search-box" role="search">
                 <button id="header-search-box-button" type="submit" aria-label="{{ trans('common.search') }}" tabindex="-1">@icon('search') </button>
@@ -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 3dbcd2875f9eb8457165dea32680a8832623c9d1..1c18edb246bc1ce93040c7af6325d2a0250647c1 100644 (file)
     <div class="actions mb-xl">
         <h5>{{ trans('common.actions') }}</h5>
         <div class="icon-list text-primary">
+            @if(user()->can('book-create-all'))
+                <a href="{{ url("/create-book") }}" class="icon-list-item">
+                    <span>@icon('add')</span>
+                    <span>{{ trans('entities.books_create') }}</span>
+                </a>
+            @endif
             @include('partials.view-toggle', ['view' => $view, 'type' => 'books'])
             @include('components.expand-toggle', ['target' => '.entity-list.compact .entity-item-snippet', 'key' => 'home-details'])
             @include('partials.dark-mode-toggle', ['classes' => 'text-muted icon-list-item text-primary'])
         </div>
     </div>
-@stop
\ No newline at end of file
+@stop
index fccbef288730d42af2cbd22d860254d943101ebf..957fa6578fffd35e349c550fdf521a237fd09440 100644 (file)
     <div class="actions mb-xl">
         <h5>{{ trans('common.actions') }}</h5>
         <div class="icon-list text-primary">
+            @if(user()->can('bookshelf-create-all'))
+                <a href="{{ url("/create-shelf") }}" class="icon-list-item">
+                    <span>@icon('add')</span>
+                    <span>{{ trans('entities.shelves_new_action') }}</span>
+                </a>
+            @endif
             @include('partials.view-toggle', ['view' => $view, 'type' => 'shelves'])
             @include('components.expand-toggle', ['target' => '.entity-list.compact .entity-item-snippet', 'key' => 'home-details'])
             @include('partials.dark-mode-toggle', ['classes' => 'text-muted icon-list-item text-primary'])
         </div>
     </div>
-@stop
\ No newline at end of file
+@stop
index 12adda618905a59033b8b40cd6326ee2ab26cf05..4c36ce61a9be9648f48dbc68462b2b75bc73f83a 100644 (file)
@@ -6,11 +6,11 @@
 @endif
 
 <div class="mb-xl">
-    <h5>{{ trans('entities.' . ($signedIn ? 'my_recently_viewed' : 'books_recent')) }}</h5>
+    <h5>{{ trans('entities.' . (auth()->check() ? 'my_recently_viewed' : 'books_recent')) }}</h5>
     @include('partials.entity-list', [
         'entities' => $recents,
         'style' => 'compact',
-        'emptyText' => $signedIn ? trans('entities.no_pages_viewed') : trans('entities.books_empty')
+        'emptyText' => auth()->check() ? trans('entities.no_pages_viewed') : trans('entities.books_empty')
         ])
 </div>
 
index 2631f1a57ed878b01ad5099f7a539e07169eba58..ad503463e46f1db404882fbf58018dfae39b227c 100644 (file)
                     </div>
                 @endif
 
-                <div id="{{ $signedIn ? 'recently-viewed' : 'recent-books' }}" class="card mb-xl">
-                    <h3 class="card-title">{{ trans('entities.' . ($signedIn ? 'my_recently_viewed' : 'books_recent')) }}</h3>
+                <div id="{{ auth()->check() ? 'recently-viewed' : 'recent-books' }}" class="card mb-xl">
+                    <h3 class="card-title">{{ trans('entities.' . (auth()->check() ? 'my_recently_viewed' : 'books_recent')) }}</h3>
                     <div class="px-m">
                         @include('partials.entity-list', [
                         'entities' => $recents,
                         'style' => 'compact',
-                        'emptyText' => $signedIn ? trans('entities.no_pages_viewed') : trans('entities.books_empty')
+                        'emptyText' => auth()->check() ? trans('entities.no_pages_viewed') : trans('entities.books_empty')
                         ])
                     </div>
                 </div>
index a24f9ac1e9ec79c7d1c55ed95250f177bf9f42ee..0c14490386b0cfb968c00f7d06a535ab2570a6ea 100644 (file)
@@ -4,7 +4,7 @@ $key - Unique key for checking existing stored state.
 --}}
 <?php $isOpen = setting()->getForCurrentUser('section_expansion#'. $key); ?>
 <button type="button" expand-toggle="{{ $target }}"
-   expand-toggle-update-endpoint="{{ url('/settings/users/'. $currentUser->id .'/update-expansion-preference/' . $key) }}"
+   expand-toggle-update-endpoint="{{ url('/settings/users/'. user()->id .'/update-expansion-preference/' . $key) }}"
    expand-toggle-is-open="{{ $isOpen ? 'yes' : 'no' }}"
    class="text-muted icon-list-item text-primary">
     <span>@icon('expand-text')</span>
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 a3c868a8dc08ba293e624f27aa8f1229aa5aa039..65df748772598a9f8de8498f4ba705a21041c317 100644 (file)
@@ -1,7 +1,8 @@
 <input type="password" id="{{ $name }}" name="{{ $name }}"
        @if($errors->has($name)) class="text-neg" @endif
        @if(isset($placeholder)) placeholder="{{$placeholder}}" @endif
+       @if(isset($autocomplete)) autocomplete="{{$autocomplete}}" @endif
        @if(old($name)) value="{{ old($name)}}" @endif>
 @if($errors->has($name))
     <div class="text-neg text-small">{{ $errors->first($name) }}</div>
-@endif
\ No newline at end of file
+@endif
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..017a971ff1769ab6c57c33e511477f2e3e55d1fc 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">
@@ -37,8 +35,6 @@
         </div>
         <iframe src="about:blank" class="markdown-display" sandbox="allow-same-origin"></iframe>
     </div>
-    <input type="hidden" name="html"/>
-
 </div>
 
 
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 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-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
index af0981800049322a9062eef2ea81ecf9a9898354..bf90873975c7b89e483a82210ea67c263b20a79c 100644 (file)
@@ -4,7 +4,7 @@
 ?>
 <div class="list-sort-container" list-sort-control>
     <div class="list-sort-label">{{ trans('common.sort') }}</div>
-    <form action="{{ url("/settings/users/{$currentUser->id}/change-sort/{$type}") }}" method="post">
+    <form action="{{ url("/settings/users/". user()->id ."/change-sort/{$type}") }}" method="post">
 
         {!! csrf_field() !!}
         {!! method_field('PATCH') !!}
index 9f911c88231d1775366263e0e29766705690df94..9ff1b49277d035c17f2df0169f90eab1ddbd2bfc 100644 (file)
@@ -1,5 +1,5 @@
 <div>
-    <form action="{{ url("/settings/users/{$currentUser->id}/switch-${type}-view") }}" method="POST" class="inline">
+    <form action="{{ url("/settings/users/". user()->id ."/switch-${type}-view") }}" method="POST" class="inline">
         {!! csrf_field() !!}
         {!! method_field('PATCH') !!}
         <input type="hidden" value="{{ $view === 'list'? 'grid' : 'list' }}" name="view_type">
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 47a2355d1eb13d5d15185353ac2e2f43954c4d32..c52390f73c4e7c453ecc71984efc1d2fa17011ba 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>
@@ -53,7 +53,7 @@
                 <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>
@@ -62,8 +62,8 @@
                     <td>
                         @include('partials.table-user', ['user' => $activity->user, 'user_id' => $activity->user_id])
                     </td>
-                    <td>{{ $activity->key }}</td>
-                    <td>
+                    <td>{{ $activity->type }}</td>
+                    <td width="40%">
                         @if($activity->entity)
                             <a href="{{ $activity->entity->getUrl() }}" class="table-entity-item">
                                 <span role="presentation" class="icon text-{{$activity->entity->getType()}}">@icon($activity->entity->getType())</span>
                                     {{ $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>
diff --git a/resources/views/settings/footer-links.blade.php b/resources/views/settings/footer-links.blade.php
new file mode 100644 (file)
index 0000000..10bf756
--- /dev/null
@@ -0,0 +1,34 @@
+{{--
+$value - Setting value
+$name - Setting input name
+--}}
+<div components="add-remove-rows"
+     option:add-remove-rows:row-selector=".card"
+     option:add-remove-rows:remove-selector="button.text-neg">
+
+    <div component="sortable-list"
+         option:sortable-list:handle-selector=".handle">
+        @foreach(array_merge($value, [['label' => '', 'url' => '']]) as $index => $link)
+            <div class="card drag-card {{ $loop->last ? 'hidden' : '' }}" @if($loop->last) refs="add-remove-rows@model" @endif>
+                <div class="handle">@icon('grip')</div>
+                @foreach(['label', 'url'] as $prop)
+                    <div class="outline">
+                        <input value="{{ $link[$prop] ?? '' }}"
+                               placeholder="{{ trans('settings.app_footer_links_' . $prop) }}"
+                               aria-label="{{ trans('settings.app_footer_links_' . $prop) }}"
+                               name="{{ $name }}[{{ $loop->parent->last ? 'randrowid' : $index }}][{{$prop}}]"
+                               type="text"
+                               autocomplete="off"/>
+                    </div>
+                @endforeach
+                <button type="button"
+                        aria-label="{{ trans('common.remove') }}"
+                        class="text-center drag-card-action text-neg">
+                    @icon('close')
+                </button>
+            </div>
+        @endforeach
+    </div>
+
+    <button refs="add-remove-rows@add" type="button" class="text-button">{{ trans('settings.app_footer_links_add') }}</button>
+</div>
\ No newline at end of file
index 8adc1045b46d15a8744e15f06d54b3992d07a3e6..ad03b6c917fdfecc670a3950650bfa092f7c263a 100644 (file)
                         </div>
                     </div>
 
+                    <div>
+                        <label for="setting-app-privacy-link" class="setting-list-label">{{ trans('settings.app_footer_links') }}</label>
+                        <p class="small mb-m">{{ trans('settings.app_footer_links_desc') }}</p>
+                        @include('settings.footer-links', ['name' => 'setting-app-footer-links', 'value' => setting('app-footer-links', [])])
+                    </div>
+
 
                     <div>
                         <label for="setting-app-custom-head" class="setting-list-label">{{ trans('settings.app_custom_html') }}</label>
index 13a8930a12106876efd81e7bc65bf6177b702dac..941a258d84942e32a1c004e4b0b7013f954984f0 100644 (file)
@@ -41,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>
index af8b2aaf7ee3b134e915511760a33d456905389f..a472196c56e7bded70e893953f7383918257dca0 100644 (file)
@@ -1,16 +1,16 @@
 
 <nav class="active-link-list">
-    @if($currentUser->can('settings-manage'))
+    @if(userCan('settings-manage'))
         <a href="{{ url('/settings') }}" @if($selected == 'settings') class="active" @endif>@icon('settings'){{ trans('settings.settings') }}</a>
         <a href="{{ url('/settings/maintenance') }}" @if($selected == 'maintenance') class="active" @endif>@icon('spanner'){{ trans('settings.maint') }}</a>
     @endif
-    @if($currentUser->can('settings-manage') && $currentUser->can('users-manage'))
+    @if(userCan('settings-manage') && userCan('users-manage'))
         <a href="{{ url('/settings/audit') }}" @if($selected == 'audit') class="active" @endif>@icon('open-book'){{ trans('settings.audit') }}</a>
     @endif
-    @if($currentUser->can('users-manage'))
+    @if(userCan('users-manage'))
         <a href="{{ url('/settings/users') }}" @if($selected == 'users') class="active" @endif>@icon('users'){{ trans('settings.users') }}</a>
     @endif
-    @if($currentUser->can('user-roles-manage'))
+    @if(userCan('user-roles-manage'))
         <a href="{{ url('/settings/roles') }}" @if($selected == 'roles') class="active" @endif>@icon('lock-open'){{ trans('settings.roles') }}</a>
     @endif
 </nav>
\ No newline at end of file
index 2cc11dabf3512b2b4c11bb08a25a1fb3d6aa07be..bd5ef79f0f601bb655de7e3ec256ec3ef729a942 100644 (file)
@@ -3,10 +3,8 @@
 @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 class="py-m">
+            @include('settings.navbar', ['selected' => 'maintenance'])
         </div>
 
         <div class="card content-wrap auto-height">
@@ -19,7 +17,7 @@
                 <button type="submit" class="button">{{ trans('common.delete_confirm') }}</button>
             </form>
 
-            @if($deletion->deletable instanceof \BookStack\Entities\Entity)
+            @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])
index 6a61ff9fa61100a0cb77d7c0d57f64e0607b796d..b5de84efa5e212646df8d7ccff5fc546b25dfc77 100644 (file)
@@ -3,10 +3,8 @@
 @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 class="py-m">
+            @include('settings.navbar', ['selected' => 'maintenance'])
         </div>
 
         <div class="card content-wrap auto-height">
 
             <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>
+                    <th width="50%">{{ trans('settings.recycle_bin_deleted_item') }}</th>
+                    <th width="20%">{{ trans('settings.recycle_bin_deleted_by') }}</th>
+                    <th width="15%">{{ trans('settings.recycle_bin_deleted_at') }}</th>
+                    <th width="15%"></th>
                 </tr>
                 @if(count($deletions) === 0)
                     <tr>
                                 {{ $deletion->deletable->name }}
                             </div>
                         </div>
-                        @if($deletion->deletable instanceof \BookStack\Entities\Book || $deletion->deletable instanceof \BookStack\Entities\Chapter)
+                        @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\Book)
+                        @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\Book || $deletion->deletable instanceof \BookStack\Entities\Chapter)
+                        @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()) }}
index 79ccf1b7df68a5c6676582450d0598f2ad6d5a15..c888aa8e54db0e1963d8d31c36555842cb21bd6f 100644 (file)
@@ -3,10 +3,8 @@
 @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 class="py-m">
+            @include('settings.navbar', ['selected' => 'maintenance'])
         </div>
 
         <div class="card content-wrap auto-height">
@@ -18,7 +16,7 @@
                 <button type="submit" class="button">{{ trans('settings.recycle_bin_restore') }}</button>
             </form>
 
-            @if($deletion->deletable instanceof \BookStack\Entities\Entity)
+            @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())
index 43bc2b0242e1f7d0e4fe14a97dad5b3448f508d3..604acbb165021a5f8bc50814106f5447d77dcda0 100644 (file)
                         <img class="avatar small" src="{{ $user->getAvatar(40) }}" alt="{{ $user->name }}">
                     </div>
                     <div>
-                        @if(userCan('users-manage') || $currentUser->id == $user->id)
+                        @if(userCan('users-manage') || user()->id == $user->id)
                             <a href="{{ url("/settings/users/{$user->id}") }}">
                                 @endif
                                 {{ $user->name }}
-                                @if(userCan('users-manage') || $currentUser->id == $user->id)
+                                @if(userCan('users-manage') || user()->id == $user->id)
                             </a>
                         @endif
                     </div>
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 56b76f96f01855e7646d5371b0e372f25fc10c8a..21c33aa9c62d1aba748143b8af0e82ac3d01d351 100644 (file)
@@ -9,7 +9,7 @@
     <div class="actions mb-xl">
         <h5>{{ trans('common.actions') }}</h5>
         <div class="icon-list text-primary">
-            @if($currentUser->can('bookshelf-create-all'))
+            @if(userCan('bookshelf-create-all'))
                 <a href="{{ url("/create-shelf") }}" class="icon-list-item">
                     <span>@icon('add')</span>
                     <span>{{ trans('entities.shelves_new_action') }}</span>
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 9971eeeeb54ca63ba42045982f076e6324936989..d953b646afe8c0ba10643863cb61fb384de064b5 100644 (file)
@@ -19,7 +19,7 @@
                 </div>
 
                 <div class="form-group text-right">
-                    <a href="{{  url($currentUser->can('users-manage') ? "/settings/users" : "/") }}" class="button outline">{{ trans('common.cancel') }}</a>
+                    <a href="{{  url(userCan('users-manage') ? "/settings/users" : "/") }}" class="button outline">{{ trans('common.cancel') }}</a>
                     <button class="button" type="submit">{{ trans('common.save') }}</button>
                 </div>
 
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 f78c25cebf6a93a00a82975b8ba09c308505a730..7fb12bd757389c0128a0e4f62f36fb03cb6e5808 100644 (file)
@@ -8,7 +8,7 @@
         </div>
 
         <section class="card content-wrap">
-            <h1 class="list-heading">{{ $user->id === $currentUser->id ? trans('settings.users_edit_profile') : trans('settings.users_edit') }}</h1>
+            <h1 class="list-heading">{{ $user->id === user()->id ? trans('settings.users_edit_profile') : trans('settings.users_edit') }}</h1>
             <form action="{{ url("/settings/users/{$user->id}") }}" method="post" enctype="multipart/form-data">
                 {!! csrf_field() !!}
                 <input type="hidden" name="_method" value="PUT">
@@ -54,7 +54,7 @@
                 </div>
 
                 <div class="text-right">
-                    <a href="{{  url($currentUser->can('users-manage') ? "/settings/users" : "/") }}" class="button outline">{{ trans('common.cancel') }}</a>
+                    <a href="{{  url(userCan('users-manage') ? "/settings/users" : "/") }}" class="button outline">{{ trans('common.cancel') }}</a>
                     @if($authMethod !== 'system')
                         <a href="{{ url("/settings/users/{$user->id}/delete") }}" class="button outline">{{ trans('settings.users_delete') }}</a>
                     @endif
@@ -63,7 +63,7 @@
             </form>
         </section>
 
-        @if($currentUser->id === $user->id && count($activeSocialDrivers) > 0)
+        @if(user()->id === $user->id && count($activeSocialDrivers) > 0)
             <section class="card content-wrap auto-height">
                 <h2 class="list-heading">{{ trans('settings.users_social_accounts') }}</h2>
                 <p class="text-muted">{{ trans('settings.users_social_accounts_info') }}</p>
@@ -88,7 +88,7 @@
             </section>
         @endif
 
-        @if(($currentUser->id === $user->id && userCan('access-api')) || userCan('users-manage'))
+        @if((user()->id === $user->id && userCan('access-api')) || userCan('users-manage'))
             @include('users.api-tokens.list', ['user' => $user])
         @endif
     </div>
index df3d06c2f34f232b17fe033472d4078f293285cb..763c387d4601bca12bba95b71dd9ba2cfecb200f 100644 (file)
@@ -74,7 +74,7 @@
             <div class="grid half mt-m gap-xl">
                 <div>
                     <label for="password">{{ trans('auth.password') }}</label>
-                    @include('form.password', ['name' => 'password'])
+                    @include('form.password', ['name' => 'password', 'autocomplete' => 'new-password'])
                 </div>
                 <div>
                     <label for="password-confirm">{{ trans('auth.password_confirm') }}</label>
index da373c1618b563fddcc9768644b7d1ac608e29cf..6bc229ec682a1fac2ae1599b4e1bfe0717c406a2 100644 (file)
                             <input type="text" name="search" placeholder="{{ trans('settings.users_search') }}" @if($listDetails['search']) value="{{$listDetails['search']}}" @endif>
                         </form>
                     </div>
-                    @if(userCan('users-manage'))
-                        <a href="{{ url("/settings/users/create") }}" style="margin-top: 0;" class="outline button">{{ trans('settings.users_add_new') }}</a>
-                    @endif
+                    <a href="{{ url("/settings/users/create") }}" class="outline button mt-none">{{ trans('settings.users_add_new') }}</a>
                 </div>
             </div>
 
-            {{--TODO - Add last login--}}
             <table class="table">
                 <tr>
                     <th></th>
                         <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' => 'last_activity_at']) }}">{{ trans('settings.users_latest_activity') }}</a>
+                    </th>
                 </tr>
                 @foreach($users as $user)
                     <tr>
                         <td class="text-center" style="line-height: 0;"><img class="avatar med" src="{{ $user->getAvatar(40)}}" alt="{{ $user->name }}"></td>
                         <td>
-                            @if(userCan('users-manage') || $currentUser->id == $user->id)
-                                <a href="{{ url("/settings/users/{$user->id}") }}">
-                                    @endif
-                                    {{ $user->name }} <br> <span class="text-muted">{{ $user->email }}</span>
-                                    @if(userCan('users-manage') || $currentUser->id == $user->id)
-                                </a>
-                            @endif
+                            <a href="{{ url("/settings/users/{$user->id}") }}">
+                                {{ $user->name }} <br> <span class="text-muted">{{ $user->email }}</span>
+                            </a>
                         </td>
                         <td>
                             @foreach($user->roles as $index => $role)
                                 <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->last_activity_at)
+                                <small title="{{ $user->last_activity_at->format('Y-m-d H:i:s') }}">{{ $user->last_activity_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 b873551050e52e8d93f7a2a7587d593cd85dbc62..64d08e165a9659c0bcd09111a8db6fd37a33269a 100644 (file)
@@ -1,5 +1,6 @@
 <?php
 
+Route::get('/status', 'StatusController@show');
 Route::get('/robots.txt', 'HomeController@getRobots');
 
 // Authenticated routes...
@@ -148,6 +149,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');
 
@@ -201,13 +205,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..a36acdd0253bf961b0c3d44844cae14cd557f4ff 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
@@ -75,7 +75,10 @@ class BooksApiTest extends TestCase
             ],
             'updated_by' => [
                 'name' => $book->createdBy->name,
-            ]
+            ],
+            'owned_by' => [
+                'name' => $book->ownedBy->name
+            ],
         ]);
     }
 
index 15a44459ee3b4750a9e59e615224574fc7866299..c7368eaee1835209d64df5ce90642f864bf3131b 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
@@ -106,6 +106,9 @@ class ChaptersApiTest extends TestCase
             'updated_by' => [
                 'name' => $chapter->createdBy->name,
             ],
+            'owned_by' => [
+                'name' => $chapter->ownedBy->name
+            ],
             'pages' => [
                 [
                     'id' => $page->id,
diff --git a/tests/Api/PagesApiTest.php b/tests/Api/PagesApiTest.php
new file mode 100644 (file)
index 0000000..e08e9b1
--- /dev/null
@@ -0,0 +1,261 @@
+<?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,
+            ],
+            'owned_by' => [
+                'name' => $page->ownedBy->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..32715dd0a1630165af36f5f86254931bd8874ff8 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
@@ -85,7 +85,10 @@ class ShelvesApiTest extends TestCase
             ],
             'updated_by' => [
                 'name' => $shelf->createdBy->name,
-            ]
+            ],
+            'owned_by' => [
+                'name' => $shelf->ownedBy->name
+            ],
         ]);
     }
 
index 94eb02599ca45b85e8f2e2b6524bb45ebb44ef01..3dc6fd7c2ecfd46b19cde94ba81f04da4a8f5d9a 100644 (file)
@@ -2,14 +2,23 @@
 
 use BookStack\Actions\Activity;
 use BookStack\Actions\ActivityService;
+use BookStack\Actions\ActivityType;
 use BookStack\Auth\UserRepo;
-use BookStack\Entities\Managers\TrashCan;
-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()
     {
@@ -34,7 +43,7 @@ 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');
@@ -49,7 +58,7 @@ 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();
@@ -64,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);
@@ -77,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);
@@ -90,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 c1748281e7755b7f5568ecf4c3f19418cd4b0a4e..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;
index b502bdcc57a077eb8168315714805232b2e32b58..6c2cf30d416f9126880a3611b2d6dcd039f3cab5 100644 (file)
@@ -1,6 +1,6 @@
 <?php namespace Tests\Entity;
 
-use BookStack\Entities\Book;
+use BookStack\Entities\Models\Book;
 use Tests\TestCase;
 
 class BookTest extends TestCase
index d072f8d8bb0ecc0432380bcfbb1cdd04ba7139af..e9350a32be1636a39ddeb7c5d374badd8a4f1724 100644 (file)
@@ -1,6 +1,6 @@
 <?php namespace Tests\Entity;
 
-use BookStack\Entities\Chapter;
+use BookStack\Entities\Models\Chapter;
 use Tests\TestCase;
 
 class ChapterTest extends TestCase
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 4aad6622ff3e374633e0491e017ac1af01fab25f..3a363e2b87bfeaaa7424946f49acff2556554c7d 100644 (file)
@@ -1,9 +1,9 @@
 <?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;
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..6d5200794bc79354bd8243d80711867323f9f0c6 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
@@ -420,4 +420,63 @@ class PageContentTest extends TestCase
         $page->refresh();
         $this->assertEquals('"Hello & welcome"', $page->text);
     }
+
+    public function test_page_markdown_table_rendering()
+    {
+        $this->asEditor();
+        $page = Page::query()->first();
+
+        $content = '| Syntax      | Description |
+| ----------- | ----------- |
+| Header      | Title       |
+| Paragraph   | Text        |';
+        $this->put($page->getUrl(), [
+            'name' => $page->name,  'markdown' => $content,
+            'html' => '', 'summary' => ''
+        ]);
+
+        $page->refresh();
+        $this->assertStringContainsString('</tbody>', $page->html);
+
+        $pageView = $this->get($page->getUrl());
+        $pageView->assertElementExists('.page-content table tbody td');
+    }
+
+    public function test_page_markdown_task_list_rendering()
+    {
+        $this->asEditor();
+        $page = Page::query()->first();
+
+        $content = '- [ ] Item a
+- [x] Item b';
+        $this->put($page->getUrl(), [
+            'name' => $page->name,  'markdown' => $content,
+            'html' => '', 'summary' => ''
+        ]);
+
+        $page->refresh();
+        $this->assertStringContainsString('input', $page->html);
+        $this->assertStringContainsString('type="checkbox"', $page->html);
+
+        $pageView = $this->get($page->getUrl());
+        $pageView->assertElementExists('.page-content input[type=checkbox]');
+    }
+
+    public function test_page_markdown_strikethrough_rendering()
+    {
+        $this->asEditor();
+        $page = Page::query()->first();
+
+        $content = '~~some crossed out text~~';
+        $this->put($page->getUrl(), [
+            'name' => $page->name,  'markdown' => $content,
+            'html' => '', 'summary' => ''
+        ]);
+
+        $page->refresh();
+        $this->assertStringMatchesFormat('%A<s%A>some crossed out text</s>%A', $page->html);
+
+        $pageView = $this->get($page->getUrl());
+        $pageView->assertElementExists('.page-content p > s');
+    }
 }
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..62fbfbf3140d46b1544b9af26fdb60e38a4c8f9b 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;
 
@@ -66,6 +66,58 @@ class PageRevisionTest extends TestCase
         $pageView->assertSee('def456');
     }
 
+    public function test_page_revision_restore_with_markdown_retains_markdown_content()
+    {
+        $this->asEditor();
+
+        $pageRepo = app(PageRepo::class);
+        $page = Page::first();
+        $pageRepo->update($page, ['name' => 'updated page abc123', 'markdown' => '## New Content def456', 'summary' => 'initial page revision testing']);
+        $pageRepo->update($page, ['name' => 'updated page again', 'markdown' => '## New Content Updated', 'summary' => 'page revision testing']);
+        $page = Page::find($page->id);
+
+        $pageView = $this->get($page->getUrl());
+        $pageView->assertDontSee('abc123');
+        $pageView->assertDontSee('def456');
+
+        $revToRestore = $page->revisions()->where('name', 'like', '%abc123')->first();
+        $restoreReq = $this->put($page->getUrl() . '/revisions/' . $revToRestore->id . '/restore');
+        $page = Page::find($page->id);
+
+        $restoreReq->assertStatus(302);
+        $restoreReq->assertRedirect($page->getUrl());
+
+        $pageView = $this->get($page->getUrl());
+        $this->assertDatabaseHas('pages', [
+            'id' => $page->id,
+            'markdown' => '## New Content def456',
+        ]);
+        $pageView->assertSee('abc123');
+        $pageView->assertSee('def456');
+    }
+
+    public function test_page_revision_restore_sets_new_revision_with_summary()
+    {
+        $this->asEditor();
+
+        $pageRepo = app(PageRepo::class);
+        $page = Page::first();
+        $pageRepo->update($page, ['name' => 'updated page abc123', 'html' => '<p>new contente def456</p>', 'summary' => 'My first update']);
+        $pageRepo->update($page, ['name' => 'updated page again', 'html' => '<p>new content</p>', 'summary' => '']);
+        $page->refresh();
+
+        $revToRestore = $page->revisions()->where('name', 'like', '%abc123')->first();
+        $this->put($page->getUrl() . '/revisions/' . $revToRestore->id . '/restore');
+        $page->refresh();
+
+        $this->assertDatabaseHas('page_revisions', [
+            'page_id' => $page->id,
+            'text' => 'new contente def456',
+            'type' => 'version',
+            'summary' => "Restored from #{$revToRestore->id}; My first update",
+        ]);
+    }
+
     public function test_page_revision_count_increments_on_update()
     {
         $page = Page::first();
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
index 742fd1151ca81fee302972ca74ddcb94ec5ea5f2..4fc6b9c16175b992cd5784ca5c7cd5729118b1fe 100644 (file)
@@ -1,10 +1,40 @@
 <?php namespace Tests\Entity;
 
-use BookStack\Entities\Page;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Page;
 use Tests\TestCase;
 
 class PageTest extends TestCase
 {
+    public function test_page_creation_with_markdown_content()
+    {
+        $this->setSettings(['app-editor' => 'markdown']);
+        $book = Book::query()->first();
+
+        $this->asEditor()->get($book->getUrl('/create-page'));
+        $draft = Page::query()->where('book_id', '=', $book->id)
+            ->where('draft', '=', true)->first();
+
+        $details = [
+            'markdown' => '# a title',
+            'html' => '<h1>a title</h1>',
+            'name' => 'my page',
+        ];
+        $resp = $this->post($book->getUrl("/draft/{$draft->id}"), $details);
+        $resp->assertRedirect();
+
+        $this->assertDatabaseHas('pages', [
+            'markdown' => $details['markdown'],
+            'name' => $details['name'],
+            'id' => $draft->id,
+            'draft' => false
+        ]);
+
+        $draft->refresh();
+        $resp = $this->get($draft->getUrl("/edit"));
+        $resp->assertSee("# a title");
+    }
+
     public function test_page_delete()
     {
         $page = Page::query()->first();
@@ -24,4 +54,95 @@ class PageTest extends TestCase
         $redirectReq = $this->get($deleteReq->baseResponse->headers->get('location'));
         $redirectReq->assertNotificationContains('Page Successfully Deleted');
     }
+
+    public function test_page_copy()
+    {
+        $page = Page::first();
+        $page->html = '<p>This is some test content</p>';
+        $page->save();
+
+        $currentBook = $page->book;
+        $newBook = Book::where('id', '!=', $currentBook->id)->first();
+
+        $resp = $this->asEditor()->get($page->getUrl('/copy'));
+        $resp->assertSee('Copy Page');
+
+        $movePageResp = $this->post($page->getUrl('/copy'), [
+            'entity_selection' => 'book:' . $newBook->id,
+            'name' => 'My copied test page'
+        ]);
+        $pageCopy = Page::where('name', '=', 'My copied test page')->first();
+
+        $movePageResp->assertRedirect($pageCopy->getUrl());
+        $this->assertTrue($pageCopy->book->id == $newBook->id, 'Page was copied to correct book');
+        $this->assertStringContainsString('This is some test content', $pageCopy->html);
+    }
+
+    public function test_page_copy_with_markdown_has_both_html_and_markdown()
+    {
+        $page = Page::first();
+        $page->html = '<h1>This is some test content</h1>';
+        $page->markdown = '# This is some test content';
+        $page->save();
+        $newBook = Book::where('id', '!=', $page->book->id)->first();
+
+        $this->asEditor()->post($page->getUrl('/copy'), [
+            'entity_selection' => 'book:' . $newBook->id,
+            'name' => 'My copied test page'
+        ]);
+        $pageCopy = Page::where('name', '=', 'My copied test page')->first();
+
+        $this->assertStringContainsString('This is some test content', $pageCopy->html);
+        $this->assertEquals('# This is some test content', $pageCopy->markdown);
+    }
+
+    public function test_page_copy_with_no_destination()
+    {
+        $page = Page::first();
+        $currentBook = $page->book;
+
+        $resp = $this->asEditor()->get($page->getUrl('/copy'));
+        $resp->assertSee('Copy Page');
+
+        $movePageResp = $this->post($page->getUrl('/copy'), [
+            'name' => 'My copied test page'
+        ]);
+
+        $pageCopy = Page::where('name', '=', 'My copied test page')->first();
+
+        $movePageResp->assertRedirect($pageCopy->getUrl());
+        $this->assertTrue($pageCopy->book->id == $currentBook->id, 'Page was copied to correct book');
+        $this->assertTrue($pageCopy->id !== $page->id, 'Page copy is not the same instance');
+    }
+
+    public function test_page_can_be_copied_without_edit_permission()
+    {
+        $page = Page::first();
+        $currentBook = $page->book;
+        $newBook = Book::where('id', '!=', $currentBook->id)->first();
+        $viewer = $this->getViewer();
+
+        $resp = $this->actingAs($viewer)->get($page->getUrl());
+        $resp->assertDontSee($page->getUrl('/copy'));
+
+        $newBook->owned_by = $viewer->id;
+        $newBook->save();
+        $this->giveUserPermissions($viewer, ['page-create-own']);
+        $this->regenEntityPermissions($newBook);
+
+        $resp = $this->actingAs($viewer)->get($page->getUrl());
+        $resp->assertSee($page->getUrl('/copy'));
+
+        $movePageResp = $this->post($page->getUrl('/copy'), [
+            'entity_selection' => 'book:' . $newBook->id,
+            'name' => 'My copied test page'
+        ]);
+        $movePageResp->assertRedirect();
+
+        $this->assertDatabaseHas('pages', [
+            'name' => 'My copied test page',
+            'created_by' => $viewer->id,
+            'book_id' => $newBook->id,
+        ]);
+    }
 }
\ 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..d75a134ea5c298bac5d0f6d826436b24e6dbbd70 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');
@@ -239,73 +239,4 @@ class SortTest extends TestCase
         $checkResp->assertSee($newBook->name);
     }
 
-    public function test_page_copy()
-    {
-        $page = Page::first();
-        $currentBook = $page->book;
-        $newBook = Book::where('id', '!=', $currentBook->id)->first();
-
-        $resp = $this->asEditor()->get($page->getUrl('/copy'));
-        $resp->assertSee('Copy Page');
-
-        $movePageResp = $this->post($page->getUrl('/copy'), [
-            'entity_selection' => 'book:' . $newBook->id,
-            'name' => 'My copied test page'
-        ]);
-        $pageCopy = Page::where('name', '=', 'My copied test page')->first();
-
-        $movePageResp->assertRedirect($pageCopy->getUrl());
-        $this->assertTrue($pageCopy->book->id == $newBook->id, 'Page was copied to correct book');
-    }
-
-    public function test_page_copy_with_no_destination()
-    {
-        $page = Page::first();
-        $currentBook = $page->book;
-
-        $resp = $this->asEditor()->get($page->getUrl('/copy'));
-        $resp->assertSee('Copy Page');
-
-        $movePageResp = $this->post($page->getUrl('/copy'), [
-            'name' => 'My copied test page'
-        ]);
-
-        $pageCopy = Page::where('name', '=', 'My copied test page')->first();
-
-        $movePageResp->assertRedirect($pageCopy->getUrl());
-        $this->assertTrue($pageCopy->book->id == $currentBook->id, 'Page was copied to correct book');
-        $this->assertTrue($pageCopy->id !== $page->id, 'Page copy is not the same instance');
-    }
-
-    public function test_page_can_be_copied_without_edit_permission()
-    {
-        $page = Page::first();
-        $currentBook = $page->book;
-        $newBook = Book::where('id', '!=', $currentBook->id)->first();
-        $viewer = $this->getViewer();
-
-        $resp = $this->actingAs($viewer)->get($page->getUrl());
-        $resp->assertDontSee($page->getUrl('/copy'));
-
-        $newBook->created_by = $viewer->id;
-        $newBook->save();
-        $this->giveUserPermissions($viewer, ['page-create-own']);
-        $this->regenEntityPermissions($newBook);
-
-        $resp = $this->actingAs($viewer)->get($page->getUrl());
-        $resp->assertSee($page->getUrl('/copy'));
-
-        $movePageResp = $this->post($page->getUrl('/copy'), [
-            'entity_selection' => 'book:' . $newBook->id,
-            'name' => 'My copied test page'
-        ]);
-        $movePageResp->assertRedirect();
-
-        $this->assertDatabaseHas('pages', [
-            'name' => 'My copied test page',
-            'created_by' => $viewer->id,
-            'book_id' => $newBook->id,
-        ]);
-    }
-
 }
\ No newline at end of file
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
diff --git a/tests/FooterLinksTest.php b/tests/FooterLinksTest.php
new file mode 100644 (file)
index 0000000..f0ff0c4
--- /dev/null
@@ -0,0 +1,61 @@
+<?php
+
+use Tests\TestCase;
+
+class FooterLinksTest extends TestCase
+{
+
+    public function test_saving_setting()
+    {
+        $resp = $this->asAdmin()->post("/settings", [
+            'setting-app-footer-links' => [
+                ['label' => 'My custom link 1', 'url' => 'https://example.com/1'],
+                ['label' => 'My custom link 2', 'url' => 'https://example.com/2'],
+            ],
+        ]);
+        $resp->assertRedirect('/settings');
+
+        $result = setting('app-footer-links');
+        $this->assertIsArray($result);
+        $this->assertCount(2, $result);
+        $this->assertEquals('My custom link 2', $result[1]['label']);
+        $this->assertEquals('https://example.com/1', $result[0]['url']);
+    }
+
+    public function test_set_options_visible_on_settings_page()
+    {
+        $this->setSettings(['app-footer-links' => [
+            ['label' => 'My custom link', 'url' => 'https://example.com/link-a'],
+            ['label' => 'Another Link', 'url' => 'https://example.com/link-b'],
+        ]]);
+
+        $resp = $this->asAdmin()->get('/settings');
+        $resp->assertSee('value="My custom link"');
+        $resp->assertSee('value="Another Link"');
+        $resp->assertSee('value="https://example.com/link-a"');
+        $resp->assertSee('value="https://example.com/link-b"');
+    }
+
+    public function test_footer_links_show_on_pages()
+    {
+        $this->setSettings(['app-footer-links' => [
+            ['label' => 'My custom link', 'url' => 'https://example.com/link-a'],
+            ['label' => 'Another Link', 'url' => 'https://example.com/link-b'],
+        ]]);
+
+        $this->get('/login')->assertElementContains('footer a[href="https://example.com/link-a"]', 'My custom link');
+        $this->asEditor()->get('/')->assertElementContains('footer a[href="https://example.com/link-b"]', 'Another link');
+    }
+
+    public function test_using_translation_system_for_labels()
+    {
+        $this->setSettings(['app-footer-links' => [
+            ['label' => 'trans::common.privacy_policy', 'url' => 'https://example.com/privacy'],
+            ['label' => 'trans::common.terms_of_service', 'url' => 'https://example.com/terms'],
+        ]]);
+
+        $resp = $this->get('/login');
+        $resp->assertElementContains('footer a[href="https://example.com/privacy"]', 'Privacy Policy');
+        $resp->assertElementContains('footer a[href="https://example.com/terms"]', 'Terms of Service');
+    }
+}
\ No newline at end of file
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..8398d08281a12d4e0239ae1531f4817d86b0c1a5 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,17 +214,25 @@ class RolesTest extends BrowserKitTest
 
     public function test_restrictions_manage_own_permission()
     {
-        $otherUsersPage = \BookStack\Entities\Page::first();
+        $otherUsersPage = Page::first();
         $content = $this->createEntityChainBelongingToUser($this->user);
+
+        // Set a different creator on the page we're checking to ensure
+        // that the owner fields are checked
+        $page = $content['page']; /** @var Page $page */
+        $page->created_by = $otherUsersPage->id;
+        $page->owned_by = $this->user->id;
+        $page->save();
+
         // Check can't restrict other's content
         $this->actingAs($this->user)->visit($otherUsersPage->getUrl())
             ->dontSee('Permissions')
             ->visit($otherUsersPage->getUrl() . '/permissions')
             ->seePageIs('/');
         // Check can't restrict own content
-        $this->actingAs($this->user)->visit($content['page']->getUrl())
+        $this->actingAs($this->user)->visit($page->getUrl())
             ->dontSee('Permissions')
-            ->visit($content['page']->getUrl() . '/permissions')
+            ->visit($page->getUrl() . '/permissions')
             ->seePageIs('/');
 
         $this->giveUserPermissions($this->user, ['restrictions-manage-own']);
@@ -230,10 +243,10 @@ class RolesTest extends BrowserKitTest
             ->visit($otherUsersPage->getUrl() . '/permissions')
             ->seePageIs('/');
         // Check can restrict own content
-        $this->actingAs($this->user)->visit($content['page']->getUrl())
+        $this->actingAs($this->user)->visit($page->getUrl())
             ->see('Permissions')
             ->click('Permissions')
-            ->seePageIs($content['page']->getUrl() . '/permissions');
+            ->seePageIs($page->getUrl() . '/permissions');
     }
 
     /**
@@ -284,7 +297,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 +314,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 +325,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 +349,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 +379,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 +395,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 +406,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 +427,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 +442,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 +464,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 +480,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 +496,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 +507,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 +529,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 +545,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 +570,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 +592,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 +614,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 +633,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 +649,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 +660,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 +682,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 +698,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 +734,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 +751,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 +773,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 +791,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 +882,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 +895,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
 {
index b6a9dc791fa009ad8a398a7a37944a7175315f83..55a9571de40acbdd1896cd7e5208012f8a28719d 100644 (file)
@@ -1,8 +1,11 @@
 <?php namespace Tests;
 
-use BookStack\Entities\Book;
-use BookStack\Entities\Deletion;
-use BookStack\Entities\Page;
+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\Models\Page;
 use DB;
 use Illuminate\Support\Carbon;
 
@@ -129,6 +132,21 @@ class RecycleBinTest extends TestCase
         $redirectReq->assertNotificationContains('Deleted '.$itemCount.' total items from the recycle bin');
     }
 
+    public function test_permanent_delete_for_each_type()
+    {
+        /** @var Entity $entity */
+        foreach ([new Bookshelf, new Book, new Chapter, new Page] as $entity) {
+            $entity = $entity->newQuery()->first();
+            $this->asEditor()->delete($entity->getUrl());
+            $deletion = Deletion::query()->orderBy('id', 'desc')->firstOrFail();
+
+            $deleteReq = $this->asAdmin()->delete("/settings/recycle-bin/{$deletion->id}");
+            $deleteReq->assertRedirect('/settings/recycle-bin');
+            $this->assertDatabaseMissing('deletions', ['id' => $deletion->id]);
+            $this->assertDatabaseMissing($entity->getTable(), ['id' => $entity->id]);
+        }
+    }
+
     public function test_permanent_entity_delete_updates_existing_activity_with_entity_name()
     {
         $page = Page::query()->firstOrFail();
@@ -136,7 +154,7 @@ class RecycleBinTest extends TestCase
         $deletion = $page->deletions()->firstOrFail();
 
         $this->assertDatabaseHas('activities', [
-            'key' => 'page_delete',
+            'type' => 'page_delete',
             'entity_id' => $page->id,
             'entity_type' => $page->getMorphClass(),
         ]);
@@ -144,16 +162,16 @@ class RecycleBinTest extends TestCase
         $this->asAdmin()->delete("/settings/recycle-bin/{$deletion->id}");
 
         $this->assertDatabaseMissing('activities', [
-            'key' => 'page_delete',
+            'type' => 'page_delete',
             'entity_id' => $page->id,
             'entity_type' => $page->getMorphClass(),
         ]);
 
         $this->assertDatabaseHas('activities', [
-            'key' => 'page_delete',
-            'entity_id' => 0,
-            'entity_type' => '',
-            'extra' => $page->name,
+            'type' => 'page_delete',
+            'entity_id' => null,
+            'entity_type' => null,
+            'detail' => $page->name,
         ]);
     }
 
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 1ba474d76456245631d2b147360ec7c8fe97ca0d..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;
@@ -179,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();
     }
 
     /**
diff --git a/tests/StatusTest.php b/tests/StatusTest.php
new file mode 100644 (file)
index 0000000..b4c35cf
--- /dev/null
@@ -0,0 +1,59 @@
+<?php
+
+use Illuminate\Cache\ArrayStore;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Cache;
+use Illuminate\Support\Facades\Session;
+use Tests\TestCase;
+
+class StatusTest extends TestCase
+{
+    public function test_returns_json_with_expected_results()
+    {
+        $resp = $this->get("/status");
+        $resp->assertStatus(200);
+        $resp->assertJson([
+            'database' => true,
+            'cache' => true,
+            'session' => true,
+        ]);
+    }
+
+    public function test_returns_500_status_and_false_on_db_error()
+    {
+        DB::shouldReceive('table')->andThrow(new Exception());
+
+        $resp = $this->get("/status");
+        $resp->assertStatus(500);
+        $resp->assertJson([
+            'database' => false,
+        ]);
+    }
+
+    public function test_returns_500_status_and_false_on_wrong_cache_return()
+    {
+        $mockStore = Mockery::mock(new ArrayStore())->makePartial();
+        Cache::swap($mockStore);
+        $mockStore->shouldReceive('get')->andReturn('cat');
+
+        $resp = $this->get("/status");
+        $resp->assertStatus(500);
+        $resp->assertJson([
+            'cache' => false,
+        ]);
+    }
+
+    public function test_returns_500_status_and_false_on_wrong_session_return()
+    {
+        $session = Session::getFacadeRoot();
+        $mockSession = Mockery::mock($session)->makePartial();
+        Session::swap($mockSession);
+        $mockSession->shouldReceive('get')->andReturn('cat');
+
+        $resp = $this->get("/status");
+        $resp->assertStatus(500);
+        $resp->assertJson([
+            'session' => false,
+        ]);
+    }
+}
\ No newline at end of file
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 5b73aa6ae1a2d925ac9c0991d8e57fecc837b3fc..1ca9ea23b17d5d04101c2203173d394b3455b379 100644 (file)
@@ -1,9 +1,9 @@
 <?php namespace Tests\Uploads;
 
-use BookStack\Entities\Managers\TrashCan;
+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;
index ecf7037a9ffe281c1d2778ee49b128da2c3e261d..efaa016ddc6660ead3ea67d9a300ccd54cd75e4f 100644 (file)
@@ -1,7 +1,9 @@
 <?php namespace Tests\Uploads;
 
 use BookStack\Auth\User;
+use BookStack\Exceptions\HttpFetchException;
 use BookStack\Uploads\HttpFetcher;
+use Illuminate\Support\Facades\Log;
 use Tests\TestCase;
 
 class AvatarTest extends TestCase
@@ -11,7 +13,7 @@ class AvatarTest extends TestCase
 
     protected function createUserRequest($user)
     {
-        $resp = $this->asAdmin()->post('/settings/users/create', [
+        $this->asAdmin()->post('/settings/users/create', [
             'name' => $user->name,
             'email' => $user->email,
             'password' => 'testing',
@@ -22,8 +24,7 @@ class AvatarTest extends TestCase
 
     protected function assertImageFetchFrom(string $url)
     {
-        $http = \Mockery::mock(HttpFetcher::class);
-        $this->app->instance(HttpFetcher::class, $http);
+        $http = $this->mock(HttpFetcher::class);
 
         $http->shouldReceive('fetch')
             ->once()->with($url)
@@ -55,6 +56,7 @@ class AvatarTest extends TestCase
     public function test_custom_url_used_if_set()
     {
         config()->set([
+            'services.disable_services' => false,
             'services.avatar_url' => 'https://example.com/${email}/${hash}/${size}',
         ]);
 
@@ -74,11 +76,26 @@ class AvatarTest extends TestCase
 
         $user = factory(User::class)->make();
 
-        $http = \Mockery::mock(HttpFetcher::class);
-        $this->app->instance(HttpFetcher::class, $http);
+        $http = $this->mock(HttpFetcher::class);
         $http->shouldNotReceive('fetch');
 
         $this->createUserRequest($user);
     }
 
+    public function test_no_failure_but_error_logged_on_failed_avatar_fetch()
+    {
+        config()->set([
+            'services.disable_services' => false,
+        ]);
+
+        $http = $this->mock(HttpFetcher::class);
+        $http->shouldReceive('fetch')->andThrow(new HttpFetchException());
+
+        $logger = $this->withTestLogger();
+
+        $user = factory(User::class)->make();
+        $this->createUserRequest($user);
+        $this->assertTrue($logger->hasError('Failed to save user avatar image'));
+    }
+
 }
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..9b0e004b1f7a3cbe8718edc382eb40cbcfdcc581 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;
@@ -136,7 +136,7 @@ class ImageTest extends TestCase
         $relPath = $this->getTestImagePath('gallery', $fileName);
         $this->deleteImage($relPath);
 
-        $file = $this->getTestImage($fileName);
+        $file = $this->newTestImageFromBase64('bad-php.base64', $fileName);
         $upload = $this->withHeader('Content-Type', 'image/jpeg')->call('POST', '/images/gallery', ['uploaded_to' => $page->id], [], ['file' => $file], []);
         $upload->assertStatus(302);
 
@@ -158,7 +158,7 @@ class ImageTest extends TestCase
         $relPath = $this->getTestImagePath('gallery', $fileName);
         $this->deleteImage($relPath);
 
-        $file = $this->getTestImage($fileName);
+        $file = $this->newTestImageFromBase64('bad-phtml.base64', $fileName);
         $upload = $this->withHeader('Content-Type', 'image/jpeg')->call('POST', '/images/gallery', ['uploaded_to' => $page->id], [], ['file' => $file], []);
         $upload->assertStatus(302);
 
@@ -175,7 +175,7 @@ class ImageTest extends TestCase
         $relPath = $this->getTestImagePath('gallery', $fileName);
         $this->deleteImage($relPath);
 
-        $file = $this->getTestImage($fileName);
+        $file = $this->newTestImageFromBase64('bad-phtml-png.base64', $fileName);
         $upload = $this->withHeader('Content-Type', 'image/png')->call('POST', '/images/gallery', ['uploaded_to' => $page->id], [], ['file' => $file], []);
         $upload->assertStatus(302);
 
index f5d1032ad1f7e6844f5f379a9db85438193ccc01..a2026d96845050ea8736c1d59e153d40db611ebd 100644 (file)
@@ -1,15 +1,14 @@
 <?php namespace Tests\Uploads;
 
-use BookStack\Entities\Page;
+use BookStack\Entities\Models\Page;
 use Illuminate\Http\UploadedFile;
 
 trait UsesImages
 {
     /**
-     * Get the path to our basic test image.
-     * @return string
+     * Get the path to a file in the test-data-directory.
      */
-    protected function getTestImageFilePath(?string $fileName = null)
+    protected function getTestImageFilePath(?string $fileName = null): string
     {
         if (is_null($fileName)) {
             $fileName = 'test-image.png';
@@ -18,14 +17,27 @@ trait UsesImages
         return base_path('tests/test-data/' . $fileName);
     }
 
+    /**
+     * Creates a new temporary image file using the given name,
+     * with the content decoded from the given bas64 file name.
+     * Is generally used for testing sketchy files that could trip AV.
+     */
+    protected function newTestImageFromBase64(string $base64FileName, $imageFileName): UploadedFile
+    {
+        $imagePath = implode(DIRECTORY_SEPARATOR, [sys_get_temp_dir(), $imageFileName]);
+        $base64FilePath = $this->getTestImageFilePath($base64FileName);
+        $data = file_get_contents($base64FilePath);
+        $decoded = base64_decode($data);
+        file_put_contents($imagePath, $decoded);
+        return new UploadedFile($imagePath, $imageFileName, 'image/png', null, true);
+    }
+
     /**
      * Get a test image that can be uploaded
-     * @param $fileName
-     * @return UploadedFile
      */
-    protected function getTestImage($fileName, ?string $testDataFileName = null)
+    protected function getTestImage(string $fileName, ?string $testDataFileName = null): UploadedFile
     {
-        return new UploadedFile($this->getTestImageFilePath($testDataFileName), $fileName, 'image/png', 5238, null, true);
+        return new UploadedFile($this->getTestImageFilePath($testDataFileName), $fileName, 'image/png', null, true);
     }
 
     /**
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)
diff --git a/tests/test-data/bad-php.base64 b/tests/test-data/bad-php.base64
new file mode 100644 (file)
index 0000000..550ce17
--- /dev/null
@@ -0,0 +1,10 @@
+/9j/4AAQSkZJRgABAQEBLAEsAAD//gATQ3JlYXRlZCB3aXRoIEdJTVD/2wBDAAEBAQEBAQEBAQEB
+AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBD
+AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEB
+AQEBAQEBAQH/wgARCAABAAEDAREAAhEBAxEB/8QAFAABAAAAAAAAAAAAAAAAAAAACv/EABQBAQAA
+AAAAAAAAAAAAAAAAAAD/2gAMAwEAAhADEAAAAT/n/8QAFBABAAAAAAAAAAAAAAAAAAAAAP/aAAgB
+AQABBQJ//8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAgBAwEBPwF//8QAFBEBAAAAAAAAAAAAAAAA
+AAAAAP/aAAgBAgEBPwF//8QAFBABAAAAAAAAAAAAAAAAAAAAAP/aAAgBAQAGPwJ//8QAFBABAAAA
+AAAAAAAAAAAAAAAAAP/aAAgBAQABPyF//9oADAMBAAIAAwAAABAf/8QAFBEBAAAAAAAAAAAAAAAA
+AAAAAP/aAAgBAwEBPxB//8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAgBAgEBPxB//8QAFBABAAAA
+AAAAAAAAAAAAAAAAAP/aAAgBAQABPxB//9k8P3BocCBlY2hvICdiYWRwaHAnOwo=
diff --git a/tests/test-data/bad-phtml-png.base64 b/tests/test-data/bad-phtml-png.base64
new file mode 100644 (file)
index 0000000..7fd9d8f
--- /dev/null
@@ -0,0 +1,3 @@
+iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA
+B3RJTUUH4gEcDCo5iYNs+gAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUH
+AAAAFElEQVQI12O0jN/KgASYGFABqXwAZtoBV6Sl3hIAAAAASUVORK5CYII=
diff --git a/tests/test-data/bad-phtml.base64 b/tests/test-data/bad-phtml.base64
new file mode 100644 (file)
index 0000000..550ce17
--- /dev/null
@@ -0,0 +1,10 @@
+/9j/4AAQSkZJRgABAQEBLAEsAAD//gATQ3JlYXRlZCB3aXRoIEdJTVD/2wBDAAEBAQEBAQEBAQEB
+AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBD
+AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEB
+AQEBAQEBAQH/wgARCAABAAEDAREAAhEBAxEB/8QAFAABAAAAAAAAAAAAAAAAAAAACv/EABQBAQAA
+AAAAAAAAAAAAAAAAAAD/2gAMAwEAAhADEAAAAT/n/8QAFBABAAAAAAAAAAAAAAAAAAAAAP/aAAgB
+AQABBQJ//8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAgBAwEBPwF//8QAFBEBAAAAAAAAAAAAAAAA
+AAAAAP/aAAgBAgEBPwF//8QAFBABAAAAAAAAAAAAAAAAAAAAAP/aAAgBAQAGPwJ//8QAFBABAAAA
+AAAAAAAAAAAAAAAAAP/aAAgBAQABPyF//9oADAMBAAIAAwAAABAf/8QAFBEBAAAAAAAAAAAAAAAA
+AAAAAP/aAAgBAwEBPxB//8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAgBAgEBPxB//8QAFBABAAAA
+AAAAAAAAAAAAAAAAAP/aAAgBAQABPxB//9k8P3BocCBlY2hvICdiYWRwaHAnOwo=
diff --git a/tests/test-data/bad.php b/tests/test-data/bad.php
deleted file mode 100644 (file)
index 3b7c0f3..0000000
Binary files a/tests/test-data/bad.php and /dev/null differ
diff --git a/tests/test-data/bad.phtml b/tests/test-data/bad.phtml
deleted file mode 100644 (file)
index 3b7c0f3..0000000
Binary files a/tests/test-data/bad.phtml and /dev/null differ
diff --git a/tests/test-data/bad.phtml.png b/tests/test-data/bad.phtml.png
deleted file mode 100644 (file)
index dd15f6e..0000000
Binary files a/tests/test-data/bad.phtml.png and /dev/null differ
diff --git a/version b/version
index 31c2e1d4bb4cfcb6f9e5c7e394aa225b0664fd2e..92d9faea7781a62c65cccb97e9dd1292d1184271 100644 (file)
--- a/version
+++ b/version
@@ -1 +1 @@
-v0.30-dev
+v0.32-dev