]> BookStack Code Mirror - bookstack/commitdiff
Merge branch 'conflict_warnings' of https://github.com/MatthieuParis/BookStack into...
authorDan Brown <redacted>
Mon, 4 Oct 2021 16:10:40 +0000 (17:10 +0100)
committerDan Brown <redacted>
Mon, 4 Oct 2021 16:10:40 +0000 (17:10 +0100)
561 files changed:
.env.example
.env.example.complete
.github/translators.txt
app/Actions/ActivityService.php
app/Actions/ActivityType.php
app/Actions/Comment.php
app/Actions/TagRepo.php
app/Api/ApiTokenGuard.php
app/Auth/Access/EmailConfirmationService.php
app/Auth/Access/Guards/ExternalBaseSessionGuard.php
app/Auth/Access/LoginService.php [new file with mode: 0644]
app/Auth/Access/Mfa/BackupCodeService.php [new file with mode: 0644]
app/Auth/Access/Mfa/MfaSession.php [new file with mode: 0644]
app/Auth/Access/Mfa/MfaValue.php [new file with mode: 0644]
app/Auth/Access/Mfa/TotpService.php [new file with mode: 0644]
app/Auth/Access/Mfa/TotpValidationRule.php [new file with mode: 0644]
app/Auth/Access/Saml2Service.php
app/Auth/Access/SocialAuthService.php
app/Auth/Access/UserTokenService.php
app/Auth/Permissions/PermissionService.php
app/Auth/Permissions/PermissionsRepo.php
app/Auth/Role.php
app/Auth/User.php
app/Auth/UserRepo.php
app/Config/app.php
app/Config/database.php
app/Config/dompdf.php
app/Console/Commands/RegenerateSearch.php
app/Console/Commands/ResetMfa.php [new file with mode: 0644]
app/Entities/Models/Book.php
app/Entities/Models/BookChild.php
app/Entities/Models/Chapter.php
app/Entities/Models/Page.php
app/Entities/Repos/PageRepo.php
app/Entities/Tools/BookContents.php
app/Entities/Tools/ExportFormatter.php
app/Entities/Tools/Markdown/CustomListItemRenderer.php [new file with mode: 0644]
app/Entities/Tools/PageContent.php
app/Exceptions/StoppedAuthenticationException.php [new file with mode: 0644]
app/Http/Controllers/Api/BookExportApiController.php
app/Http/Controllers/Api/ChapterExportApiController.php
app/Http/Controllers/Api/PageExportApiController.php
app/Http/Controllers/Auth/ConfirmEmailController.php
app/Http/Controllers/Auth/HandlesPartialLogins.php [new file with mode: 0644]
app/Http/Controllers/Auth/LoginController.php
app/Http/Controllers/Auth/MfaBackupCodesController.php [new file with mode: 0644]
app/Http/Controllers/Auth/MfaController.php [new file with mode: 0644]
app/Http/Controllers/Auth/MfaTotpController.php [new file with mode: 0644]
app/Http/Controllers/Auth/RegisterController.php
app/Http/Controllers/Auth/SocialController.php
app/Http/Controllers/Auth/UserInviteController.php
app/Http/Controllers/BookExportController.php
app/Http/Controllers/BookSortController.php
app/Http/Controllers/ChapterExportController.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/PageExportController.php
app/Http/Controllers/PageTemplateController.php
app/Http/Controllers/SearchController.php
app/Http/Controllers/UserController.php
app/Http/Controllers/UserSearchController.php
app/Http/Kernel.php
app/Http/Middleware/ApiAuthenticate.php
app/Http/Middleware/ApplyCspRules.php [new file with mode: 0644]
app/Http/Middleware/Authenticate.php
app/Http/Middleware/AuthenticatedOrPendingMfa.php [new file with mode: 0644]
app/Http/Middleware/CheckEmailConfirmed.php [new file with mode: 0644]
app/Http/Middleware/CheckUserHasPermission.php [new file with mode: 0644]
app/Http/Middleware/ChecksForEmailConfirmation.php [deleted file]
app/Http/Middleware/ControlIframeSecurity.php [deleted file]
app/Http/Middleware/Localization.php
app/Http/Middleware/PermissionMiddleware.php [deleted file]
app/Providers/AppServiceProvider.php
app/Providers/AuthServiceProvider.php
app/Providers/RouteServiceProvider.php
app/Theming/CustomHtmlHeadContentProvider.php [new file with mode: 0644]
app/Theming/ThemeEvents.php
app/Translation/FileLoader.php
app/Uploads/AttachmentService.php
app/Uploads/ImageService.php
app/Util/CspService.php [new file with mode: 0644]
app/Util/HtmlContentFilter.php
app/Util/HtmlNonceApplicator.php [new file with mode: 0644]
composer.json
composer.lock
database/migrations/2016_04_20_192649_create_joint_permissions_table.php
database/migrations/2021_06_30_173111_create_mfa_values_table.php [new file with mode: 0644]
database/migrations/2021_07_03_085038_add_mfa_enforced_to_roles_table.php [new file with mode: 0644]
database/migrations/2021_08_28_161743_add_export_role_permission.php [new file with mode: 0644]
database/migrations/2021_09_26_044614_add_activities_ip_column.php [new file with mode: 0644]
package-lock.json
package.json
phpunit.xml
readme.md
resources/lang/ar/activities.php
resources/lang/ar/auth.php
resources/lang/ar/common.php
resources/lang/ar/entities.php
resources/lang/ar/settings.php
resources/lang/ar/validation.php
resources/lang/bg/activities.php
resources/lang/bg/auth.php
resources/lang/bg/common.php
resources/lang/bg/entities.php
resources/lang/bg/settings.php
resources/lang/bg/validation.php
resources/lang/bs/activities.php
resources/lang/bs/auth.php
resources/lang/bs/common.php
resources/lang/bs/entities.php
resources/lang/bs/settings.php
resources/lang/bs/validation.php
resources/lang/ca/activities.php
resources/lang/ca/auth.php
resources/lang/ca/common.php
resources/lang/ca/entities.php
resources/lang/ca/settings.php
resources/lang/ca/validation.php
resources/lang/cs/activities.php
resources/lang/cs/auth.php
resources/lang/cs/common.php
resources/lang/cs/components.php
resources/lang/cs/entities.php
resources/lang/cs/errors.php
resources/lang/cs/passwords.php
resources/lang/cs/settings.php
resources/lang/cs/validation.php
resources/lang/da/activities.php
resources/lang/da/auth.php
resources/lang/da/common.php
resources/lang/da/entities.php
resources/lang/da/errors.php
resources/lang/da/settings.php
resources/lang/da/validation.php
resources/lang/de/activities.php
resources/lang/de/auth.php
resources/lang/de/common.php
resources/lang/de/entities.php
resources/lang/de/errors.php
resources/lang/de/settings.php
resources/lang/de/validation.php
resources/lang/de_informal/activities.php
resources/lang/de_informal/auth.php
resources/lang/de_informal/common.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/auth.php
resources/lang/en/common.php
resources/lang/en/entities.php
resources/lang/en/settings.php
resources/lang/en/validation.php
resources/lang/es/activities.php
resources/lang/es/auth.php
resources/lang/es/common.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/auth.php
resources/lang/es_AR/common.php
resources/lang/es_AR/entities.php
resources/lang/es_AR/settings.php
resources/lang/es_AR/validation.php
resources/lang/fa/activities.php
resources/lang/fa/auth.php
resources/lang/fa/common.php
resources/lang/fa/components.php
resources/lang/fa/entities.php
resources/lang/fa/errors.php
resources/lang/fa/pagination.php
resources/lang/fa/passwords.php
resources/lang/fa/settings.php
resources/lang/fa/validation.php
resources/lang/fr/activities.php
resources/lang/fr/auth.php
resources/lang/fr/common.php
resources/lang/fr/components.php
resources/lang/fr/entities.php
resources/lang/fr/errors.php
resources/lang/fr/passwords.php
resources/lang/fr/settings.php
resources/lang/fr/validation.php
resources/lang/he/activities.php
resources/lang/he/auth.php
resources/lang/he/common.php
resources/lang/he/entities.php
resources/lang/he/settings.php
resources/lang/he/validation.php
resources/lang/hr/activities.php
resources/lang/hr/auth.php
resources/lang/hr/common.php
resources/lang/hr/entities.php
resources/lang/hr/settings.php
resources/lang/hr/validation.php
resources/lang/hu/activities.php
resources/lang/hu/auth.php
resources/lang/hu/common.php
resources/lang/hu/entities.php
resources/lang/hu/settings.php
resources/lang/hu/validation.php
resources/lang/id/activities.php
resources/lang/id/auth.php
resources/lang/id/common.php
resources/lang/id/entities.php
resources/lang/id/settings.php
resources/lang/id/validation.php
resources/lang/it/activities.php
resources/lang/it/auth.php
resources/lang/it/common.php
resources/lang/it/entities.php
resources/lang/it/settings.php
resources/lang/it/validation.php
resources/lang/ja/activities.php
resources/lang/ja/auth.php
resources/lang/ja/common.php
resources/lang/ja/entities.php
resources/lang/ja/settings.php
resources/lang/ja/validation.php
resources/lang/ko/activities.php
resources/lang/ko/auth.php
resources/lang/ko/common.php
resources/lang/ko/entities.php
resources/lang/ko/settings.php
resources/lang/ko/validation.php
resources/lang/lt/activities.php [new file with mode: 0644]
resources/lang/lt/auth.php [new file with mode: 0644]
resources/lang/lt/common.php [new file with mode: 0644]
resources/lang/lt/components.php [new file with mode: 0644]
resources/lang/lt/entities.php [new file with mode: 0644]
resources/lang/lt/errors.php [new file with mode: 0644]
resources/lang/lt/pagination.php [new file with mode: 0644]
resources/lang/lt/passwords.php [new file with mode: 0644]
resources/lang/lt/settings.php [new file with mode: 0644]
resources/lang/lt/validation.php [new file with mode: 0644]
resources/lang/lv/activities.php
resources/lang/lv/auth.php
resources/lang/lv/common.php
resources/lang/lv/entities.php
resources/lang/lv/settings.php
resources/lang/lv/validation.php
resources/lang/nb/activities.php
resources/lang/nb/auth.php
resources/lang/nb/common.php
resources/lang/nb/entities.php
resources/lang/nb/errors.php
resources/lang/nb/settings.php
resources/lang/nb/validation.php
resources/lang/nl/activities.php
resources/lang/nl/auth.php
resources/lang/nl/common.php
resources/lang/nl/entities.php
resources/lang/nl/settings.php
resources/lang/nl/validation.php
resources/lang/pl/activities.php
resources/lang/pl/auth.php
resources/lang/pl/common.php
resources/lang/pl/entities.php
resources/lang/pl/errors.php
resources/lang/pl/settings.php
resources/lang/pl/validation.php
resources/lang/pt/activities.php
resources/lang/pt/auth.php
resources/lang/pt/common.php
resources/lang/pt/entities.php
resources/lang/pt/settings.php
resources/lang/pt/validation.php
resources/lang/pt_BR/activities.php
resources/lang/pt_BR/auth.php
resources/lang/pt_BR/common.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/auth.php
resources/lang/ru/common.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/common.php
resources/lang/sk/entities.php
resources/lang/sk/errors.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/entities.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/entities.php
resources/lang/sv/settings.php
resources/lang/sv/validation.php
resources/lang/tr/activities.php
resources/lang/tr/auth.php
resources/lang/tr/common.php
resources/lang/tr/entities.php
resources/lang/tr/settings.php
resources/lang/tr/validation.php
resources/lang/uk/activities.php
resources/lang/uk/auth.php
resources/lang/uk/common.php
resources/lang/uk/entities.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/entities.php
resources/lang/vi/errors.php
resources/lang/vi/settings.php
resources/lang/vi/validation.php
resources/lang/zh_CN/activities.php
resources/lang/zh_CN/auth.php
resources/lang/zh_CN/common.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/auth.php
resources/lang/zh_TW/common.php
resources/lang/zh_TW/entities.php
resources/lang/zh_TW/errors.php
resources/lang/zh_TW/settings.php
resources/lang/zh_TW/validation.php
resources/sass/_forms.scss
resources/sass/_layout.scss
resources/sass/_text.scss
resources/views/api-docs/index.blade.php
resources/views/api-docs/parts/endpoint.blade.php [new file with mode: 0644]
resources/views/api-docs/parts/getting-started.blade.php [new file with mode: 0644]
resources/views/attachments/manager-edit-form.blade.php
resources/views/attachments/manager.blade.php
resources/views/auth/invite-set-password.blade.php
resources/views/auth/login.blade.php
resources/views/auth/parts/login-form-ldap.blade.php [moved from resources/views/auth/forms/login/ldap.blade.php with 100% similarity]
resources/views/auth/parts/login-form-saml2.blade.php [moved from resources/views/auth/forms/login/saml2.blade.php with 100% similarity]
resources/views/auth/parts/login-form-standard.blade.php [moved from resources/views/auth/forms/login/standard.blade.php with 95% similarity]
resources/views/auth/passwords/email.blade.php
resources/views/auth/passwords/reset.blade.php
resources/views/auth/register-confirm.blade.php
resources/views/auth/register.blade.php
resources/views/auth/user-unconfirmed.blade.php
resources/views/books/_breadcrumbs.blade.php [deleted file]
resources/views/books/create.blade.php
resources/views/books/delete.blade.php
resources/views/books/edit.blade.php
resources/views/books/export.blade.php
resources/views/books/index.blade.php
resources/views/books/parts/form.blade.php [moved from resources/views/books/form.blade.php with 92% similarity]
resources/views/books/parts/list-item.blade.php [moved from resources/views/books/list-item.blade.php with 100% similarity]
resources/views/books/parts/list.blade.php [moved from resources/views/books/list.blade.php with 84% similarity]
resources/views/books/parts/sort-box.blade.php [moved from resources/views/books/sort-box.blade.php with 100% similarity]
resources/views/books/permissions.blade.php
resources/views/books/show.blade.php
resources/views/books/sort.blade.php
resources/views/chapters/create.blade.php
resources/views/chapters/delete.blade.php
resources/views/chapters/edit.blade.php
resources/views/chapters/export.blade.php
resources/views/chapters/move.blade.php
resources/views/chapters/parts/child-menu.blade.php [moved from resources/views/chapters/child-menu.blade.php with 81% similarity]
resources/views/chapters/parts/form.blade.php [moved from resources/views/chapters/form.blade.php with 92% similarity]
resources/views/chapters/parts/list-item.blade.php [moved from resources/views/chapters/list-item.blade.php with 92% similarity]
resources/views/chapters/permissions.blade.php
resources/views/chapters/show.blade.php
resources/views/comments/comment.blade.php
resources/views/comments/create.blade.php
resources/views/common/activity-item.blade.php [moved from resources/views/partials/activity-item.blade.php with 100% similarity]
resources/views/common/activity-list.blade.php [moved from resources/views/partials/activity-list.blade.php with 76% similarity]
resources/views/common/custom-head.blade.php [moved from resources/views/partials/custom-head.blade.php with 55% similarity]
resources/views/common/custom-styles.blade.php [moved from resources/views/partials/custom-styles.blade.php with 100% similarity]
resources/views/common/dark-mode-toggle.blade.php [moved from resources/views/partials/dark-mode-toggle.blade.php with 100% similarity]
resources/views/common/detailed-listing-paginated.blade.php
resources/views/common/detailed-listing-with-more.blade.php
resources/views/common/export-custom-head.blade.php [new file with mode: 0644]
resources/views/common/export-styles.blade.php [moved from resources/views/partials/export-styles.blade.php with 100% similarity]
resources/views/common/header.blade.php
resources/views/common/loading-icon.blade.php [moved from resources/views/partials/loading-icon.blade.php with 100% similarity]
resources/views/common/notifications.blade.php [moved from resources/views/partials/notifications.blade.php with 100% similarity]
resources/views/common/skip-to-content.blade.php [moved from resources/views/common/parts/skip-to-content.blade.php with 100% similarity]
resources/views/entities/book-tree.blade.php [moved from resources/views/partials/book-tree.blade.php with 76% similarity]
resources/views/entities/breadcrumb-listing.blade.php [moved from resources/views/partials/breadcrumb-listing.blade.php with 95% similarity]
resources/views/entities/breadcrumbs.blade.php [moved from resources/views/partials/breadcrumbs.blade.php with 96% similarity]
resources/views/entities/export-menu.blade.php [moved from resources/views/partials/entity-export-menu.blade.php with 100% similarity]
resources/views/entities/export-meta.blade.php [moved from resources/views/partials/entity-export-meta.blade.php with 100% similarity]
resources/views/entities/favourite-action.blade.php [moved from resources/views/partials/entity-favourite-action.blade.php with 100% similarity]
resources/views/entities/grid-item.blade.php [moved from resources/views/partials/entity-grid-item.blade.php with 100% similarity]
resources/views/entities/list-basic.blade.php [moved from resources/views/partials/entity-list-basic.blade.php with 76% similarity]
resources/views/entities/list-item-basic.blade.php [moved from resources/views/partials/entity-list-item-basic.blade.php with 100% similarity]
resources/views/entities/list-item.blade.php [moved from resources/views/partials/entity-list-item.blade.php with 79% similarity]
resources/views/entities/list.blade.php [moved from resources/views/partials/entity-list.blade.php with 63% similarity]
resources/views/entities/meta.blade.php [moved from resources/views/partials/entity-meta.blade.php with 100% similarity]
resources/views/entities/search-form.blade.php [moved from resources/views/partials/entity-search-form.blade.php with 100% similarity]
resources/views/entities/search-results.blade.php [moved from resources/views/partials/entity-search-results.blade.php with 91% similarity]
resources/views/entities/selector-popup.blade.php [moved from resources/views/components/entity-selector-popup.blade.php with 88% similarity]
resources/views/entities/selector.blade.php [moved from resources/views/components/entity-selector.blade.php with 94% similarity]
resources/views/entities/sibling-navigation.blade.php [moved from resources/views/partials/entity-sibling-navigation.blade.php with 100% similarity]
resources/views/entities/sort.blade.php [moved from resources/views/partials/sort.blade.php with 100% similarity]
resources/views/entities/tag-list.blade.php [moved from resources/views/components/tag-list.blade.php with 100% similarity]
resources/views/entities/tag-manager-list.blade.php [moved from resources/views/components/tag-manager-list.blade.php with 100% similarity]
resources/views/entities/tag-manager.blade.php [moved from resources/views/components/tag-manager.blade.php with 84% similarity]
resources/views/entities/view-toggle.blade.php [moved from resources/views/partials/view-toggle.blade.php with 100% similarity]
resources/views/errors/404.blade.php
resources/views/errors/500.blade.php
resources/views/errors/503.blade.php
resources/views/form/checkbox.blade.php
resources/views/form/custom-checkbox.blade.php [moved from resources/views/components/custom-checkbox.blade.php with 100% similarity]
resources/views/form/dropzone.blade.php [moved from resources/views/components/dropzone.blade.php with 100% similarity]
resources/views/form/entity-permissions.blade.php
resources/views/form/image-picker.blade.php [moved from resources/views/components/image-picker.blade.php with 100% similarity]
resources/views/form/restriction-checkbox.blade.php
resources/views/form/role-checkboxes.blade.php
resources/views/form/toggle-switch.blade.php [moved from resources/views/components/toggle-switch.blade.php with 100% similarity]
resources/views/form/user-select-list.blade.php [moved from resources/views/components/user-select-list.blade.php with 100% similarity]
resources/views/form/user-select.blade.php [moved from resources/views/components/user-select.blade.php with 96% similarity]
resources/views/home/books.blade.php [moved from resources/views/common/home-book.blade.php with 63% similarity]
resources/views/home/default.blade.php [moved from resources/views/common/home.blade.php with 83% similarity]
resources/views/home/parts/expand-toggle.blade.php [moved from resources/views/components/expand-toggle.blade.php with 100% similarity]
resources/views/home/parts/sidebar.blade.php [moved from resources/views/common/home-sidebar.blade.php with 80% similarity]
resources/views/home/shelves.blade.php [moved from resources/views/common/home-shelves.blade.php with 63% similarity]
resources/views/home/specific-page.blade.php [moved from resources/views/common/home-custom.blade.php with 62% similarity]
resources/views/layouts/base.blade.php [moved from resources/views/base.blade.php with 87% similarity]
resources/views/layouts/export.blade.php [moved from resources/views/export-layout.blade.php with 68% similarity]
resources/views/layouts/simple.blade.php [moved from resources/views/simple-layout.blade.php with 90% similarity]
resources/views/layouts/tri.blade.php [moved from resources/views/tri-layout.blade.php with 98% similarity]
resources/views/mfa/backup-codes-generate.blade.php [new file with mode: 0644]
resources/views/mfa/parts/setup-method-row.blade.php [new file with mode: 0644]
resources/views/mfa/parts/verify-backup_codes.blade.php [new file with mode: 0644]
resources/views/mfa/parts/verify-totp.blade.php [new file with mode: 0644]
resources/views/mfa/setup.blade.php [new file with mode: 0644]
resources/views/mfa/totp-generate.blade.php [new file with mode: 0644]
resources/views/mfa/verify.blade.php [new file with mode: 0644]
resources/views/misc/robots.blade.php [moved from resources/views/common/robots.blade.php with 100% similarity]
resources/views/pages/copy.blade.php
resources/views/pages/delete.blade.php
resources/views/pages/edit.blade.php
resources/views/pages/export.blade.php
resources/views/pages/guest-create.blade.php
resources/views/pages/move.blade.php
resources/views/pages/parts/code-editor.blade.php [moved from resources/views/components/code-editor.blade.php with 100% similarity]
resources/views/pages/parts/editor-toolbox.blade.php [moved from resources/views/pages/editor-toolbox.blade.php with 86% similarity]
resources/views/pages/parts/form.blade.php [moved from resources/views/pages/form.blade.php with 97% similarity]
resources/views/pages/parts/image-manager-form.blade.php [moved from resources/views/components/image-manager-form.blade.php with 100% similarity]
resources/views/pages/parts/image-manager-list.blade.php [moved from resources/views/components/image-manager-list.blade.php with 100% similarity]
resources/views/pages/parts/image-manager.blade.php [moved from resources/views/components/image-manager.blade.php with 98% similarity]
resources/views/pages/parts/list-item.blade.php [moved from resources/views/pages/list-item.blade.php with 60% similarity]
resources/views/pages/parts/markdown-editor.blade.php [moved from resources/views/pages/markdown-editor.blade.php with 100% similarity]
resources/views/pages/parts/page-display.blade.php [moved from resources/views/pages/page-display.blade.php with 100% similarity]
resources/views/pages/parts/pointer.blade.php [moved from resources/views/pages/pointer.blade.php with 100% similarity]
resources/views/pages/parts/template-manager-list.blade.php [moved from resources/views/pages/template-manager-list.blade.php with 100% similarity]
resources/views/pages/parts/template-manager.blade.php [moved from resources/views/pages/template-manager.blade.php with 86% similarity]
resources/views/pages/parts/wysiwyg-editor.blade.php [moved from resources/views/pages/wysiwyg-editor.blade.php with 100% similarity]
resources/views/pages/permissions.blade.php
resources/views/pages/revision.blade.php
resources/views/pages/revisions.blade.php
resources/views/pages/show.blade.php
resources/views/pages/sidebar-tree-list.blade.php [deleted file]
resources/views/partials/export-custom-head.blade.php [deleted file]
resources/views/readme.md [new file with mode: 0644]
resources/views/search/all.blade.php
resources/views/search/parts/boolean-filter.blade.php [moved from resources/views/search/form/boolean-filter.blade.php with 100% similarity]
resources/views/search/parts/date-filter.blade.php [moved from resources/views/search/form/date-filter.blade.php with 100% similarity]
resources/views/search/parts/entity-ajax-list.blade.php [moved from resources/views/search/entity-ajax-list.blade.php with 77% similarity]
resources/views/search/parts/term-list.blade.php [moved from resources/views/search/form/term-list.blade.php with 100% similarity]
resources/views/search/parts/type-filter.blade.php [moved from resources/views/search/form/type-filter.blade.php with 100% similarity]
resources/views/settings/audit.blade.php
resources/views/settings/index.blade.php
resources/views/settings/maintenance.blade.php
resources/views/settings/parts/footer-links.blade.php [moved from resources/views/settings/footer-links.blade.php with 100% similarity]
resources/views/settings/parts/navbar-with-version.blade.php [moved from resources/views/settings/navbar-with-version.blade.php with 86% similarity]
resources/views/settings/parts/navbar.blade.php [moved from resources/views/settings/navbar.blade.php with 100% similarity]
resources/views/settings/parts/page-picker.blade.php [moved from resources/views/components/page-picker.blade.php with 100% similarity]
resources/views/settings/parts/setting-entity-color-picker.blade.php [moved from resources/views/components/setting-entity-color-picker.blade.php with 100% similarity]
resources/views/settings/parts/table-user.blade.php [moved from resources/views/partials/table-user.blade.php with 100% similarity]
resources/views/settings/recycle-bin/deletable-entity-list.blade.php [deleted file]
resources/views/settings/recycle-bin/destroy.blade.php
resources/views/settings/recycle-bin/index.blade.php
resources/views/settings/recycle-bin/parts/deletable-entity-list.blade.php [new file with mode: 0644]
resources/views/settings/recycle-bin/parts/entity-display-item.blade.php [moved from resources/views/partials/entity-display-item.blade.php with 100% similarity]
resources/views/settings/recycle-bin/restore.blade.php
resources/views/settings/roles/create.blade.php
resources/views/settings/roles/delete.blade.php
resources/views/settings/roles/edit.blade.php
resources/views/settings/roles/index.blade.php
resources/views/settings/roles/parts/checkbox.blade.php [moved from resources/views/settings/roles/checkbox.blade.php with 85% similarity]
resources/views/settings/roles/parts/form.blade.php [moved from resources/views/settings/roles/form.blade.php with 51% similarity]
resources/views/shelves/_breadcrumbs.blade.php [deleted file]
resources/views/shelves/create.blade.php
resources/views/shelves/delete.blade.php
resources/views/shelves/edit.blade.php
resources/views/shelves/index.blade.php
resources/views/shelves/parts/form.blade.php [moved from resources/views/shelves/form.blade.php with 95% similarity]
resources/views/shelves/parts/list-item.blade.php [moved from resources/views/shelves/list-item.blade.php with 100% similarity]
resources/views/shelves/parts/list.blade.php [moved from resources/views/shelves/list.blade.php with 83% similarity]
resources/views/shelves/permissions.blade.php
resources/views/shelves/show.blade.php
resources/views/users/api-tokens/create.blade.php
resources/views/users/api-tokens/delete.blade.php
resources/views/users/api-tokens/edit.blade.php
resources/views/users/api-tokens/parts/form.blade.php [moved from resources/views/users/api-tokens/form.blade.php with 100% similarity]
resources/views/users/api-tokens/parts/list.blade.php [moved from resources/views/users/api-tokens/list.blade.php with 100% similarity]
resources/views/users/create.blade.php
resources/views/users/delete.blade.php
resources/views/users/edit.blade.php
resources/views/users/index.blade.php
resources/views/users/parts/form.blade.php [moved from resources/views/users/form.blade.php with 98% similarity]
resources/views/users/profile.blade.php
routes/api.php
routes/web.php
tests/ActivityTrackingTest.php [deleted file]
tests/Api/ApiDocsTest.php
tests/Api/BooksApiTest.php
tests/Api/ChaptersApiTest.php
tests/Api/PagesApiTest.php
tests/AuditLogTest.php
tests/Auth/AuthTest.php
tests/Auth/LdapTest.php
tests/Auth/MfaConfigurationTest.php [new file with mode: 0644]
tests/Auth/MfaVerificationTest.php [new file with mode: 0644]
tests/Auth/Saml2Test.php
tests/Auth/SocialAuthTest.php
tests/Auth/UserInviteTest.php
tests/BrowserKitTest.php [deleted file]
tests/Commands/ResetMfaCommandTest.php [new file with mode: 0644]
tests/Entity/BookShelfTest.php
tests/Entity/BookTest.php
tests/Entity/ChapterTest.php
tests/Entity/EntityAccessTest.php [new file with mode: 0644]
tests/Entity/EntityTest.php [deleted file]
tests/Entity/ExportTest.php
tests/Entity/MarkdownTest.php [deleted file]
tests/Entity/PageContentTest.php
tests/Entity/PageDraftTest.php
tests/Entity/PageEditorTest.php [new file with mode: 0644]
tests/Entity/PageTest.php
tests/Entity/SortTest.php
tests/HomepageTest.php
tests/Permissions/EntityPermissionsTest.php
tests/Permissions/RolesTest.php
tests/PublicActionTest.php
tests/RecycleBinTest.php
tests/SecurityHeaderTest.php
tests/Settings/CustomHeadContentTest.php [new file with mode: 0644]
tests/Settings/FooterLinksTest.php [moved from tests/FooterLinksTest.php with 98% similarity]
tests/SharedTestHelpers.php
tests/TestCase.php
tests/TestResponse.php
tests/ThemeTest.php
tests/Unit/ConfigTest.php
tests/User/UserManagementTest.php
tests/User/UserPreferencesTest.php
tests/User/UserProfileTest.php

index 05383f04abcce2f08d732f2b08719cb5b3775a76..a0a1b72e6836bcab495a34c0f8633ea295edb644 100644 (file)
@@ -41,4 +41,4 @@ MAIL_HOST=localhost
 MAIL_PORT=1025
 MAIL_USERNAME=null
 MAIL_PASSWORD=null
-MAIL_ENCRYPTION=null
\ No newline at end of file
+MAIL_ENCRYPTION=null
index 26df8f3cb8e918102ddc73fe8fae0a812bc1d33e..5eb65c27f09f346fa806d48253ab3e2a26c87337 100644 (file)
@@ -42,6 +42,14 @@ APP_TIMEZONE=UTC
 # overrides can be made. Defaults to disabled.
 APP_THEME=false
 
+# Trusted Proxies
+# Used to indicate trust of systems that proxy to the application so
+# certain header values (Such as "X-Forwarded-For") can be used from the
+# incoming proxy request to provide origin detail.
+# Set to an IP address, or multiple comma seperated IP addresses.
+# Can alternatively be set to "*" to trust all proxy addresses.
+APP_PROXIES=null
+
 # Database details
 # Host can contain a port (localhost:3306) or a separate DB_PORT option can be used.
 DB_HOST=localhost
@@ -281,6 +289,12 @@ ALLOW_CONTENT_SCRIPTS=false
 # Contents of the robots.txt file can be overridden, making this option obsolete.
 ALLOW_ROBOTS=null
 
+# Allow server-side fetches to be performed to potentially unknown
+# and user-provided locations. Primarily used in exports when loading
+# in externally referenced assets.
+# Can be 'true' or 'false'.
+ALLOW_UNTRUSTED_SERVER_FETCHING=false
+
 # 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"
index baaea5d92261fc0035000e4a42fe715b76bc58bf..78dccde42e7c5acfdce9ca13a5fd547d3af1eb29 100644 (file)
@@ -167,3 +167,26 @@ whenwesober :: Indonesian
 Rem (remkovdhoef) :: Dutch
 syn7ax69 :: Bulgarian; Turkish
 Blaade :: French
+Behzad HosseinPoor (behzad.hp) :: Persian
+Ole Aldric (Swoy) :: Norwegian Bokmal
+fharis arabia (raednahdi) :: Arabic
+Alexander Predl (Harveyhase68) :: German
+Rem (Rem9000) :: Dutch
+Michał Stelmach (stelmach-web) :: Polish
+arniom :: French
+REMOVED_USER :: Turkish
+林祖年 (contagion) :: Chinese Traditional
+Siamak Guodarzi (siamakgoudarzi88) :: Persian
+Lis Maestrelo (lismtrl) :: Portuguese, Brazilian
+Nathanaël (nathanaelhoun) :: French
+A Ibnu Hibban (abd.ibnuhibban) :: Indonesian
+Frost-ZX :: Chinese Simplified
+Kuzma Simonov (ovmach) :: Russian
+Vojtěch Krystek (acantophis) :: Czech
+Michał Lipok (mLipok) :: Polish
+Nicolas Pawlak (Mikolajek) :: French; Polish; German
+Thomas Hansen (thomasdk81) :: Danish
+Hl2run :: Slovak
+Ngo Tri Hoai (trihoai) :: Vietnamese
+Atalonica :: Catalan
+慕容潭谈 (591442386) :: Chinese Simplified
index dce7dc7b2595df00a1652bcfbcce7124de811b62..bc7a6b6b7c3353f39384ae73d88eaca284d5ed41 100644 (file)
@@ -55,9 +55,12 @@ class ActivityService
      */
     protected function newActivityForUser(string $type): Activity
     {
+        $ip = request()->ip() ?? '';
+
         return $this->activity->newInstance()->forceFill([
             'type'     => strtolower($type),
             'user_id'  => user()->id,
+            'ip'       => config('app.env') === 'demo' ? '127.0.0.1' : $ip,
         ]);
     }
 
index a19f143b7be0d3dacfc5f46b865db67f9c83c084..60b1630e075121693e4084f7940bd5ac9b793fb1 100644 (file)
@@ -50,4 +50,7 @@ class ActivityType
     const AUTH_PASSWORD_RESET_UPDATE = 'auth_password_reset_update';
     const AUTH_LOGIN = 'auth_login';
     const AUTH_REGISTER = 'auth_register';
+
+    const MFA_SETUP_METHOD = 'mfa_setup_method';
+    const MFA_REMOVE_METHOD = 'mfa_remove_method';
 }
index ef390939e2fe7f65c98a151364ced89788294864..34fd84709ec1d746bd84c2de87854a3845743b01 100644 (file)
@@ -7,10 +7,11 @@ use BookStack\Traits\HasCreatorAndUpdater;
 use Illuminate\Database\Eloquent\Relations\MorphTo;
 
 /**
- * @property string text
- * @property string html
- * @property int|null parent_id
- * @property int local_id
+ * @property int      $id
+ * @property string   $text
+ * @property string   $html
+ * @property int|null $parent_id
+ * @property int      $local_id
  */
 class Comment extends Model
 {
index ca65b78e8e2dab7b7742204f6decd8e0c5a908f2..b892efe577901191c4a7fea292e134eefbff86ea 100644 (file)
@@ -4,8 +4,8 @@ namespace BookStack\Actions;
 
 use BookStack\Auth\Permissions\PermissionService;
 use BookStack\Entities\Models\Entity;
-use DB;
 use Illuminate\Support\Collection;
+use Illuminate\Support\Facades\DB;
 
 class TagRepo
 {
index 75ed5cb3567b041afaf599b4dc17c20735e2207a..8b9cbc8e1b44ebff4ed2586336bc74b19aed600d 100644 (file)
@@ -2,6 +2,7 @@
 
 namespace BookStack\Api;
 
+use BookStack\Auth\Access\LoginService;
 use BookStack\Exceptions\ApiAuthException;
 use Illuminate\Auth\GuardHelpers;
 use Illuminate\Contracts\Auth\Authenticatable;
@@ -19,6 +20,11 @@ class ApiTokenGuard implements Guard
      */
     protected $request;
 
+    /**
+     * @var LoginService
+     */
+    protected $loginService;
+
     /**
      * The last auth exception thrown in this request.
      *
@@ -29,9 +35,10 @@ class ApiTokenGuard implements Guard
     /**
      * ApiTokenGuard constructor.
      */
-    public function __construct(Request $request)
+    public function __construct(Request $request, LoginService $loginService)
     {
         $this->request = $request;
+        $this->loginService = $loginService;
     }
 
     /**
@@ -95,6 +102,10 @@ class ApiTokenGuard implements Guard
 
         $this->validateToken($token, $secret);
 
+        if ($this->loginService->awaitingEmailConfirmation($token->user)) {
+            throw new ApiAuthException(trans('errors.email_confirmation_awaiting'));
+        }
+
         return $token->user;
     }
 
index 3425c1e720de0416f066018f436e15c9c06eae54..9c357d95f955f8dfde8227bd3c4525fd056d5d21 100644 (file)
@@ -15,8 +15,6 @@ class EmailConfirmationService extends UserTokenService
      * Create new confirmation for a user,
      * Also removes any existing old ones.
      *
-     * @param User $user
-     *
      * @throws ConfirmationEmailException
      */
     public function sendConfirmation(User $user)
@@ -33,8 +31,6 @@ class EmailConfirmationService extends UserTokenService
 
     /**
      * Check if confirmation is required in this instance.
-     *
-     * @return bool
      */
     public function confirmationRequired(): bool
     {
index 9c3b47e977dde54bca48f7a387cc4d3046bdac79..99bfd2e795ecd4f12198d21ea2ae97d39062af87 100644 (file)
@@ -186,12 +186,8 @@ class ExternalBaseSessionGuard implements StatefulGuard
      */
     public function loginUsingId($id, $remember = false)
     {
-        if (!is_null($user = $this->provider->retrieveById($id))) {
-            $this->login($user, $remember);
-
-            return $user;
-        }
-
+        // Always return false as to disable this method,
+        // Logins should route through LoginService.
         return false;
     }
 
diff --git a/app/Auth/Access/LoginService.php b/app/Auth/Access/LoginService.php
new file mode 100644 (file)
index 0000000..e02296b
--- /dev/null
@@ -0,0 +1,164 @@
+<?php
+
+namespace BookStack\Auth\Access;
+
+use BookStack\Actions\ActivityType;
+use BookStack\Auth\Access\Mfa\MfaSession;
+use BookStack\Auth\User;
+use BookStack\Exceptions\StoppedAuthenticationException;
+use BookStack\Facades\Activity;
+use BookStack\Facades\Theme;
+use BookStack\Theming\ThemeEvents;
+use Exception;
+
+class LoginService
+{
+    protected const LAST_LOGIN_ATTEMPTED_SESSION_KEY = 'auth-login-last-attempted';
+
+    protected $mfaSession;
+    protected $emailConfirmationService;
+
+    public function __construct(MfaSession $mfaSession, EmailConfirmationService $emailConfirmationService)
+    {
+        $this->mfaSession = $mfaSession;
+        $this->emailConfirmationService = $emailConfirmationService;
+    }
+
+    /**
+     * Log the given user into the system.
+     * Will start a login of the given user but will prevent if there's
+     * a reason to (MFA or Unconfirmed Email).
+     * Returns a boolean to indicate the current login result.
+     *
+     * @throws StoppedAuthenticationException
+     */
+    public function login(User $user, string $method, bool $remember = false): void
+    {
+        if ($this->awaitingEmailConfirmation($user) || $this->needsMfaVerification($user)) {
+            $this->setLastLoginAttemptedForUser($user, $method, $remember);
+
+            throw new StoppedAuthenticationException($user, $this);
+        }
+
+        $this->clearLastLoginAttempted();
+        auth()->login($user, $remember);
+        Activity::add(ActivityType::AUTH_LOGIN, "{$method}; {$user->logDescriptor()}");
+        Theme::dispatch(ThemeEvents::AUTH_LOGIN, $method, $user);
+
+        // Authenticate on all session guards if a likely admin
+        if ($user->can('users-manage') && $user->can('user-roles-manage')) {
+            $guards = ['standard', 'ldap', 'saml2'];
+            foreach ($guards as $guard) {
+                auth($guard)->login($user);
+            }
+        }
+    }
+
+    /**
+     * Reattempt a system login after a previous stopped attempt.
+     *
+     * @throws Exception
+     */
+    public function reattemptLoginFor(User $user)
+    {
+        if ($user->id !== ($this->getLastLoginAttemptUser()->id ?? null)) {
+            throw new Exception('Login reattempt user does align with current session state');
+        }
+
+        $lastLoginDetails = $this->getLastLoginAttemptDetails();
+        $this->login($user, $lastLoginDetails['method'], $lastLoginDetails['remember'] ?? false);
+    }
+
+    /**
+     * Get the last user that was attempted to be logged in.
+     * Only exists if the last login attempt had correct credentials
+     * but had been prevented by a secondary factor.
+     */
+    public function getLastLoginAttemptUser(): ?User
+    {
+        $id = $this->getLastLoginAttemptDetails()['user_id'];
+
+        return User::query()->where('id', '=', $id)->first();
+    }
+
+    /**
+     * Get the details of the last login attempt.
+     * Checks upon a ttl of about 1 hour since that last attempted login.
+     *
+     * @return array{user_id: ?string, method: ?string, remember: bool}
+     */
+    protected function getLastLoginAttemptDetails(): array
+    {
+        $value = session()->get(self::LAST_LOGIN_ATTEMPTED_SESSION_KEY);
+        if (!$value) {
+            return ['user_id' => null, 'method' => null];
+        }
+
+        [$id, $method, $remember, $time] = explode(':', $value);
+        $hourAgo = time() - (60 * 60);
+        if ($time < $hourAgo) {
+            $this->clearLastLoginAttempted();
+
+            return ['user_id' => null, 'method' => null];
+        }
+
+        return ['user_id' => $id, 'method' => $method, 'remember' => boolval($remember)];
+    }
+
+    /**
+     * Set the last login attempted user.
+     * Must be only used when credentials are correct and a login could be
+     * achieved but a secondary factor has stopped the login.
+     */
+    protected function setLastLoginAttemptedForUser(User $user, string $method, bool $remember)
+    {
+        session()->put(
+            self::LAST_LOGIN_ATTEMPTED_SESSION_KEY,
+            implode(':', [$user->id, $method, $remember, time()])
+        );
+    }
+
+    /**
+     * Clear the last login attempted session value.
+     */
+    protected function clearLastLoginAttempted(): void
+    {
+        session()->remove(self::LAST_LOGIN_ATTEMPTED_SESSION_KEY);
+    }
+
+    /**
+     * Check if MFA verification is needed.
+     */
+    public function needsMfaVerification(User $user): bool
+    {
+        return !$this->mfaSession->isVerifiedForUser($user) && $this->mfaSession->isRequiredForUser($user);
+    }
+
+    /**
+     * Check if the given user is awaiting email confirmation.
+     */
+    public function awaitingEmailConfirmation(User $user): bool
+    {
+        return $this->emailConfirmationService->confirmationRequired() && !$user->email_confirmed;
+    }
+
+    /**
+     * Attempt the login of a user using the given credentials.
+     * Meant to mirror Laravel's default guard 'attempt' method
+     * but in a manner that always routes through our login system.
+     * May interrupt the flow if extra authentication requirements are imposed.
+     *
+     * @throws StoppedAuthenticationException
+     */
+    public function attempt(array $credentials, string $method, bool $remember = false): bool
+    {
+        $result = auth()->attempt($credentials, $remember);
+        if ($result) {
+            $user = auth()->user();
+            auth()->logout();
+            $this->login($user, $method, $remember);
+        }
+
+        return $result;
+    }
+}
diff --git a/app/Auth/Access/Mfa/BackupCodeService.php b/app/Auth/Access/Mfa/BackupCodeService.php
new file mode 100644 (file)
index 0000000..d58d28a
--- /dev/null
@@ -0,0 +1,62 @@
+<?php
+
+namespace BookStack\Auth\Access\Mfa;
+
+use Illuminate\Support\Str;
+
+class BackupCodeService
+{
+    /**
+     * Generate a new set of 16 backup codes.
+     */
+    public function generateNewSet(): array
+    {
+        $codes = [];
+        while (count($codes) < 16) {
+            $code = Str::random(5) . '-' . Str::random(5);
+            if (!in_array($code, $codes)) {
+                $codes[] = strtolower($code);
+            }
+        }
+
+        return $codes;
+    }
+
+    /**
+     * Check if the given code matches one of the available options.
+     */
+    public function inputCodeExistsInSet(string $code, string $codeSet): bool
+    {
+        $cleanCode = $this->cleanInputCode($code);
+        $codes = json_decode($codeSet);
+
+        return in_array($cleanCode, $codes);
+    }
+
+    /**
+     * Remove the given input code from the given available options.
+     * Will return a JSON string containing the codes.
+     */
+    public function removeInputCodeFromSet(string $code, string $codeSet): string
+    {
+        $cleanCode = $this->cleanInputCode($code);
+        $codes = json_decode($codeSet);
+        $pos = array_search($cleanCode, $codes, true);
+        array_splice($codes, $pos, 1);
+
+        return json_encode($codes);
+    }
+
+    /**
+     * Count the number of codes in the given set.
+     */
+    public function countCodesInSet(string $codeSet): int
+    {
+        return count(json_decode($codeSet));
+    }
+
+    protected function cleanInputCode(string $code): string
+    {
+        return strtolower(str_replace(' ', '-', trim($code)));
+    }
+}
diff --git a/app/Auth/Access/Mfa/MfaSession.php b/app/Auth/Access/Mfa/MfaSession.php
new file mode 100644 (file)
index 0000000..72163b5
--- /dev/null
@@ -0,0 +1,60 @@
+<?php
+
+namespace BookStack\Auth\Access\Mfa;
+
+use BookStack\Auth\User;
+
+class MfaSession
+{
+    /**
+     * Check if MFA is required for the given user.
+     */
+    public function isRequiredForUser(User $user): bool
+    {
+        // TODO - Test both these cases
+        return $user->mfaValues()->exists() || $this->userRoleEnforcesMfa($user);
+    }
+
+    /**
+     * Check if the given user is pending MFA setup.
+     * (MFA required but not yet configured).
+     */
+    public function isPendingMfaSetup(User $user): bool
+    {
+        return $this->isRequiredForUser($user) && !$user->mfaValues()->exists();
+    }
+
+    /**
+     * Check if a role of the given user enforces MFA.
+     */
+    protected function userRoleEnforcesMfa(User $user): bool
+    {
+        return $user->roles()
+            ->where('mfa_enforced', '=', true)
+            ->exists();
+    }
+
+    /**
+     * Check if the current MFA session has already been verified for the given user.
+     */
+    public function isVerifiedForUser(User $user): bool
+    {
+        return session()->get($this->getMfaVerifiedSessionKey($user)) === 'true';
+    }
+
+    /**
+     * Mark the current session as MFA-verified.
+     */
+    public function markVerifiedForUser(User $user): void
+    {
+        session()->put($this->getMfaVerifiedSessionKey($user), 'true');
+    }
+
+    /**
+     * Get the session key in which the MFA verification status is stored.
+     */
+    protected function getMfaVerifiedSessionKey(User $user): string
+    {
+        return 'mfa-verification-passed:' . $user->id;
+    }
+}
diff --git a/app/Auth/Access/Mfa/MfaValue.php b/app/Auth/Access/Mfa/MfaValue.php
new file mode 100644 (file)
index 0000000..8f07c66
--- /dev/null
@@ -0,0 +1,76 @@
+<?php
+
+namespace BookStack\Auth\Access\Mfa;
+
+use BookStack\Auth\User;
+use Carbon\Carbon;
+use Illuminate\Database\Eloquent\Model;
+
+/**
+ * @property int    $id
+ * @property int    $user_id
+ * @property string $method
+ * @property string $value
+ * @property Carbon $created_at
+ * @property Carbon $updated_at
+ */
+class MfaValue extends Model
+{
+    protected static $unguarded = true;
+
+    const METHOD_TOTP = 'totp';
+    const METHOD_BACKUP_CODES = 'backup_codes';
+
+    /**
+     * Get all the MFA methods available.
+     */
+    public static function allMethods(): array
+    {
+        return [self::METHOD_TOTP, self::METHOD_BACKUP_CODES];
+    }
+
+    /**
+     * Upsert a new MFA value for the given user and method
+     * using the provided value.
+     */
+    public static function upsertWithValue(User $user, string $method, string $value): void
+    {
+        /** @var MfaValue $mfaVal */
+        $mfaVal = static::query()->firstOrNew([
+            'user_id' => $user->id,
+            'method'  => $method,
+        ]);
+        $mfaVal->setValue($value);
+        $mfaVal->save();
+    }
+
+    /**
+     * Easily get the decrypted MFA value for the given user and method.
+     */
+    public static function getValueForUser(User $user, string $method): ?string
+    {
+        /** @var MfaValue $mfaVal */
+        $mfaVal = static::query()
+            ->where('user_id', '=', $user->id)
+            ->where('method', '=', $method)
+            ->first();
+
+        return $mfaVal ? $mfaVal->getValue() : null;
+    }
+
+    /**
+     * Decrypt the value attribute upon access.
+     */
+    protected function getValue(): string
+    {
+        return decrypt($this->value);
+    }
+
+    /**
+     * Encrypt the value attribute upon access.
+     */
+    protected function setValue($value): void
+    {
+        $this->value = encrypt($value);
+    }
+}
diff --git a/app/Auth/Access/Mfa/TotpService.php b/app/Auth/Access/Mfa/TotpService.php
new file mode 100644 (file)
index 0000000..0d9bd37
--- /dev/null
@@ -0,0 +1,72 @@
+<?php
+
+namespace BookStack\Auth\Access\Mfa;
+
+use BaconQrCode\Renderer\Color\Rgb;
+use BaconQrCode\Renderer\Image\SvgImageBackEnd;
+use BaconQrCode\Renderer\ImageRenderer;
+use BaconQrCode\Renderer\RendererStyle\Fill;
+use BaconQrCode\Renderer\RendererStyle\RendererStyle;
+use BaconQrCode\Writer;
+use PragmaRX\Google2FA\Google2FA;
+use PragmaRX\Google2FA\Support\Constants;
+
+class TotpService
+{
+    protected $google2fa;
+
+    public function __construct(Google2FA $google2fa)
+    {
+        $this->google2fa = $google2fa;
+        // Use SHA1 as a default, Personal testing of other options in 2021 found
+        // many apps lack support for other algorithms yet still will scan
+        // the code causing a confusing UX.
+        $this->google2fa->setAlgorithm(Constants::SHA1);
+    }
+
+    /**
+     * Generate a new totp secret key.
+     */
+    public function generateSecret(): string
+    {
+        /** @noinspection PhpUnhandledExceptionInspection */
+        return $this->google2fa->generateSecretKey();
+    }
+
+    /**
+     * Generate a TOTP URL from secret key.
+     */
+    public function generateUrl(string $secret): string
+    {
+        return $this->google2fa->getQRCodeUrl(
+            setting('app-name'),
+            user()->email,
+            $secret
+        );
+    }
+
+    /**
+     * Generate a QR code to display a TOTP URL.
+     */
+    public function generateQrCodeSvg(string $url): string
+    {
+        $color = Fill::uniformColor(new Rgb(255, 255, 255), new Rgb(32, 110, 167));
+
+        return (new Writer(
+            new ImageRenderer(
+                new RendererStyle(192, 4, null, null, $color),
+                new SvgImageBackEnd()
+            )
+        ))->writeString($url);
+    }
+
+    /**
+     * Verify that the user provided code is valid for the secret.
+     * The secret must be known, not user-provided.
+     */
+    public function verifyCode(string $code, string $secret): bool
+    {
+        /** @noinspection PhpUnhandledExceptionInspection */
+        return $this->google2fa->verifyKey($secret, $code);
+    }
+}
diff --git a/app/Auth/Access/Mfa/TotpValidationRule.php b/app/Auth/Access/Mfa/TotpValidationRule.php
new file mode 100644 (file)
index 0000000..22cb7da
--- /dev/null
@@ -0,0 +1,37 @@
+<?php
+
+namespace BookStack\Auth\Access\Mfa;
+
+use Illuminate\Contracts\Validation\Rule;
+
+class TotpValidationRule implements Rule
+{
+    protected $secret;
+    protected $totpService;
+
+    /**
+     * Create a new rule instance.
+     * Takes the TOTP secret that must be system provided, not user provided.
+     */
+    public function __construct(string $secret)
+    {
+        $this->secret = $secret;
+        $this->totpService = app()->make(TotpService::class);
+    }
+
+    /**
+     * Determine if the validation rule passes.
+     */
+    public function passes($attribute, $value)
+    {
+        return $this->totpService->verifyCode($value, $this->secret);
+    }
+
+    /**
+     * Get the validation error message.
+     */
+    public function message()
+    {
+        return trans('validation.totp');
+    }
+}
index 28d4d40303242bca8f96c8c60b173dc721279873..6cbfdac0b2808a646ea284fab404aa01dbc2fa21 100644 (file)
@@ -2,14 +2,11 @@
 
 namespace BookStack\Auth\Access;
 
-use BookStack\Actions\ActivityType;
 use BookStack\Auth\User;
 use BookStack\Exceptions\JsonDebugException;
 use BookStack\Exceptions\SamlException;
+use BookStack\Exceptions\StoppedAuthenticationException;
 use BookStack\Exceptions\UserRegistrationException;
-use BookStack\Facades\Activity;
-use BookStack\Facades\Theme;
-use BookStack\Theming\ThemeEvents;
 use Exception;
 use Illuminate\Support\Str;
 use OneLogin\Saml2\Auth;
@@ -25,16 +22,16 @@ class Saml2Service extends ExternalAuthService
 {
     protected $config;
     protected $registrationService;
-    protected $user;
+    protected $loginService;
 
     /**
      * Saml2Service constructor.
      */
-    public function __construct(RegistrationService $registrationService, User $user)
+    public function __construct(RegistrationService $registrationService, LoginService $loginService)
     {
         $this->config = config('saml2');
         $this->registrationService = $registrationService;
-        $this->user = $user;
+        $this->loginService = $loginService;
     }
 
     /**
@@ -332,7 +329,7 @@ class Saml2Service extends ExternalAuthService
      */
     protected function getOrRegisterUser(array $userDetails): ?User
     {
-        $user = $this->user->newQuery()
+        $user = User::query()
           ->where('external_auth_id', '=', $userDetails['external_id'])
           ->first();
 
@@ -357,6 +354,7 @@ class Saml2Service extends ExternalAuthService
      * @throws SamlException
      * @throws JsonDebugException
      * @throws UserRegistrationException
+     * @throws StoppedAuthenticationException
      */
     public function processLoginCallback(string $samlID, array $samlAttributes): User
     {
@@ -389,9 +387,7 @@ class Saml2Service extends ExternalAuthService
             $this->syncWithGroups($user, $groups);
         }
 
-        auth()->login($user);
-        Activity::add(ActivityType::AUTH_LOGIN, "saml2; {$user->logDescriptor()}");
-        Theme::dispatch(ThemeEvents::AUTH_LOGIN, 'saml2', $user);
+        $this->loginService->login($user, 'saml2');
 
         return $user;
     }
index 2f1a6876a13e5a753d5d437dd24e49a36fda81a7..d165e76b121bbe2b6f5064c1b844906272d04f99 100644 (file)
@@ -2,15 +2,11 @@
 
 namespace BookStack\Auth\Access;
 
-use BookStack\Actions\ActivityType;
 use BookStack\Auth\SocialAccount;
 use BookStack\Auth\User;
 use BookStack\Exceptions\SocialDriverNotConfigured;
 use BookStack\Exceptions\SocialSignInAccountNotUsed;
 use BookStack\Exceptions\UserRegistrationException;
-use BookStack\Facades\Activity;
-use BookStack\Facades\Theme;
-use BookStack\Theming\ThemeEvents;
 use Illuminate\Support\Facades\Event;
 use Illuminate\Support\Str;
 use Laravel\Socialite\Contracts\Factory as Socialite;
@@ -28,6 +24,11 @@ class SocialAuthService
      */
     protected $socialite;
 
+    /**
+     * @var LoginService
+     */
+    protected $loginService;
+
     /**
      * The default built-in social drivers we support.
      *
@@ -59,9 +60,10 @@ class SocialAuthService
     /**
      * SocialAuthService constructor.
      */
-    public function __construct(Socialite $socialite)
+    public function __construct(Socialite $socialite, LoginService $loginService)
     {
         $this->socialite = $socialite;
+        $this->loginService = $loginService;
     }
 
     /**
@@ -139,9 +141,7 @@ class SocialAuthService
         // When a user is not logged in and a matching SocialAccount exists,
         // Simply log the user into the application.
         if (!$isLoggedIn && $socialAccount !== null) {
-            auth()->login($socialAccount->user);
-            Activity::add(ActivityType::AUTH_LOGIN, $socialAccount);
-            Theme::dispatch(ThemeEvents::AUTH_LOGIN, $socialDriver, $socialAccount->user);
+            $this->loginService->login($socialAccount->user, $socialDriver);
 
             return redirect()->intended('/');
         }
index 565dcb948b4caab36ee2056784d200241417fa27..ffd828ab5095194b8df7ab638a73980215095c52 100644 (file)
@@ -6,7 +6,7 @@ use BookStack\Auth\User;
 use BookStack\Exceptions\UserTokenExpiredException;
 use BookStack\Exceptions\UserTokenNotFoundException;
 use Carbon\Carbon;
-use Illuminate\Database\Connection as Database;
+use Illuminate\Support\Facades\DB;
 use Illuminate\Support\Str;
 use stdClass;
 
@@ -26,18 +26,6 @@ class UserTokenService
      */
     protected $expiryTime = 24;
 
-    protected $db;
-
-    /**
-     * UserTokenService constructor.
-     *
-     * @param Database $db
-     */
-    public function __construct(Database $db)
-    {
-        $this->db = $db;
-    }
-
     /**
      * Delete all email confirmations that belong to a user.
      *
@@ -47,7 +35,7 @@ class UserTokenService
      */
     public function deleteByUser(User $user)
     {
-        return $this->db->table($this->tokenTable)
+        return DB::table($this->tokenTable)
             ->where('user_id', '=', $user->id)
             ->delete();
     }
@@ -102,7 +90,7 @@ class UserTokenService
     protected function createTokenForUser(User $user): string
     {
         $token = $this->generateToken();
-        $this->db->table($this->tokenTable)->insert([
+        DB::table($this->tokenTable)->insert([
             'user_id'    => $user->id,
             'token'      => $token,
             'created_at' => Carbon::now(),
@@ -121,7 +109,7 @@ class UserTokenService
      */
     protected function tokenExists(string $token): bool
     {
-        return $this->db->table($this->tokenTable)
+        return DB::table($this->tokenTable)
             ->where('token', '=', $token)->exists();
     }
 
@@ -134,7 +122,7 @@ class UserTokenService
      */
     protected function getEntryByToken(string $token)
     {
-        return $this->db->table($this->tokenTable)
+        return DB::table($this->tokenTable)
             ->where('token', '=', $token)
             ->first();
     }
index f84f518944ab81f2d95e8fee776f7e00b8cde8ba..139725339717edb04175d64a8e849b0226afe41d 100644 (file)
@@ -603,7 +603,7 @@ class PermissionService
     /**
      * Filter items that have entities set as a polymorphic relation.
      *
-     * @param Builder|\Illuminate\Database\Query\Builder $query
+     * @param Builder|QueryBuilder $query
      */
     public function filterRestrictedEntityRelations($query, string $tableName, string $entityIdColumn, string $entityTypeColumn, string $action = 'view')
     {
@@ -611,9 +611,10 @@ class PermissionService
 
         $q = $query->where(function ($query) use ($tableDetails, $action) {
             $query->whereExists(function ($permissionQuery) use (&$tableDetails, $action) {
+                /** @var Builder $permissionQuery */
                 $permissionQuery->select(['role_id'])->from('joint_permissions')
-                    ->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
-                    ->whereRaw('joint_permissions.entity_type=' . $tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn'])
+                    ->whereColumn('joint_permissions.entity_id', '=', $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
+                    ->whereColumn('joint_permissions.entity_type', '=', $tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn'])
                     ->where('action', '=', $action)
                     ->whereIn('role_id', $this->getCurrentUserRoles())
                     ->where(function (QueryBuilder $query) {
@@ -639,8 +640,9 @@ class PermissionService
         $q = $query->where(function ($query) use ($tableDetails, $morphClass) {
             $query->where(function ($query) use (&$tableDetails, $morphClass) {
                 $query->whereExists(function ($permissionQuery) use (&$tableDetails, $morphClass) {
+                    /** @var Builder $permissionQuery */
                     $permissionQuery->select('id')->from('joint_permissions')
-                        ->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
+                        ->whereColumn('joint_permissions.entity_id', '=', $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
                         ->where('entity_type', '=', $morphClass)
                         ->where('action', '=', 'view')
                         ->whereIn('role_id', $this->getCurrentUserRoles())
index 4d191679da64b7125ed9c2a895275aef53144e40..988146700f80e1760c6d667ac0fe29dc0de22542 100644 (file)
@@ -57,6 +57,7 @@ class PermissionsRepo
     public function saveNewRole(array $roleData): Role
     {
         $role = $this->role->newInstance($roleData);
+        $role->mfa_enforced = ($roleData['mfa_enforced'] ?? 'false') === 'true';
         $role->save();
 
         $permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
@@ -90,6 +91,7 @@ class PermissionsRepo
         $this->assignRolePermissions($role, $permissions);
 
         $role->fill($roleData);
+        $role->mfa_enforced = ($roleData['mfa_enforced'] ?? 'false') === 'true';
         $role->save();
         $this->permissionService->buildJointPermissionForRole($role);
         Activity::add(ActivityType::ROLE_UPDATE, $role);
index 94ba39d1d1f15fb7952f51fa18664a6440e3ae95..46921caeb1a2adebb9255088c39dd1c13489497b 100644 (file)
@@ -13,11 +13,13 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
 /**
  * Class Role.
  *
- * @property int    $id
- * @property string $display_name
- * @property string $description
- * @property string $external_auth_id
- * @property string $system_name
+ * @property int        $id
+ * @property string     $display_name
+ * @property string     $description
+ * @property string     $external_auth_id
+ * @property string     $system_name
+ * @property bool       $mfa_enforced
+ * @property Collection $users
  */
 class Role extends Model implements Loggable
 {
index 1a3560691d4db8792c2fe18f52322c608f956dac..0a6849fe008323aca74f08cf108441a78b59a0c6 100644 (file)
@@ -4,6 +4,7 @@ namespace BookStack\Auth;
 
 use BookStack\Actions\Favourite;
 use BookStack\Api\ApiToken;
+use BookStack\Auth\Access\Mfa\MfaValue;
 use BookStack\Entities\Tools\SlugGenerator;
 use BookStack\Interfaces\Loggable;
 use BookStack\Interfaces\Sluggable;
@@ -38,6 +39,7 @@ use Illuminate\Support\Collection;
  * @property string     $external_auth_id
  * @property string     $system_name
  * @property Collection $roles
+ * @property Collection $mfaValues
  */
 class User extends Model implements AuthenticatableContract, CanResetPasswordContract, Loggable, Sluggable
 {
@@ -265,6 +267,14 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
         return $this->hasMany(Favourite::class);
     }
 
+    /**
+     * Get the MFA values belonging to this use.
+     */
+    public function mfaValues(): HasMany
+    {
+        return $this->hasMany(MfaValue::class);
+    }
+
     /**
      * Get the last activity time for this user.
      */
index 61ca12dcc8f7d00e410170e0567e3b107ca0dc86..6d48f12402060edbbe56f5660301dc1183ca5dcc 100644 (file)
@@ -15,7 +15,7 @@ use Exception;
 use Illuminate\Database\Eloquent\Builder;
 use Illuminate\Database\Eloquent\Collection;
 use Illuminate\Pagination\LengthAwarePaginator;
-use Log;
+use Illuminate\Support\Facades\Log;
 
 class UserRepo
 {
@@ -71,6 +71,7 @@ class UserRepo
         $query = User::query()->select(['*'])
             ->withLastActivityAt()
             ->with(['roles', 'avatar'])
+            ->withCount('mfaValues')
             ->orderBy($sort, $sortData['order']);
 
         if ($sortData['search']) {
@@ -188,6 +189,7 @@ class UserRepo
         $user->socialAccounts()->delete();
         $user->apiTokens()->delete();
         $user->favourites()->delete();
+        $user->mfaValues()->delete();
         $user->delete();
 
         // Delete user profile images
index 9d8ba7eb2d0d71f9dbaee50be53a9c91783bf80e..120644aede9b3e048e38120719ff10a4a92549eb 100755 (executable)
@@ -36,6 +36,11 @@ return [
     // Even when overridden the WYSIWYG editor may still escape script content.
     'allow_content_scripts' => env('ALLOW_CONTENT_SCRIPTS', false),
 
+    // Allow server-side fetches to be performed to potentially unknown
+    // and user-provided locations. Primarily used in exports when loading
+    // in externally referenced assets.
+    'allow_untrusted_server_fetching' => env('ALLOW_UNTRUSTED_SERVER_FETCHING', false),
+
     // Override the default behaviour for allowing crawlers to crawl the instance.
     // May be ignored if view has be overridden or modified.
     // Defaults to null since, if not set, 'app-public' status used instead.
@@ -56,7 +61,7 @@ return [
     'locale' => env('APP_LANG', 'en'),
 
     // Locales available
-    'locales' => ['en', 'ar', 'bg', 'bs', 'ca', 'cs', 'da', 'de', 'de_informal', 'es', 'es_AR', 'fa', 'fr', 'he', 'hr', 'hu', 'id', 'it', 'ja', 'ko', 'lv', 'nl', 'nb', 'pt', 'pt_BR', 'sk', 'sl', 'sv', 'pl',  'ru', 'th', 'tr', 'uk', 'vi', 'zh_CN', 'zh_TW'],
+    'locales' => ['en', 'ar', 'bg', 'bs', 'ca', 'cs', 'da', 'de', 'de_informal', 'es', 'es_AR', 'fa', 'fr', 'he', 'hr', 'hu', 'id', 'it', 'ja', 'ko', 'lt', 'lv', 'nl', 'nb', 'pt', 'pt_BR', 'sk', 'sl', 'sv', 'pl',  'ru', 'th', 'tr', 'uk', 'vi', 'zh_CN', 'zh_TW'],
 
     //  Application Fallback Locale
     'fallback_locale' => 'en',
index 7fb51a13bf0664c3994447d2df4cf20591dccdc8..0c696609526fa5fc02fca74e975f076f87ee6884 100644 (file)
@@ -69,7 +69,10 @@ return [
             'port'           => $mysql_port,
             'charset'        => 'utf8mb4',
             'collation'      => 'utf8mb4_unicode_ci',
-            'prefix'         => '',
+            // Prefixes are only semi-supported and may be unstable
+            // since they are not tested as part of our automated test suite.
+            // If used, the prefix should not be changed otherwise you will likely receive errors.
+            'prefix'         => env('DB_TABLE_PREFIX', ''),
             'prefix_indexes' => true,
             'strict'         => false,
             'engine'         => null,
index 71ea716f38729b6073ae96c48eb07c1bafd2a755..cf07312e8a25854b4387376fbe015789a4773602 100644 (file)
@@ -37,7 +37,7 @@ return [
          * Times-Roman, Times-Bold, Times-BoldItalic, Times-Italic,
          * Symbol, ZapfDingbats.
          */
-        'DOMPDF_FONT_DIR' => storage_path('fonts/'),  // advised by dompdf (https://github.com/dompdf/dompdf/pull/782)
+        'font_dir' => storage_path('fonts/'),  // advised by dompdf (https://github.com/dompdf/dompdf/pull/782)
 
         /**
          * The location of the DOMPDF font cache directory.
@@ -47,7 +47,7 @@ return [
          *
          * Note: This directory must exist and be writable by the webserver process.
          */
-        'DOMPDF_FONT_CACHE' => storage_path('fonts/'),
+        'font_cache' => storage_path('fonts/'),
 
         /**
          * The location of a temporary directory.
@@ -56,7 +56,7 @@ return [
          * The temporary directory is required to download remote images and when
          * using the PFDLib back end.
          */
-        'DOMPDF_TEMP_DIR' => sys_get_temp_dir(),
+        'temp_dir' => sys_get_temp_dir(),
 
         /**
          * ==== IMPORTANT ====.
@@ -70,7 +70,7 @@ return [
          * direct class use like:
          * $dompdf = new DOMPDF();  $dompdf->load_html($htmldata); $dompdf->render(); $pdfdata = $dompdf->output();
          */
-        'DOMPDF_CHROOT' => realpath(base_path()),
+        'chroot' => realpath(base_path()),
 
         /**
          * Whether to use Unicode fonts or not.
@@ -81,12 +81,12 @@ return [
          * When enabled, dompdf can support all Unicode glyphs. Any glyphs used in a
          * document must be present in your fonts, however.
          */
-        'DOMPDF_UNICODE_ENABLED' => true,
+        'unicode_enabled' => true,
 
         /**
          * Whether to enable font subsetting or not.
          */
-        'DOMPDF_ENABLE_FONTSUBSETTING' => false,
+        'enable_fontsubsetting' => false,
 
         /**
          * The PDF rendering backend to use.
@@ -115,7 +115,7 @@ return [
          * @link http://www.ros.co.nz/pdf
          * @link http://www.php.net/image
          */
-        'DOMPDF_PDF_BACKEND' => 'CPDF',
+        'pdf_backend' => 'CPDF',
 
         /**
          * PDFlib license key.
@@ -141,7 +141,7 @@ return [
          * the desired content might be different (e.g. screen or projection view of html file).
          * Therefore allow specification of content here.
          */
-        'DOMPDF_DEFAULT_MEDIA_TYPE' => 'print',
+        'default_media_type' => 'print',
 
         /**
          * The default paper size.
@@ -150,7 +150,7 @@ return [
          *
          * @see CPDF_Adapter::PAPER_SIZES for valid sizes ('letter', 'legal', 'A4', etc.)
          */
-        'DOMPDF_DEFAULT_PAPER_SIZE' => 'a4',
+        'default_paper_size' => 'a4',
 
         /**
          * The default font family.
@@ -159,7 +159,7 @@ return [
          *
          * @var string
          */
-        'DOMPDF_DEFAULT_FONT' => 'dejavu sans',
+        'default_font' => 'dejavu sans',
 
         /**
          * Image DPI setting.
@@ -194,7 +194,7 @@ return [
          *
          * @var int
          */
-        'DOMPDF_DPI' => 96,
+        'dpi' => 96,
 
         /**
          * Enable inline PHP.
@@ -208,7 +208,7 @@ return [
          *
          * @var bool
          */
-        'DOMPDF_ENABLE_PHP' => false,
+        'enable_php' => false,
 
         /**
          * Enable inline Javascript.
@@ -218,7 +218,7 @@ return [
          *
          * @var bool
          */
-        'DOMPDF_ENABLE_JAVASCRIPT' => false,
+        'enable_javascript' => false,
 
         /**
          * Enable remote file access.
@@ -237,12 +237,12 @@ return [
          *
          * @var bool
          */
-        'DOMPDF_ENABLE_REMOTE' => true,
+        'enable_remote' => env('ALLOW_UNTRUSTED_SERVER_FETCHING', false),
 
         /**
          * A ratio applied to the fonts height to be more like browsers' line height.
          */
-        'DOMPDF_FONT_HEIGHT_RATIO' => 1.1,
+        'font_height_ratio' => 1.1,
 
         /**
          * Enable CSS float.
@@ -251,12 +251,12 @@ return [
          *
          * @var bool
          */
-        'DOMPDF_ENABLE_CSS_FLOAT' => true,
+        'enable_css_float' => true,
 
         /**
          * Use the more-than-experimental HTML5 Lib parser.
          */
-        'DOMPDF_ENABLE_HTML5PARSER' => true,
+        'enable_html5parser' => true,
 
     ],
 
index 3dc3ec0af0e98b33bd3f1d741540dd13e1d319c5..50e81a2b8e2578edeb8b2d2208f158def6a0256f 100644 (file)
@@ -3,8 +3,8 @@
 namespace BookStack\Console\Commands;
 
 use BookStack\Entities\Tools\SearchIndex;
-use DB;
 use Illuminate\Console\Command;
+use Illuminate\Support\Facades\DB;
 
 class RegenerateSearch extends Command
 {
diff --git a/app/Console/Commands/ResetMfa.php b/app/Console/Commands/ResetMfa.php
new file mode 100644 (file)
index 0000000..031bec0
--- /dev/null
@@ -0,0 +1,77 @@
+<?php
+
+namespace BookStack\Console\Commands;
+
+use BookStack\Auth\User;
+use Illuminate\Console\Command;
+
+class ResetMfa extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'bookstack:reset-mfa
+                            {--id= : Numeric ID of the user to reset MFA for}
+                            {--email= : Email address of the user to reset MFA for} 
+                            ';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'Reset & Clear any configured MFA methods for the given user';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return mixed
+     */
+    public function handle()
+    {
+        $id = $this->option('id');
+        $email = $this->option('email');
+        if (!$id && !$email) {
+            $this->error('Either a --id=<number> or --email=<email> option must be provided.');
+
+            return 1;
+        }
+
+        /** @var User $user */
+        $field = $id ? 'id' : 'email';
+        $value = $id ?: $email;
+        $user = User::query()
+            ->where($field, '=', $value)
+            ->first();
+
+        if (!$user) {
+            $this->error("A user where {$field}={$value} could not be found.");
+
+            return 1;
+        }
+
+        $this->info("This will delete any configure multi-factor authentication methods for user: \n- ID: {$user->id}\n- Name: {$user->name}\n- Email: {$user->email}\n");
+        $this->info('If multi-factor authentication is required for this user they will be asked to reconfigure their methods on next login.');
+        $confirm = $this->confirm('Are you sure you want to proceed?');
+        if ($confirm) {
+            $user->mfaValues()->delete();
+            $this->info('User MFA methods have been reset.');
+
+            return 0;
+        }
+
+        return 1;
+    }
+}
index df30c1c714abb4fff2ddd9d92e2b3c4fa6119623..1e4591bd75d3f385e5a4cffe217b5f8c7f643f2c 100644 (file)
@@ -12,9 +12,12 @@ use Illuminate\Support\Collection;
 /**
  * Class Book.
  *
- * @property string     $description
- * @property int        $image_id
- * @property Image|null $cover
+ * @property string                                   $description
+ * @property int                                      $image_id
+ * @property Image|null                               $cover
+ * @property \Illuminate\Database\Eloquent\Collection $chapters
+ * @property \Illuminate\Database\Eloquent\Collection $pages
+ * @property \Illuminate\Database\Eloquent\Collection $directPages
  */
 class Book extends Entity implements HasCoverImage
 {
index f61878208eac80fbdb84d0dfa2e2810bf4e63910..e1ba0b6f708d75a18f5cb38dc38262e7396d61c0 100644 (file)
@@ -8,16 +8,31 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
 /**
  * Class BookChild.
  *
- * @property int  $book_id
- * @property int  $priority
- * @property Book $book
+ * @property int    $book_id
+ * @property int    $priority
+ * @property string $book_slug
+ * @property Book   $book
  *
  * @method Builder whereSlugs(string $bookSlug, string $childSlug)
  */
 abstract class BookChild extends Entity
 {
+    protected static function boot()
+    {
+        parent::boot();
+
+        // Load book slugs onto these models by default during query-time
+        static::addGlobalScope('book_slug', function (Builder $builder) {
+            $builder->addSelect(['book_slug' => function ($builder) {
+                $builder->select('slug')
+                    ->from('books')
+                    ->whereColumn('books.id', '=', 'book_id');
+            }]);
+        });
+    }
+
     /**
-     * Scope a query to find items where the the child has the given childSlug
+     * Scope a query to find items where the child has the given childSlug
      * where its parent has the bookSlug.
      */
     public function scopeWhereSlugs(Builder $query, string $bookSlug, string $childSlug)
index fd818b703736b9fe9e7bacd0f67fc5848e94a47c..f6f8427a3a0050e6908250cab0be9fdf9491878d 100644 (file)
@@ -36,7 +36,7 @@ class Chapter extends BookChild
     {
         $parts = [
             'books',
-            urlencode($this->getAttribute('bookSlug') ?? $this->book->slug),
+            urlencode($this->book_slug ?? $this->book->slug),
             'chapter',
             urlencode($this->slug),
             trim($path, '/'),
index 123600539b053f71b7a428d408e75e80cbfe95a6..b8467c38cf58ce6cc8334b287959824bf8f5fd16 100644 (file)
@@ -25,9 +25,10 @@ use Permissions;
  */
 class Page extends BookChild
 {
-    protected $fillable = ['name', 'priority', 'markdown'];
+    public static $listAttributes = ['name', 'id', 'slug', 'book_id', 'chapter_id', 'draft', 'template', 'text', 'created_at', 'updated_at', 'priority'];
+    public static $contentAttributes = ['name', 'id', 'slug', 'book_id', 'chapter_id', 'draft', 'template', 'html', 'text', 'created_at', 'updated_at', 'priority'];
 
-    protected $simpleAttributes = ['name', 'id', 'slug'];
+    protected $fillable = ['name', 'priority', 'markdown'];
 
     public $textField = 'text';
 
@@ -48,19 +49,6 @@ class Page extends BookChild
         return parent::scopeVisible($query);
     }
 
-    /**
-     * Converts this page into a simplified array.
-     *
-     * @return mixed
-     */
-    public function toSimpleArray()
-    {
-        $array = array_intersect_key($this->toArray(), array_flip($this->simpleAttributes));
-        $array['url'] = $this->getUrl();
-
-        return $array;
-    }
-
     /**
      * Get the chapter that this page is in, If applicable.
      *
@@ -119,7 +107,7 @@ class Page extends BookChild
     {
         $parts = [
             'books',
-            urlencode($this->getAttribute('bookSlug') ?? $this->book->slug),
+            urlencode($this->book_slug ?? $this->book->slug),
             $this->draft ? 'draft' : 'page',
             $this->draft ? $this->id : urlencode($this->slug),
             trim($path, '/'),
index 28949b2dd7e3dd35f80d915fb053fd3918e5dfba..ffa06d45954c9a7269998c1a719c23fa81bbba56 100644 (file)
@@ -218,8 +218,8 @@ class PageRepo
         $pageContent = new PageContent($page);
         if (!empty($input['markdown'] ?? '')) {
             $pageContent->setNewMarkdown($input['markdown']);
-        } else {
-            $pageContent->setNewHTML($input['html'] ?? '');
+        } elseif (isset($input['html'])) {
+            $pageContent->setNewHTML($input['html']);
         }
     }
 
index af7746e561c135bb85c7d6d1c15892d4864aa420..8622d5e129486f78112e1b23867de77a4a4b7c0c 100644 (file)
@@ -45,7 +45,7 @@ class BookContents
      */
     public function getTree(bool $showDrafts = false, bool $renderPages = false): Collection
     {
-        $pages = $this->getPages($showDrafts);
+        $pages = $this->getPages($showDrafts, $renderPages);
         $chapters = Chapter::visible()->where('book_id', '=', $this->book->id)->get();
         $all = collect()->concat($pages)->concat($chapters);
         $chapterMap = $chapters->keyBy('id');
@@ -93,9 +93,11 @@ class BookContents
     /**
      * Get the visible pages within this book.
      */
-    protected function getPages(bool $showDrafts = false): Collection
+    protected function getPages(bool $showDrafts = false, bool $getPageContent = false): Collection
     {
-        $query = Page::visible()->where('book_id', '=', $this->book->id);
+        $query = Page::visible()
+            ->select($getPageContent ? Page::$contentAttributes : Page::$listAttributes)
+            ->where('book_id', '=', $this->book->id);
 
         if (!$showDrafts) {
             $query->where('draft', '=', false);
index c299f9c71937cd279182249dbfa9d1aba3a79839..05d0ff13466ad81c9de1da4ee60b2373429e6dac 100644 (file)
@@ -140,7 +140,7 @@ class ExportFormatter
     protected function htmlToPdf(string $html): string
     {
         $containedHtml = $this->containHtml($html);
-        $useWKHTML = config('snappy.pdf.binary') !== false;
+        $useWKHTML = config('snappy.pdf.binary') !== false && config('app.allow_untrusted_server_fetching') === true;
         if ($useWKHTML) {
             $pdf = SnappyPDF::loadHTML($containedHtml);
             $pdf->setOption('print-media-type', true);
diff --git a/app/Entities/Tools/Markdown/CustomListItemRenderer.php b/app/Entities/Tools/Markdown/CustomListItemRenderer.php
new file mode 100644 (file)
index 0000000..be4cac4
--- /dev/null
@@ -0,0 +1,43 @@
+<?php
+
+namespace BookStack\Entities\Tools\Markdown;
+
+use League\CommonMark\Block\Element\AbstractBlock;
+use League\CommonMark\Block\Element\ListItem;
+use League\CommonMark\Block\Element\Paragraph;
+use League\CommonMark\Block\Renderer\BlockRendererInterface;
+use League\CommonMark\Block\Renderer\ListItemRenderer;
+use League\CommonMark\ElementRendererInterface;
+use League\CommonMark\Extension\TaskList\TaskListItemMarker;
+use League\CommonMark\HtmlElement;
+
+class CustomListItemRenderer implements BlockRendererInterface
+{
+    protected $baseRenderer;
+
+    public function __construct()
+    {
+        $this->baseRenderer = new ListItemRenderer();
+    }
+
+    /**
+     * @return HtmlElement|string|null
+     */
+    public function render(AbstractBlock $block, ElementRendererInterface $htmlRenderer, bool $inTightList = false)
+    {
+        $listItem = $this->baseRenderer->render($block, $htmlRenderer, $inTightList);
+
+        if ($this->startsTaskListItem($block)) {
+            $listItem->setAttribute('class', 'task-list-item');
+        }
+
+        return $listItem;
+    }
+
+    private function startsTaskListItem(ListItem $block): bool
+    {
+        $firstChild = $block->firstChild();
+
+        return $firstChild instanceof Paragraph && $firstChild->firstChild() instanceof TaskListItemMarker;
+    }
+}
index 9a53e2d2458f88646f4ab941cb001bf6b2ea516b..661c554da4809c799d38f2aba8321756ca992019 100644 (file)
@@ -3,6 +3,7 @@
 namespace BookStack\Entities\Tools;
 
 use BookStack\Entities\Models\Page;
+use BookStack\Entities\Tools\Markdown\CustomListItemRenderer;
 use BookStack\Entities\Tools\Markdown\CustomStrikeThroughExtension;
 use BookStack\Exceptions\ImageUploadException;
 use BookStack\Facades\Theme;
@@ -13,6 +14,7 @@ use DOMDocument;
 use DOMNodeList;
 use DOMXPath;
 use Illuminate\Support\Str;
+use League\CommonMark\Block\Element\ListItem;
 use League\CommonMark\CommonMarkConverter;
 use League\CommonMark\Environment;
 use League\CommonMark\Extension\Table\TableExtension;
@@ -64,6 +66,8 @@ class PageContent
         $environment = Theme::dispatch(ThemeEvents::COMMONMARK_ENVIRONMENT_CONFIGURE, $environment) ?? $environment;
         $converter = new CommonMarkConverter([], $environment);
 
+        $environment->addBlockRenderer(ListItem::class, new CustomListItemRenderer(), 10);
+
         return $converter->convertToHtml($markdown);
     }
 
@@ -223,7 +227,7 @@ class PageContent
      */
     public function render(bool $blankIncludes = false): string
     {
-        $content = $this->page->html;
+        $content = $this->page->html ?? '';
 
         if (!config('app.allow_content_scripts')) {
             $content = HtmlContentFilter::removeScripts($content);
@@ -312,6 +316,7 @@ class PageContent
             }
 
             // Find page and skip this if page not found
+            /** @var ?Page $matchedPage */
             $matchedPage = Page::visible()->find($pageId);
             if ($matchedPage === null) {
                 $html = str_replace($fullMatch, '', $html);
diff --git a/app/Exceptions/StoppedAuthenticationException.php b/app/Exceptions/StoppedAuthenticationException.php
new file mode 100644 (file)
index 0000000..d10a6da
--- /dev/null
@@ -0,0 +1,64 @@
+<?php
+
+namespace BookStack\Exceptions;
+
+use BookStack\Auth\Access\LoginService;
+use BookStack\Auth\User;
+use Illuminate\Contracts\Support\Responsable;
+use Illuminate\Http\Request;
+
+class StoppedAuthenticationException extends \Exception implements Responsable
+{
+    protected $user;
+    protected $loginService;
+
+    /**
+     * StoppedAuthenticationException constructor.
+     */
+    public function __construct(User $user, LoginService $loginService)
+    {
+        $this->user = $user;
+        $this->loginService = $loginService;
+        parent::__construct();
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function toResponse($request)
+    {
+        $redirect = '/login';
+
+        if ($this->loginService->awaitingEmailConfirmation($this->user)) {
+            return $this->awaitingEmailConfirmationResponse($request);
+        }
+
+        if ($this->loginService->needsMfaVerification($this->user)) {
+            $redirect = '/mfa/verify';
+        }
+
+        return redirect($redirect);
+    }
+
+    /**
+     * Provide an error response for when the current user's email is not confirmed
+     * in a system which requires it.
+     */
+    protected function awaitingEmailConfirmationResponse(Request $request)
+    {
+        if ($request->wantsJson()) {
+            return response()->json([
+                'error' => [
+                    'code'    => 401,
+                    'message' => trans('errors.email_confirmation_awaiting'),
+                ],
+            ], 401);
+        }
+
+        if (session()->pull('sent-email-confirmation') === true) {
+            return redirect('/register/confirm');
+        }
+
+        return redirect('/register/confirm/awaiting');
+    }
+}
index c7d121f88636cec083594f2c5f376eb2f6e273b8..028bc3a817ebf726b358ca0f7b8d47f393695bf8 100644 (file)
@@ -13,6 +13,7 @@ class BookExportApiController extends ApiController
     public function __construct(ExportFormatter $exportFormatter)
     {
         $this->exportFormatter = $exportFormatter;
+        $this->middleware('can:content-export');
     }
 
     /**
index 5dfcea344def1508548f1209f1bb9eac2835d43f..5715ab2e37c6c9f7e682ad69b407f27153e3934e 100644 (file)
@@ -16,6 +16,7 @@ class ChapterExportApiController extends ApiController
     public function __construct(ExportFormatter $exportFormatter)
     {
         $this->exportFormatter = $exportFormatter;
+        $this->middleware('can:content-export');
     }
 
     /**
index 7cee2fbe7cc24510be06df1cd2c1483aa68cb066..ce5700c79b9add01a9b2eb8d418f5b3a785344e8 100644 (file)
@@ -13,6 +13,7 @@ class PageExportApiController extends ApiController
     public function __construct(ExportFormatter $exportFormatter)
     {
         $this->exportFormatter = $exportFormatter;
+        $this->middleware('can:content-export');
     }
 
     /**
index 63f220a68e5630e106fcf51dbc4488ea0a65db04..02b9ef2760559e247f8f159a8f014732d571b76b 100644 (file)
@@ -2,15 +2,13 @@
 
 namespace BookStack\Http\Controllers\Auth;
 
-use BookStack\Actions\ActivityType;
 use BookStack\Auth\Access\EmailConfirmationService;
+use BookStack\Auth\Access\LoginService;
 use BookStack\Auth\UserRepo;
 use BookStack\Exceptions\ConfirmationEmailException;
 use BookStack\Exceptions\UserTokenExpiredException;
 use BookStack\Exceptions\UserTokenNotFoundException;
-use BookStack\Facades\Theme;
 use BookStack\Http\Controllers\Controller;
-use BookStack\Theming\ThemeEvents;
 use Exception;
 use Illuminate\Http\RedirectResponse;
 use Illuminate\Http\Request;
@@ -20,14 +18,19 @@ use Illuminate\View\View;
 class ConfirmEmailController extends Controller
 {
     protected $emailConfirmationService;
+    protected $loginService;
     protected $userRepo;
 
     /**
      * Create a new controller instance.
      */
-    public function __construct(EmailConfirmationService $emailConfirmationService, UserRepo $userRepo)
-    {
+    public function __construct(
+        EmailConfirmationService $emailConfirmationService,
+        LoginService $loginService,
+        UserRepo $userRepo
+    ) {
         $this->emailConfirmationService = $emailConfirmationService;
+        $this->loginService = $loginService;
         $this->userRepo = $userRepo;
     }
 
@@ -43,12 +46,12 @@ class ConfirmEmailController extends Controller
     /**
      * Shows a notice that a user's email address has not been confirmed,
      * Also has the option to re-send the confirmation email.
-     *
-     * @return View
      */
     public function showAwaiting()
     {
-        return view('auth.user-unconfirmed');
+        $user = $this->loginService->getLastLoginAttemptUser();
+
+        return view('auth.user-unconfirmed', ['user' => $user]);
     }
 
     /**
@@ -87,11 +90,9 @@ class ConfirmEmailController extends Controller
         $user->email_confirmed = true;
         $user->save();
 
-        auth()->login($user);
-        Theme::dispatch(ThemeEvents::AUTH_LOGIN, auth()->getDefaultDriver(), $user);
-        $this->logActivity(ActivityType::AUTH_LOGIN, $user);
-        $this->showSuccessNotification(trans('auth.email_confirm_success'));
         $this->emailConfirmationService->deleteByUser($user);
+        $this->showSuccessNotification(trans('auth.email_confirm_success'));
+        $this->loginService->login($user, auth()->getDefaultDriver());
 
         return redirect('/');
     }
diff --git a/app/Http/Controllers/Auth/HandlesPartialLogins.php b/app/Http/Controllers/Auth/HandlesPartialLogins.php
new file mode 100644 (file)
index 0000000..c7f3621
--- /dev/null
@@ -0,0 +1,25 @@
+<?php
+
+namespace BookStack\Http\Controllers\Auth;
+
+use BookStack\Auth\Access\LoginService;
+use BookStack\Auth\User;
+use BookStack\Exceptions\NotFoundException;
+
+trait HandlesPartialLogins
+{
+    /**
+     * @throws NotFoundException
+     */
+    protected function currentOrLastAttemptedUser(): User
+    {
+        $loginService = app()->make(LoginService::class);
+        $user = auth()->user() ?? $loginService->getLastLoginAttemptUser();
+
+        if (!$user) {
+            throw new NotFoundException('A user for this action could not be found');
+        }
+
+        return $user;
+    }
+}
index 5154f7e97d8d64c0748daa4b2304b315756d77e5..7c8eb2c864f2cdcb6a7030defaabec161540cd02 100644 (file)
@@ -3,13 +3,11 @@
 namespace BookStack\Http\Controllers\Auth;
 
 use Activity;
-use BookStack\Actions\ActivityType;
+use BookStack\Auth\Access\LoginService;
 use BookStack\Auth\Access\SocialAuthService;
 use BookStack\Exceptions\LoginAttemptEmailNeededException;
 use BookStack\Exceptions\LoginAttemptException;
-use BookStack\Facades\Theme;
 use BookStack\Http\Controllers\Controller;
-use BookStack\Theming\ThemeEvents;
 use Illuminate\Foundation\Auth\AuthenticatesUsers;
 use Illuminate\Http\Request;
 use Illuminate\Validation\ValidationException;
@@ -37,16 +35,19 @@ class LoginController extends Controller
     protected $redirectAfterLogout = '/login';
 
     protected $socialAuthService;
+    protected $loginService;
 
     /**
      * Create a new controller instance.
      */
-    public function __construct(SocialAuthService $socialAuthService)
+    public function __construct(SocialAuthService $socialAuthService, LoginService $loginService)
     {
         $this->middleware('guest', ['only' => ['getLogin', 'login']]);
         $this->middleware('guard:standard,ldap', ['only' => ['login', 'logout']]);
 
         $this->socialAuthService = $socialAuthService;
+        $this->loginService = $loginService;
+
         $this->redirectPath = url('/');
         $this->redirectAfterLogout = url('/login');
     }
@@ -80,13 +81,7 @@ class LoginController extends Controller
         }
 
         // Store the previous location for redirect after login
-        $previous = url()->previous('');
-        if ($previous && $previous !== url('/login') && setting('app-public')) {
-            $isPreviousFromInstance = (strpos($previous, url('/')) === 0);
-            if ($isPreviousFromInstance) {
-                redirect()->setIntendedUrl($previous);
-            }
-        }
+        $this->updateIntendedFromPrevious();
 
         return view('auth.login', [
             'socialDrivers' => $socialDrivers,
@@ -140,6 +135,22 @@ class LoginController extends Controller
         return $this->sendFailedLoginResponse($request);
     }
 
+    /**
+     * Attempt to log the user into the application.
+     *
+     * @param \Illuminate\Http\Request $request
+     *
+     * @return bool
+     */
+    protected function attemptLogin(Request $request)
+    {
+        return $this->loginService->attempt(
+            $this->credentials($request),
+            auth()->getDefaultDriver(),
+            $request->filled('remember')
+        );
+    }
+
     /**
      * The user has been authenticated.
      *
@@ -150,17 +161,6 @@ class LoginController extends Controller
      */
     protected function authenticated(Request $request, $user)
     {
-        // Authenticate on all session guards if a likely admin
-        if ($user->can('users-manage') && $user->can('user-roles-manage')) {
-            $guards = ['standard', 'ldap', 'saml2'];
-            foreach ($guards as $guard) {
-                auth($guard)->login($user);
-            }
-        }
-
-        Theme::dispatch(ThemeEvents::AUTH_LOGIN, auth()->getDefaultDriver(), $user);
-        $this->logActivity(ActivityType::AUTH_LOGIN, $user);
-
         return redirect()->intended($this->redirectPath());
     }
 
@@ -222,4 +222,32 @@ class LoginController extends Controller
             $this->username() => [trans('auth.failed')],
         ])->redirectTo('/login');
     }
+
+    /**
+     * Update the intended URL location from their previous URL.
+     * Ignores if not from the current app instance or if from certain
+     * login or authentication routes.
+     */
+    protected function updateIntendedFromPrevious(): void
+    {
+        // Store the previous location for redirect after login
+        $previous = url()->previous('');
+        $isPreviousFromInstance = (strpos($previous, url('/')) === 0);
+        if (!$previous || !setting('app-public') || !$isPreviousFromInstance) {
+            return;
+        }
+
+        $ignorePrefixList = [
+            '/login',
+            '/mfa',
+        ];
+
+        foreach ($ignorePrefixList as $ignorePrefix) {
+            if (strpos($previous, url($ignorePrefix)) === 0) {
+                return;
+            }
+        }
+
+        redirect()->setIntendedUrl($previous);
+    }
 }
diff --git a/app/Http/Controllers/Auth/MfaBackupCodesController.php b/app/Http/Controllers/Auth/MfaBackupCodesController.php
new file mode 100644 (file)
index 0000000..d92029b
--- /dev/null
@@ -0,0 +1,98 @@
+<?php
+
+namespace BookStack\Http\Controllers\Auth;
+
+use BookStack\Actions\ActivityType;
+use BookStack\Auth\Access\LoginService;
+use BookStack\Auth\Access\Mfa\BackupCodeService;
+use BookStack\Auth\Access\Mfa\MfaSession;
+use BookStack\Auth\Access\Mfa\MfaValue;
+use BookStack\Exceptions\NotFoundException;
+use BookStack\Http\Controllers\Controller;
+use Exception;
+use Illuminate\Http\Request;
+use Illuminate\Validation\ValidationException;
+
+class MfaBackupCodesController extends Controller
+{
+    use HandlesPartialLogins;
+
+    protected const SETUP_SECRET_SESSION_KEY = 'mfa-setup-backup-codes';
+
+    /**
+     * Show a view that generates and displays backup codes.
+     */
+    public function generate(BackupCodeService $codeService)
+    {
+        $codes = $codeService->generateNewSet();
+        session()->put(self::SETUP_SECRET_SESSION_KEY, encrypt($codes));
+
+        $downloadUrl = 'data:application/octet-stream;base64,' . base64_encode(implode("\n\n", $codes));
+
+        return view('mfa.backup-codes-generate', [
+            'codes'       => $codes,
+            'downloadUrl' => $downloadUrl,
+        ]);
+    }
+
+    /**
+     * Confirm the setup of backup codes, storing them against the user.
+     *
+     * @throws Exception
+     */
+    public function confirm()
+    {
+        if (!session()->has(self::SETUP_SECRET_SESSION_KEY)) {
+            return response('No generated codes found in the session', 500);
+        }
+
+        $codes = decrypt(session()->pull(self::SETUP_SECRET_SESSION_KEY));
+        MfaValue::upsertWithValue($this->currentOrLastAttemptedUser(), MfaValue::METHOD_BACKUP_CODES, json_encode($codes));
+
+        $this->logActivity(ActivityType::MFA_SETUP_METHOD, 'backup-codes');
+
+        if (!auth()->check()) {
+            $this->showSuccessNotification(trans('auth.mfa_setup_login_notification'));
+
+            return redirect('/login');
+        }
+
+        return redirect('/mfa/setup');
+    }
+
+    /**
+     * Verify the MFA method submission on check.
+     *
+     * @throws NotFoundException
+     * @throws ValidationException
+     */
+    public function verify(Request $request, BackupCodeService $codeService, MfaSession $mfaSession, LoginService $loginService)
+    {
+        $user = $this->currentOrLastAttemptedUser();
+        $codes = MfaValue::getValueForUser($user, MfaValue::METHOD_BACKUP_CODES) ?? '[]';
+
+        $this->validate($request, [
+            'code' => [
+                'required',
+                'max:12', 'min:8',
+                function ($attribute, $value, $fail) use ($codeService, $codes) {
+                    if (!$codeService->inputCodeExistsInSet($value, $codes)) {
+                        $fail(trans('validation.backup_codes'));
+                    }
+                },
+            ],
+        ]);
+
+        $updatedCodes = $codeService->removeInputCodeFromSet($request->get('code'), $codes);
+        MfaValue::upsertWithValue($user, MfaValue::METHOD_BACKUP_CODES, $updatedCodes);
+
+        $mfaSession->markVerifiedForUser($user);
+        $loginService->reattemptLoginFor($user);
+
+        if ($codeService->countCodesInSet($updatedCodes) < 5) {
+            $this->showWarningNotification(trans('auth.mfa_backup_codes_usage_limit_warning'));
+        }
+
+        return redirect()->intended();
+    }
+}
diff --git a/app/Http/Controllers/Auth/MfaController.php b/app/Http/Controllers/Auth/MfaController.php
new file mode 100644 (file)
index 0000000..ca77cc7
--- /dev/null
@@ -0,0 +1,71 @@
+<?php
+
+namespace BookStack\Http\Controllers\Auth;
+
+use BookStack\Actions\ActivityType;
+use BookStack\Auth\Access\Mfa\MfaValue;
+use BookStack\Http\Controllers\Controller;
+use Illuminate\Http\Request;
+
+class MfaController extends Controller
+{
+    use HandlesPartialLogins;
+
+    /**
+     * Show the view to setup MFA for the current user.
+     */
+    public function setup()
+    {
+        $userMethods = $this->currentOrLastAttemptedUser()
+            ->mfaValues()
+            ->get(['id', 'method'])
+            ->groupBy('method');
+
+        return view('mfa.setup', [
+            'userMethods' => $userMethods,
+        ]);
+    }
+
+    /**
+     * Remove an MFA method for the current user.
+     *
+     * @throws \Exception
+     */
+    public function remove(string $method)
+    {
+        if (in_array($method, MfaValue::allMethods())) {
+            $value = user()->mfaValues()->where('method', '=', $method)->first();
+            if ($value) {
+                $value->delete();
+                $this->logActivity(ActivityType::MFA_REMOVE_METHOD, $method);
+            }
+        }
+
+        return redirect('/mfa/setup');
+    }
+
+    /**
+     * Show the page to start an MFA verification.
+     */
+    public function verify(Request $request)
+    {
+        $desiredMethod = $request->get('method');
+        $userMethods = $this->currentOrLastAttemptedUser()
+            ->mfaValues()
+            ->get(['id', 'method'])
+            ->groupBy('method');
+
+        // Basic search for the default option for a user.
+        // (Prioritises totp over backup codes)
+        $method = $userMethods->has($desiredMethod) ? $desiredMethod : $userMethods->keys()->sort()->reverse()->first();
+        $otherMethods = $userMethods->keys()->filter(function ($userMethod) use ($method) {
+            return $method !== $userMethod;
+        })->all();
+
+        return view('mfa.verify', [
+            'userMethods'  => $userMethods,
+            'method'       => $method,
+            'otherMethods' => $otherMethods,
+        ]);
+    }
+}
diff --git a/app/Http/Controllers/Auth/MfaTotpController.php b/app/Http/Controllers/Auth/MfaTotpController.php
new file mode 100644 (file)
index 0000000..694d69d
--- /dev/null
@@ -0,0 +1,97 @@
+<?php
+
+namespace BookStack\Http\Controllers\Auth;
+
+use BookStack\Actions\ActivityType;
+use BookStack\Auth\Access\LoginService;
+use BookStack\Auth\Access\Mfa\MfaSession;
+use BookStack\Auth\Access\Mfa\MfaValue;
+use BookStack\Auth\Access\Mfa\TotpService;
+use BookStack\Auth\Access\Mfa\TotpValidationRule;
+use BookStack\Exceptions\NotFoundException;
+use BookStack\Http\Controllers\Controller;
+use Illuminate\Http\Request;
+use Illuminate\Validation\ValidationException;
+
+class MfaTotpController extends Controller
+{
+    use HandlesPartialLogins;
+
+    protected const SETUP_SECRET_SESSION_KEY = 'mfa-setup-totp-secret';
+
+    /**
+     * Show a view that generates and displays a TOTP QR code.
+     */
+    public function generate(TotpService $totp)
+    {
+        if (session()->has(static::SETUP_SECRET_SESSION_KEY)) {
+            $totpSecret = decrypt(session()->get(static::SETUP_SECRET_SESSION_KEY));
+        } else {
+            $totpSecret = $totp->generateSecret();
+            session()->put(static::SETUP_SECRET_SESSION_KEY, encrypt($totpSecret));
+        }
+
+        $qrCodeUrl = $totp->generateUrl($totpSecret);
+        $svg = $totp->generateQrCodeSvg($qrCodeUrl);
+
+        return view('mfa.totp-generate', [
+            'url' => $qrCodeUrl,
+            'svg' => $svg,
+        ]);
+    }
+
+    /**
+     * Confirm the setup of TOTP and save the auth method secret
+     * against the current user.
+     *
+     * @throws ValidationException
+     * @throws NotFoundException
+     */
+    public function confirm(Request $request)
+    {
+        $totpSecret = decrypt(session()->get(static::SETUP_SECRET_SESSION_KEY));
+        $this->validate($request, [
+            'code' => [
+                'required',
+                'max:12', 'min:4',
+                new TotpValidationRule($totpSecret),
+            ],
+        ]);
+
+        MfaValue::upsertWithValue($this->currentOrLastAttemptedUser(), MfaValue::METHOD_TOTP, $totpSecret);
+        session()->remove(static::SETUP_SECRET_SESSION_KEY);
+        $this->logActivity(ActivityType::MFA_SETUP_METHOD, 'totp');
+
+        if (!auth()->check()) {
+            $this->showSuccessNotification(trans('auth.mfa_setup_login_notification'));
+
+            return redirect('/login');
+        }
+
+        return redirect('/mfa/setup');
+    }
+
+    /**
+     * Verify the MFA method submission on check.
+     *
+     * @throws NotFoundException
+     */
+    public function verify(Request $request, LoginService $loginService, MfaSession $mfaSession)
+    {
+        $user = $this->currentOrLastAttemptedUser();
+        $totpSecret = MfaValue::getValueForUser($user, MfaValue::METHOD_TOTP);
+
+        $this->validate($request, [
+            'code' => [
+                'required',
+                'max:12', 'min:4',
+                new TotpValidationRule($totpSecret),
+            ],
+        ]);
+
+        $mfaSession->markVerifiedForUser($user);
+        $loginService->reattemptLoginFor($user);
+
+        return redirect()->intended();
+    }
+}
index 1728ece3271aef723d0190fe8a2585bb5489be8a..209827d6db800d7a75a7a568ec9a1af8468f4355 100644 (file)
@@ -2,18 +2,17 @@
 
 namespace BookStack\Http\Controllers\Auth;
 
-use BookStack\Actions\ActivityType;
+use BookStack\Auth\Access\LoginService;
 use BookStack\Auth\Access\RegistrationService;
 use BookStack\Auth\Access\SocialAuthService;
 use BookStack\Auth\User;
+use BookStack\Exceptions\StoppedAuthenticationException;
 use BookStack\Exceptions\UserRegistrationException;
-use BookStack\Facades\Theme;
 use BookStack\Http\Controllers\Controller;
-use BookStack\Theming\ThemeEvents;
 use Illuminate\Foundation\Auth\RegistersUsers;
 use Illuminate\Http\Request;
 use Illuminate\Support\Facades\Hash;
-use Validator;
+use Illuminate\Support\Facades\Validator;
 
 class RegisterController extends Controller
 {
@@ -32,6 +31,7 @@ class RegisterController extends Controller
 
     protected $socialAuthService;
     protected $registrationService;
+    protected $loginService;
 
     /**
      * Where to redirect users after login / registration.
@@ -44,13 +44,17 @@ class RegisterController extends Controller
     /**
      * Create a new controller instance.
      */
-    public function __construct(SocialAuthService $socialAuthService, RegistrationService $registrationService)
-    {
+    public function __construct(
+        SocialAuthService $socialAuthService,
+        RegistrationService $registrationService,
+        LoginService $loginService
+    ) {
         $this->middleware('guest');
         $this->middleware('guard:standard');
 
         $this->socialAuthService = $socialAuthService;
         $this->registrationService = $registrationService;
+        $this->loginService = $loginService;
 
         $this->redirectTo = url('/');
         $this->redirectPath = url('/');
@@ -89,6 +93,7 @@ class RegisterController extends Controller
      * Handle a registration request for the application.
      *
      * @throws UserRegistrationException
+     * @throws StoppedAuthenticationException
      */
     public function postRegister(Request $request)
     {
@@ -98,9 +103,7 @@ class RegisterController extends Controller
 
         try {
             $user = $this->registrationService->registerUser($userData);
-            auth()->login($user);
-            Theme::dispatch(ThemeEvents::AUTH_LOGIN, auth()->getDefaultDriver(), $user);
-            $this->logActivity(ActivityType::AUTH_LOGIN, $user);
+            $this->loginService->login($user, auth()->getDefaultDriver());
         } catch (UserRegistrationException $exception) {
             if ($exception->getMessage()) {
                 $this->showErrorNotification($exception->getMessage());
index 1d1468698734ce8d61e5d415838af11212e4e0f6..1691668a2bbc37163553167e3f20d0502c6af3cb 100644 (file)
@@ -2,16 +2,14 @@
 
 namespace BookStack\Http\Controllers\Auth;
 
-use BookStack\Actions\ActivityType;
+use BookStack\Auth\Access\LoginService;
 use BookStack\Auth\Access\RegistrationService;
 use BookStack\Auth\Access\SocialAuthService;
 use BookStack\Exceptions\SocialDriverNotConfigured;
 use BookStack\Exceptions\SocialSignInAccountNotUsed;
 use BookStack\Exceptions\SocialSignInException;
 use BookStack\Exceptions\UserRegistrationException;
-use BookStack\Facades\Theme;
 use BookStack\Http\Controllers\Controller;
-use BookStack\Theming\ThemeEvents;
 use Illuminate\Http\Request;
 use Illuminate\Support\Str;
 use Laravel\Socialite\Contracts\User as SocialUser;
@@ -20,15 +18,20 @@ class SocialController extends Controller
 {
     protected $socialAuthService;
     protected $registrationService;
+    protected $loginService;
 
     /**
      * SocialController constructor.
      */
-    public function __construct(SocialAuthService $socialAuthService, RegistrationService $registrationService)
-    {
+    public function __construct(
+        SocialAuthService $socialAuthService,
+        RegistrationService $registrationService,
+        LoginService $loginService
+    ) {
         $this->middleware('guest')->only(['getRegister', 'postRegister']);
         $this->socialAuthService = $socialAuthService;
         $this->registrationService = $registrationService;
+        $this->loginService = $loginService;
     }
 
     /**
@@ -136,11 +139,8 @@ class SocialController extends Controller
         }
 
         $user = $this->registrationService->registerUser($userData, $socialAccount, $emailVerified);
-        auth()->login($user);
-        Theme::dispatch(ThemeEvents::AUTH_LOGIN, $socialDriver, $user);
-        $this->logActivity(ActivityType::AUTH_LOGIN, $user);
-
         $this->showSuccessNotification(trans('auth.register_success'));
+        $this->loginService->login($user, $socialDriver);
 
         return redirect('/');
     }
index 253e2550713b00ded9e5b6720a617cb891d706f6..bd1912b0b494313d410be80b0844ec15d6943ede 100644 (file)
@@ -2,14 +2,12 @@
 
 namespace BookStack\Http\Controllers\Auth;
 
-use BookStack\Actions\ActivityType;
+use BookStack\Auth\Access\LoginService;
 use BookStack\Auth\Access\UserInviteService;
 use BookStack\Auth\UserRepo;
 use BookStack\Exceptions\UserTokenExpiredException;
 use BookStack\Exceptions\UserTokenNotFoundException;
-use BookStack\Facades\Theme;
 use BookStack\Http\Controllers\Controller;
-use BookStack\Theming\ThemeEvents;
 use Exception;
 use Illuminate\Http\RedirectResponse;
 use Illuminate\Http\Request;
@@ -18,17 +16,19 @@ use Illuminate\Routing\Redirector;
 class UserInviteController extends Controller
 {
     protected $inviteService;
+    protected $loginService;
     protected $userRepo;
 
     /**
      * Create a new controller instance.
      */
-    public function __construct(UserInviteService $inviteService, UserRepo $userRepo)
+    public function __construct(UserInviteService $inviteService, LoginService $loginService, UserRepo $userRepo)
     {
         $this->middleware('guest');
         $this->middleware('guard:standard');
 
         $this->inviteService = $inviteService;
+        $this->loginService = $loginService;
         $this->userRepo = $userRepo;
     }
 
@@ -72,11 +72,9 @@ class UserInviteController extends Controller
         $user->email_confirmed = true;
         $user->save();
 
-        auth()->login($user);
-        Theme::dispatch(ThemeEvents::AUTH_LOGIN, auth()->getDefaultDriver(), $user);
-        $this->logActivity(ActivityType::AUTH_LOGIN, $user);
-        $this->showSuccessNotification(trans('auth.user_invite_success', ['appName' => setting('app-name')]));
         $this->inviteService->deleteByUser($user);
+        $this->showSuccessNotification(trans('auth.user_invite_success', ['appName' => setting('app-name')]));
+        $this->loginService->login($user, auth()->getDefaultDriver());
 
         return redirect('/');
     }
index 5e6d7c6adc8f22f970550814825c43f3451fb2a3..7f6dd801752b5e3901cb6188e3dc200888f32984 100644 (file)
@@ -18,6 +18,7 @@ class BookExportController extends Controller
     {
         $this->bookRepo = $bookRepo;
         $this->exportFormatter = $exportFormatter;
+        $this->middleware('can:content-export');
     }
 
     /**
index bc67a8e1cc6c1cff00641a6c731d6dec3b7b578f..0bd39477801add649b70dce3e5f9b25f70fb8c65 100644 (file)
@@ -43,7 +43,7 @@ class BookSortController extends Controller
         $book = $this->bookRepo->getBySlug($bookSlug);
         $bookChildren = (new BookContents($book))->getTree();
 
-        return view('books.sort-box', ['book' => $book, 'bookChildren' => $bookChildren]);
+        return view('books.parts.sort-box', ['book' => $book, 'bookChildren' => $bookChildren]);
     }
 
     /**
index 2758496d2062b307964774e4a669ff8884cd75bc..480280c99ef6dc83a169ba1762d5e4fc0edd4d36 100644 (file)
@@ -19,6 +19,7 @@ class ChapterExportController extends Controller
     {
         $this->chapterRepo = $chapterRepo;
         $this->exportFormatter = $exportFormatter;
+        $this->middleware('can:content-export');
     }
 
     /**
index 4a71b56b9c1d44b41820ccd66242406181a3ac2d..5451c0abfe8289730e26649eec36c713aa3c5d36 100644 (file)
@@ -10,7 +10,6 @@ use BookStack\Entities\Queries\TopFavourites;
 use BookStack\Entities\Repos\BookRepo;
 use BookStack\Entities\Repos\BookshelfRepo;
 use BookStack\Entities\Tools\PageContent;
-use Views;
 
 class HomeController extends Controller
 {
@@ -41,6 +40,7 @@ class HomeController extends Controller
             ->where('draft', false)
             ->orderBy('updated_at', 'desc')
             ->take($favourites->count() > 0 ? 6 : 12)
+            ->select(Page::$listAttributes)
             ->get();
 
         $homepageOptions = ['default', 'books', 'bookshelves', 'page'];
@@ -82,7 +82,7 @@ class HomeController extends Controller
             $shelves = app(BookshelfRepo::class)->getAllPaginated(18, $commonData['sort'], $commonData['order']);
             $data = array_merge($commonData, ['shelves' => $shelves]);
 
-            return view('common.home-shelves', $data);
+            return view('home.shelves', $data);
         }
 
         if ($homepageOption === 'books') {
@@ -90,36 +90,35 @@ class HomeController extends Controller
             $books = $bookRepo->getAllPaginated(18, $commonData['sort'], $commonData['order']);
             $data = array_merge($commonData, ['books' => $books]);
 
-            return view('common.home-book', $data);
+            return view('home.books', $data);
         }
 
         if ($homepageOption === 'page') {
             $homepageSetting = setting('app-homepage', '0:');
             $id = intval(explode(':', $homepageSetting)[0]);
+            /** @var Page $customHomepage */
             $customHomepage = Page::query()->where('draft', '=', false)->findOrFail($id);
             $pageContent = new PageContent($customHomepage);
-            $customHomepage->html = $pageContent->render(true);
+            $customHomepage->html = $pageContent->render(false);
 
-            return view('common.home-custom', array_merge($commonData, ['customHomepage' => $customHomepage]));
+            return view('home.specific-page', array_merge($commonData, ['customHomepage' => $customHomepage]));
         }
 
-        return view('common.home', $commonData);
+        return view('home.default', $commonData);
     }
 
     /**
      * Get custom head HTML, Used in ajax calls to show in editor.
-     *
-     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
      */
     public function customHeadContent()
     {
-        return view('partials.custom-head');
+        return view('common.custom-head');
     }
 
     /**
      * Show the view for /robots.txt.
      */
-    public function getRobots()
+    public function robots()
     {
         $sitePublic = setting('app-public', false);
         $allowRobots = config('app.allow_robots');
@@ -129,14 +128,14 @@ class HomeController extends Controller
         }
 
         return response()
-            ->view('common.robots', ['allowRobots' => $allowRobots])
+            ->view('misc.robots', ['allowRobots' => $allowRobots])
             ->header('Content-Type', 'text/plain');
     }
 
     /**
      * Show the route for 404 responses.
      */
-    public function getNotFound()
+    public function notFound()
     {
         return response()->view('errors.404', [], 404);
     }
index 2a4576df1d55f6c5253fb2065b8f2548a0b8f2c2..d99bb8e6f6acbb080712e8089bdfe7cfb4e2c36c 100644 (file)
@@ -30,7 +30,7 @@ class DrawioImageController extends Controller
 
         $imgData = $this->imageRepo->getEntityFiltered('drawio', $parentTypeFilter, $page, 24, $uploadedToFilter, $searchTerm);
 
-        return view('components.image-manager-list', [
+        return view('pages.parts.image-manager-list', [
             'images'  => $imgData['images'],
             'hasMore' => $imgData['has_more'],
         ]);
index 0f0b330eebf66a29b20cea8b071f261d64169459..5484411d36c4208da2055e37d9e5335689c8270a 100644 (file)
@@ -33,7 +33,7 @@ class GalleryImageController extends Controller
 
         $imgData = $this->imageRepo->getEntityFiltered('gallery', $parentTypeFilter, $page, 24, $uploadedToFilter, $searchTerm);
 
-        return view('components.image-manager-list', [
+        return view('pages.parts.image-manager-list', [
             'images'  => $imgData['images'],
             'hasMore' => $imgData['has_more'],
         ]);
index 4c97378879bd6833cf001a288bfb440993d028bb..4070a0e2fe63d1ec13061aba500b2baaf0c8ea87 100644 (file)
@@ -65,7 +65,7 @@ class ImageController extends Controller
 
         $this->imageRepo->loadThumbs($image);
 
-        return view('components.image-manager-form', [
+        return view('pages.parts.image-manager-form', [
             'image'          => $image,
             'dependantPages' => null,
         ]);
@@ -87,7 +87,7 @@ class ImageController extends Controller
 
         $this->imageRepo->loadThumbs($image);
 
-        return view('components.image-manager-form', [
+        return view('pages.parts.image-manager-form', [
             'image'          => $image,
             'dependantPages' => $dependantPages ?? null,
         ]);
index 5a331521674dfa85cb9e22f4c4f7a97cb5dc01f4..0287916de28f40008eeb2d16e948d6274eb77a3a 100644 (file)
@@ -20,6 +20,7 @@ class PageExportController extends Controller
     {
         $this->pageRepo = $pageRepo;
         $this->exportFormatter = $exportFormatter;
+        $this->middleware('can:content-export');
     }
 
     /**
index 232d0d4427a84d4ec28632d42b0af87224f92fe6..1e24c29eeffd40c27fa63ffbe6168fb27f7ade49 100644 (file)
@@ -31,7 +31,7 @@ class PageTemplateController extends Controller
             $templates->appends(['search' => $search]);
         }
 
-        return view('pages.template-manager-list', [
+        return view('pages.parts.template-manager-list', [
             'templates' => $templates,
         ]);
     }
index d4431e1662ba423a485dc46077d7aefe3a85b846..d12c23b5a2c4404cb665bec99c5160eeff4e700a 100644 (file)
@@ -54,7 +54,7 @@ class SearchController extends Controller
         $term = $request->get('term', '');
         $results = $this->searchRunner->searchBook($bookId, $term);
 
-        return view('partials.entity-list', ['entities' => $results]);
+        return view('entities.list', ['entities' => $results]);
     }
 
     /**
@@ -65,7 +65,7 @@ class SearchController extends Controller
         $term = $request->get('term', '');
         $results = $this->searchRunner->searchChapter($chapterId, $term);
 
-        return view('partials.entity-list', ['entities' => $results]);
+        return view('entities.list', ['entities' => $results]);
     }
 
     /**
@@ -86,7 +86,7 @@ class SearchController extends Controller
             $entities = (new Popular())->run(20, 0, $entityTypes, $permission);
         }
 
-        return view('search.entity-ajax-list', ['entities' => $entities]);
+        return view('search.parts.entity-ajax-list', ['entities' => $entities]);
     }
 
     /**
@@ -99,6 +99,6 @@ class SearchController extends Controller
 
         $entities = (new SiblingFetcher())->fetch($type, $id);
 
-        return view('partials.entity-list-basic', ['entities' => $entities, 'style' => 'compact']);
+        return view('entities.list-basic', ['entities' => $entities, 'style' => 'compact']);
     }
 }
index f7b2afef8d2e97d37db5ab1a7bea33bd002a0887..a0da220ee55f9735884bc5befe09677718732bf7 100644 (file)
@@ -123,17 +123,20 @@ class UserController extends Controller
     {
         $this->checkPermissionOrCurrentUser('users-manage', $id);
 
-        $user = $this->user->newQuery()->with(['apiTokens'])->findOrFail($id);
+        /** @var User $user */
+        $user = $this->user->newQuery()->with(['apiTokens', 'mfaValues'])->findOrFail($id);
 
         $authMethod = ($user->system_name) ? 'system' : config('auth.method');
 
         $activeSocialDrivers = $socialAuthService->getActiveDrivers();
+        $mfaMethods = $user->mfaValues->groupBy('method');
         $this->setPageTitle(trans('settings.user_profile'));
         $roles = $this->userRepo->getAllRoles();
 
         return view('users.edit', [
             'user'                => $user,
             'activeSocialDrivers' => $activeSocialDrivers,
+            'mfaMethods'          => $mfaMethods,
             'authMethod'          => $authMethod,
             'roles'               => $roles,
         ]);
index 4150caf04cd4d90621e7fbbe0fd9aafa3eb87125..f7ed9e57a94d1527ac98edd99aa32bf658424e46 100644 (file)
@@ -27,6 +27,6 @@ class UserSearchController extends Controller
 
         $users = $query->get();
 
-        return view('components.user-select-list', compact('users'));
+        return view('form.user-select-list', compact('users'));
     }
 }
index 4f9bfc1e64050dcd7a244c12b453db9ccc04461d..a98528d0f9ebc747a4224974a3500c573e67f0ac 100644 (file)
@@ -24,12 +24,13 @@ class Kernel extends HttpKernel
      */
     protected $middlewareGroups = [
         'web' => [
-            \BookStack\Http\Middleware\ControlIframeSecurity::class,
+            \BookStack\Http\Middleware\ApplyCspRules::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\CheckEmailConfirmed::class,
             \BookStack\Http\Middleware\RunThemeActions::class,
             \BookStack\Http\Middleware\Localization::class,
         ],
@@ -38,6 +39,7 @@ class Kernel extends HttpKernel
             \BookStack\Http\Middleware\EncryptCookies::class,
             \BookStack\Http\Middleware\StartSessionIfCookieExists::class,
             \BookStack\Http\Middleware\ApiAuthenticate::class,
+            \BookStack\Http\Middleware\CheckEmailConfirmed::class,
         ],
     ];
 
@@ -48,10 +50,10 @@ class Kernel extends HttpKernel
      */
     protected $routeMiddleware = [
         'auth'       => \BookStack\Http\Middleware\Authenticate::class,
-        'can'        => \Illuminate\Auth\Middleware\Authorize::class,
+        'can'        => \BookStack\Http\Middleware\CheckUserHasPermission::class,
         'guest'      => \BookStack\Http\Middleware\RedirectIfAuthenticated::class,
         'throttle'   => \Illuminate\Routing\Middleware\ThrottleRequests::class,
-        'perm'       => \BookStack\Http\Middleware\PermissionMiddleware::class,
         'guard'      => \BookStack\Http\Middleware\CheckGuard::class,
+        'mfa-setup'  => \BookStack\Http\Middleware\AuthenticatedOrPendingMfa::class,
     ];
 }
index 21d69810faaf5e685a6303f59c2f02476dd006a8..bc584d3c5a4660e79af15c75352c9ee57042b5f5 100644 (file)
@@ -9,8 +9,6 @@ use Illuminate\Http\Request;
 
 class ApiAuthenticate
 {
-    use ChecksForEmailConfirmation;
-
     /**
      * Handle an incoming request.
      */
@@ -37,7 +35,6 @@ class ApiAuthenticate
         // Return if the user is already found to be signed in via session-based auth.
         // This is to make it easy to browser the API via browser after just logging into the system.
         if (signedInUser() || session()->isStarted()) {
-            $this->ensureEmailConfirmedIfRequested();
             if (!user()->can('access-api')) {
                 throw new ApiAuthException(trans('errors.api_user_no_api_permission'), 403);
             }
@@ -50,7 +47,6 @@ class ApiAuthenticate
 
         // Validate the token and it's users API access
         auth()->authenticate();
-        $this->ensureEmailConfirmedIfRequested();
     }
 
     /**
diff --git a/app/Http/Middleware/ApplyCspRules.php b/app/Http/Middleware/ApplyCspRules.php
new file mode 100644 (file)
index 0000000..6c9d14e
--- /dev/null
@@ -0,0 +1,45 @@
+<?php
+
+namespace BookStack\Http\Middleware;
+
+use BookStack\Util\CspService;
+use Closure;
+use Illuminate\Http\Request;
+
+class ApplyCspRules
+{
+    /**
+     * @var CspService
+     */
+    protected $cspService;
+
+    public function __construct(CspService $cspService)
+    {
+        $this->cspService = $cspService;
+    }
+
+    /**
+     * Handle an incoming request.
+     *
+     * @param Request $request
+     * @param Closure $next
+     *
+     * @return mixed
+     */
+    public function handle($request, Closure $next)
+    {
+        view()->share('cspNonce', $this->cspService->getNonce());
+        if ($this->cspService->allowedIFrameHostsConfigured()) {
+            config()->set('session.same_site', 'none');
+        }
+
+        $response = $next($request);
+
+        $this->cspService->setFrameAncestors($response);
+        $this->cspService->setScriptSrc($response);
+        $this->cspService->setObjectSrc($response);
+        $this->cspService->setBaseUri($response);
+
+        return $response;
+    }
+}
index 3b018cde0f349f3b087507b7ea34864f48267f2a..a320291122b6f632b0bc36a8e96d15e42472a5f8 100644 (file)
@@ -7,47 +7,19 @@ use Illuminate\Http\Request;
 
 class Authenticate
 {
-    use ChecksForEmailConfirmation;
-
     /**
      * Handle an incoming request.
      */
     public function handle(Request $request, Closure $next)
     {
-        if ($this->awaitingEmailConfirmation()) {
-            return $this->emailConfirmationErrorResponse($request);
-        }
-
         if (!hasAppAccess()) {
             if ($request->ajax()) {
                 return response('Unauthorized.', 401);
-            } else {
-                return redirect()->guest(url('/login'));
             }
-        }
-
-        return $next($request);
-    }
 
-    /**
-     * Provide an error response for when the current user's email is not confirmed
-     * in a system which requires it.
-     */
-    protected function emailConfirmationErrorResponse(Request $request)
-    {
-        if ($request->wantsJson()) {
-            return response()->json([
-                'error' => [
-                    'code'    => 401,
-                    'message' => trans('errors.email_confirmation_awaiting'),
-                ],
-            ], 401);
+            return redirect()->guest(url('/login'));
         }
 
-        if (session()->get('sent-email-confirmation') === true) {
-            return redirect('/register/confirm');
-        }
-
-        return redirect('/register/confirm/awaiting');
+        return $next($request);
     }
 }
diff --git a/app/Http/Middleware/AuthenticatedOrPendingMfa.php b/app/Http/Middleware/AuthenticatedOrPendingMfa.php
new file mode 100644 (file)
index 0000000..0a05888
--- /dev/null
@@ -0,0 +1,40 @@
+<?php
+
+namespace BookStack\Http\Middleware;
+
+use BookStack\Auth\Access\LoginService;
+use BookStack\Auth\Access\Mfa\MfaSession;
+use Closure;
+
+class AuthenticatedOrPendingMfa
+{
+    protected $loginService;
+    protected $mfaSession;
+
+    public function __construct(LoginService $loginService, MfaSession $mfaSession)
+    {
+        $this->loginService = $loginService;
+        $this->mfaSession = $mfaSession;
+    }
+
+    /**
+     * Handle an incoming request.
+     *
+     * @param \Illuminate\Http\Request $request
+     * @param \Closure                 $next
+     *
+     * @return mixed
+     */
+    public function handle($request, Closure $next)
+    {
+        $user = auth()->user();
+        $loggedIn = $user !== null;
+        $lastAttemptUser = $this->loginService->getLastLoginAttemptUser();
+
+        if ($loggedIn || ($lastAttemptUser && $this->mfaSession->isPendingMfaSetup($lastAttemptUser))) {
+            return $next($request);
+        }
+
+        return redirect()->to(url('/login'));
+    }
+}
diff --git a/app/Http/Middleware/CheckEmailConfirmed.php b/app/Http/Middleware/CheckEmailConfirmed.php
new file mode 100644 (file)
index 0000000..7dd970a
--- /dev/null
@@ -0,0 +1,49 @@
+<?php
+
+namespace BookStack\Http\Middleware;
+
+use BookStack\Auth\Access\EmailConfirmationService;
+use BookStack\Auth\User;
+use Closure;
+
+/**
+ * Check that the user's email address is confirmed.
+ *
+ * As of v21.08 this is technically not required but kept as a prevention
+ * to log out any users that may be logged in but in an "awaiting confirmation" state.
+ * We'll keep this for a while until it'd be very unlikely for a user to be upgrading from
+ * a pre-v21.08 version.
+ *
+ * Ideally we'd simply invalidate all existing sessions upon update but that has
+ * proven to be a lot more difficult than expected.
+ */
+class CheckEmailConfirmed
+{
+    protected $confirmationService;
+
+    public function __construct(EmailConfirmationService $confirmationService)
+    {
+        $this->confirmationService = $confirmationService;
+    }
+
+    /**
+     * Handle an incoming request.
+     *
+     * @param \Illuminate\Http\Request $request
+     * @param \Closure                 $next
+     *
+     * @return mixed
+     */
+    public function handle($request, Closure $next)
+    {
+        /** @var User $user */
+        $user = auth()->user();
+        if (auth()->check() && !$user->email_confirmed && $this->confirmationService->confirmationRequired()) {
+            auth()->logout();
+
+            return redirect()->to('/');
+        }
+
+        return $next($request);
+    }
+}
diff --git a/app/Http/Middleware/CheckUserHasPermission.php b/app/Http/Middleware/CheckUserHasPermission.php
new file mode 100644 (file)
index 0000000..4a6a064
--- /dev/null
@@ -0,0 +1,38 @@
+<?php
+
+namespace BookStack\Http\Middleware;
+
+use Closure;
+use Illuminate\Http\Request;
+
+class CheckUserHasPermission
+{
+    /**
+     * Handle an incoming request.
+     *
+     * @param \Illuminate\Http\Request $request
+     * @param \Closure                 $next
+     * @param                          $permission
+     *
+     * @return mixed
+     */
+    public function handle($request, Closure $next, $permission)
+    {
+        if (!user()->can($permission)) {
+            return $this->errorResponse($request);
+        }
+
+        return $next($request);
+    }
+
+    protected function errorResponse(Request $request)
+    {
+        if ($request->wantsJson()) {
+            return response()->json(['error' => trans('errors.permissionJson')], 403);
+        }
+
+        session()->flash('error', trans('errors.permission'));
+
+        return redirect('/');
+    }
+}
diff --git a/app/Http/Middleware/ChecksForEmailConfirmation.php b/app/Http/Middleware/ChecksForEmailConfirmation.php
deleted file mode 100644 (file)
index 2eabeca..0000000
+++ /dev/null
@@ -1,36 +0,0 @@
-<?php
-
-namespace BookStack\Http\Middleware;
-
-use BookStack\Exceptions\UnauthorizedException;
-
-trait ChecksForEmailConfirmation
-{
-    /**
-     * Check if the current user has a confirmed email if the instance deems it as required.
-     * Throws if confirmation is required by the user.
-     *
-     * @throws UnauthorizedException
-     */
-    protected function ensureEmailConfirmedIfRequested()
-    {
-        if ($this->awaitingEmailConfirmation()) {
-            throw new UnauthorizedException(trans('errors.email_confirmation_awaiting'));
-        }
-    }
-
-    /**
-     * Check if email confirmation is required and the current user is awaiting confirmation.
-     */
-    protected function awaitingEmailConfirmation(): bool
-    {
-        if (auth()->check()) {
-            $requireConfirmation = (setting('registration-confirmation') || setting('registration-restrict'));
-            if ($requireConfirmation && !auth()->user()->email_confirmed) {
-                return true;
-            }
-        }
-
-        return false;
-    }
-}
diff --git a/app/Http/Middleware/ControlIframeSecurity.php b/app/Http/Middleware/ControlIframeSecurity.php
deleted file mode 100644 (file)
index 11d9e6d..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-<?php
-
-namespace BookStack\Http\Middleware;
-
-use Closure;
-
-/**
- * Sets CSP headers to restrict the hosts that BookStack can be
- * iframed within. Also adjusts the cookie samesite options
- * so that cookies will operate in the third-party context.
- */
-class ControlIframeSecurity
-{
-    /**
-     * Handle an incoming request.
-     *
-     * @param \Illuminate\Http\Request $request
-     * @param \Closure                 $next
-     *
-     * @return mixed
-     */
-    public function handle($request, Closure $next)
-    {
-        $iframeHosts = collect(explode(' ', config('app.iframe_hosts', '')))->filter();
-        if ($iframeHosts->count() > 0) {
-            config()->set('session.same_site', 'none');
-        }
-
-        $iframeHosts->prepend("'self'");
-
-        $response = $next($request);
-        $cspValue = 'frame-ancestors ' . $iframeHosts->join(' ');
-        $response->headers->set('Content-Security-Policy', $cspValue);
-
-        return $response;
-    }
-}
index b29bbe4b3c60491acf2e2e7e1e1eb64c4fc56dec..e824651469b5aa30493c5004ed1affdd6ac89ac8 100644 (file)
@@ -34,6 +34,7 @@ class Localization
         'it'          => 'it_IT',
         'ja'          => 'ja',
         'ko'          => 'ko_KR',
+        'lt'          => 'lt_LT',
         'lv'          => 'lv_LV',
         'nl'          => 'nl_NL',
         'nb'          => 'nb_NO',
diff --git a/app/Http/Middleware/PermissionMiddleware.php b/app/Http/Middleware/PermissionMiddleware.php
deleted file mode 100644 (file)
index 1d7e4aa..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-<?php
-
-namespace BookStack\Http\Middleware;
-
-use Closure;
-
-class PermissionMiddleware
-{
-    /**
-     * Handle an incoming request.
-     *
-     * @param \Illuminate\Http\Request $request
-     * @param \Closure                 $next
-     * @param                          $permission
-     *
-     * @return mixed
-     */
-    public function handle($request, Closure $next, $permission)
-    {
-        if (!$request->user() || !$request->user()->can($permission)) {
-            session()->flash('error', trans('errors.permission'));
-
-            return redirect()->back();
-        }
-
-        return $next($request);
-    }
-}
index abee6bcda2edd772d0c8cd5e8c0753534fd47a4f..8334bb179ae73b4e08982271d37dd4b0cf2ee971 100644 (file)
@@ -2,7 +2,7 @@
 
 namespace BookStack\Providers;
 
-use Blade;
+use BookStack\Auth\Access\LoginService;
 use BookStack\Auth\Access\SocialAuthService;
 use BookStack\Entities\BreadcrumbsViewComposer;
 use BookStack\Entities\Models\Book;
@@ -11,13 +11,15 @@ use BookStack\Entities\Models\Chapter;
 use BookStack\Entities\Models\Page;
 use BookStack\Settings\Setting;
 use BookStack\Settings\SettingService;
+use BookStack\Util\CspService;
 use Illuminate\Contracts\Cache\Repository;
 use Illuminate\Database\Eloquent\Relations\Relation;
+use Illuminate\Support\Facades\Blade;
+use Illuminate\Support\Facades\Schema;
+use Illuminate\Support\Facades\URL;
 use Illuminate\Support\Facades\View;
 use Illuminate\Support\ServiceProvider;
 use Laravel\Socialite\Contracts\Factory as SocialiteFactory;
-use Schema;
-use URL;
 
 class AppServiceProvider extends ServiceProvider
 {
@@ -53,7 +55,7 @@ class AppServiceProvider extends ServiceProvider
         ]);
 
         // View Composers
-        View::composer('partials.breadcrumbs', BreadcrumbsViewComposer::class);
+        View::composer('entities.breadcrumbs', BreadcrumbsViewComposer::class);
     }
 
     /**
@@ -68,7 +70,11 @@ class AppServiceProvider extends ServiceProvider
         });
 
         $this->app->singleton(SocialAuthService::class, function ($app) {
-            return new SocialAuthService($app->make(SocialiteFactory::class));
+            return new SocialAuthService($app->make(SocialiteFactory::class), $app->make(LoginService::class));
+        });
+
+        $this->app->singleton(CspService::class, function ($app) {
+            return new CspService();
         });
     }
 }
index 1a78214dc8937ff039d86d8e69bdcdb20c2dffd9..37b0e83b9ac9b6a6e4b390aa4e30d5f0c7d906b5 100644 (file)
@@ -2,13 +2,14 @@
 
 namespace BookStack\Providers;
 
-use Auth;
 use BookStack\Api\ApiTokenGuard;
 use BookStack\Auth\Access\ExternalBaseUserProvider;
 use BookStack\Auth\Access\Guards\LdapSessionGuard;
 use BookStack\Auth\Access\Guards\Saml2SessionGuard;
 use BookStack\Auth\Access\LdapService;
+use BookStack\Auth\Access\LoginService;
 use BookStack\Auth\Access\RegistrationService;
+use Illuminate\Support\Facades\Auth;
 use Illuminate\Support\ServiceProvider;
 
 class AuthServiceProvider extends ServiceProvider
@@ -21,7 +22,7 @@ class AuthServiceProvider extends ServiceProvider
     public function boot()
     {
         Auth::extend('api-token', function ($app, $name, array $config) {
-            return new ApiTokenGuard($app['request']);
+            return new ApiTokenGuard($app['request'], $app->make(LoginService::class));
         });
 
         Auth::extend('ldap-session', function ($app, $name, array $config) {
@@ -30,7 +31,7 @@ class AuthServiceProvider extends ServiceProvider
             return new LdapSessionGuard(
                 $name,
                 $provider,
-                $this->app['session.store'],
+                $app['session.store'],
                 $app[LdapService::class],
                 $app[RegistrationService::class]
             );
@@ -42,7 +43,7 @@ class AuthServiceProvider extends ServiceProvider
             return new Saml2SessionGuard(
                 $name,
                 $provider,
-                $this->app['session.store'],
+                $app['session.store'],
                 $app[RegistrationService::class]
             );
         });
index 8f0dab400c7efd4ce00902d7ef94874a654e516e..b60443a452895fc19a8c4a5dcffdfeb03f339b52 100644 (file)
@@ -3,7 +3,7 @@
 namespace BookStack\Providers;
 
 use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
-use Route;
+use Illuminate\Support\Facades\Route;
 
 class RouteServiceProvider extends ServiceProvider
 {
diff --git a/app/Theming/CustomHtmlHeadContentProvider.php b/app/Theming/CustomHtmlHeadContentProvider.php
new file mode 100644 (file)
index 0000000..041e5d0
--- /dev/null
@@ -0,0 +1,64 @@
+<?php
+
+namespace BookStack\Theming;
+
+use BookStack\Util\CspService;
+use BookStack\Util\HtmlContentFilter;
+use BookStack\Util\HtmlNonceApplicator;
+use Illuminate\Contracts\Cache\Repository as Cache;
+
+class CustomHtmlHeadContentProvider
+{
+    /**
+     * @var CspService
+     */
+    protected $cspService;
+
+    /**
+     * @var Cache
+     */
+    protected $cache;
+
+    public function __construct(CspService $cspService, Cache $cache)
+    {
+        $this->cspService = $cspService;
+        $this->cache = $cache;
+    }
+
+    /**
+     * Fetch our custom HTML head content prepared for use on web pages.
+     * Content has a nonce applied for CSP.
+     */
+    public function forWeb(): string
+    {
+        $content = $this->getSourceContent();
+        $hash = md5($content);
+        $html = $this->cache->remember('custom-head-web:' . $hash, 86400, function () use ($content) {
+            return HtmlNonceApplicator::prepare($content);
+        });
+
+        return HtmlNonceApplicator::apply($html, $this->cspService->getNonce());
+    }
+
+    /**
+     * Fetch our custom HTML head content prepared for use in export formats.
+     * Scripts are stripped to avoid potential issues.
+     */
+    public function forExport(): string
+    {
+        $content = $this->getSourceContent();
+        $hash = md5($content);
+
+        return $this->cache->remember('custom-head-export:' . $hash, 86400, function () use ($content) {
+            return HtmlContentFilter::removeScripts($content);
+        });
+    }
+
+    /**
+     * Get the original custom head content to use.
+     */
+    protected function getSourceContent(): string
+    {
+        return setting('app-custom-head', '');
+    }
+}
index 76bf649f942a36459e70ab532ad6097ddd91f26a..1965556a9f13e47f6962cfcedcd0e67431295232 100644 (file)
@@ -41,7 +41,7 @@ class ThemeEvents
      * Provides both the original request and the currently resolved response.
      * Return values, if provided, will be used as a new response to use.
      *
-     * @param \Illuminate\Http\Request $request
+     * @param \Illuminate\Http\Request                                                      $request
      * @param \Illuminate\Http\Response|Symfony\Component\HttpFoundation\BinaryFileResponse $response
      * @returns \Illuminate\Http\Response|null
      */
index 775eefc472b72f1553253ee376cbd2fe50c63569..de1124046183186ffeca53be74b1e3db3399293d 100644 (file)
@@ -26,7 +26,8 @@ class FileLoader extends BaseLoader
         if (is_null($namespace) || $namespace === '*') {
             $themePath = theme_path('lang');
             $themeTranslations = $themePath ? $this->loadPath($themePath, $locale, $group) : [];
-            $originalTranslations =  $this->loadPath($this->path, $locale, $group);
+            $originalTranslations = $this->loadPath($this->path, $locale, $group);
+
             return array_merge($originalTranslations, $themeTranslations);
         }
 
index 298d53a04c109ff42175318fd3ed69f9166ea375..b4cb1b88b15e26b0d982174b97fa5b80ed13c385 100644 (file)
@@ -7,8 +7,8 @@ use Exception;
 use Illuminate\Contracts\Filesystem\Factory as FileSystem;
 use Illuminate\Contracts\Filesystem\FileNotFoundException;
 use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
+use Illuminate\Support\Facades\Log;
 use Illuminate\Support\Str;
-use Log;
 use Symfony\Component\HttpFoundation\File\UploadedFile;
 
 class AttachmentService
index 51ddf9bdc55912c3884e9b126aa3b92634a66ab2..2c38c24f4f7c5f6d31646722937516e42b4cb042 100644 (file)
@@ -3,7 +3,6 @@
 namespace BookStack\Uploads;
 
 use BookStack\Exceptions\ImageUploadException;
-use DB;
 use ErrorException;
 use Exception;
 use Illuminate\Contracts\Cache\Repository as Cache;
@@ -11,6 +10,7 @@ use Illuminate\Contracts\Filesystem\Factory as FileSystem;
 use Illuminate\Contracts\Filesystem\FileNotFoundException;
 use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
 use Illuminate\Contracts\Filesystem\Filesystem as Storage;
+use Illuminate\Support\Facades\DB;
 use Illuminate\Support\Str;
 use Intervention\Image\Exception\NotSupportedException;
 use Intervention\Image\ImageManager;
diff --git a/app/Util/CspService.php b/app/Util/CspService.php
new file mode 100644 (file)
index 0000000..812e1a4
--- /dev/null
@@ -0,0 +1,96 @@
+<?php
+
+namespace BookStack\Util;
+
+use Illuminate\Support\Str;
+use Symfony\Component\HttpFoundation\Response;
+
+class CspService
+{
+    /** @var string */
+    protected $nonce;
+
+    public function __construct(string $nonce = '')
+    {
+        $this->nonce = $nonce ?: Str::random(24);
+    }
+
+    /**
+     * Get the nonce value for CSP.
+     */
+    public function getNonce(): string
+    {
+        return $this->nonce;
+    }
+
+    /**
+     * Sets CSP 'script-src' headers to restrict the forms of script that can
+     * run on the page.
+     */
+    public function setScriptSrc(Response $response)
+    {
+        if (config('app.allow_content_scripts')) {
+            return;
+        }
+
+        $parts = [
+            'http:',
+            'https:',
+            '\'nonce-' . $this->nonce . '\'',
+            '\'strict-dynamic\'',
+        ];
+
+        $value = 'script-src ' . implode(' ', $parts);
+        $response->headers->set('Content-Security-Policy', $value, false);
+    }
+
+    /**
+     * Sets CSP "frame-ancestors" 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.
+     */
+    public function setFrameAncestors(Response $response)
+    {
+        $iframeHosts = $this->getAllowedIframeHosts();
+        array_unshift($iframeHosts, "'self'");
+        $cspValue = 'frame-ancestors ' . implode(' ', $iframeHosts);
+        $response->headers->set('Content-Security-Policy', $cspValue, false);
+    }
+
+    /**
+     * Check if the user has configured some allowed iframe hosts.
+     */
+    public function allowedIFrameHostsConfigured(): bool
+    {
+        return count($this->getAllowedIframeHosts()) > 0;
+    }
+
+    /**
+     * Sets CSP 'object-src' headers to restrict the types of dynamic content
+     * that can be embedded on the page.
+     */
+    public function setObjectSrc(Response $response)
+    {
+        if (config('app.allow_content_scripts')) {
+            return;
+        }
+
+        $response->headers->set('Content-Security-Policy', 'object-src \'self\'', false);
+    }
+
+    /**
+     * Sets CSP 'base-uri' headers to restrict what base tags can be set on
+     * the page to prevent manipulation of relative links.
+     */
+    public function setBaseUri(Response $response)
+    {
+        $response->headers->set('Content-Security-Policy', 'base-uri \'self\'', false);
+    }
+
+    protected function getAllowedIframeHosts(): array
+    {
+        $hosts = config('app.iframe_hosts', '');
+
+        return array_filter(explode(' ', $hosts));
+    }
+}
index f251a22fdc94730ea1fc049299641b3cd42a279a..1943aa7802c81d6edb7707bc0f7d211d45514839 100644 (file)
@@ -2,6 +2,7 @@
 
 namespace BookStack\Util;
 
+use DOMAttr;
 use DOMDocument;
 use DOMNodeList;
 use DOMXPath;
@@ -9,7 +10,7 @@ use DOMXPath;
 class HtmlContentFilter
 {
     /**
-     * Remove all of the script elements from the given HTML.
+     * Remove all the script elements from the given HTML.
      */
     public static function removeScripts(string $html): string
     {
@@ -28,28 +29,29 @@ class HtmlContentFilter
         static::removeNodes($scriptElems);
 
         // Remove clickable links to JavaScript URI
-        $badLinks = $xPath->query('//*[contains(@href, \'javascript:\')]');
+        $badLinks = $xPath->query('//*[' . static::xpathContains('@href', 'javascript:') . ']');
         static::removeNodes($badLinks);
 
         // Remove forms with calls to JavaScript URI
-        $badForms = $xPath->query('//*[contains(@action, \'javascript:\')] | //*[contains(@formaction, \'javascript:\')]');
+        $badForms = $xPath->query('//*[' . static::xpathContains('@action', 'javascript:') . '] | //*[' . static::xpathContains('@formaction', 'javascript:') . ']');
         static::removeNodes($badForms);
 
         // Remove meta tag to prevent external redirects
-        $metaTags = $xPath->query('//meta[contains(@content, \'url\')]');
+        $metaTags = $xPath->query('//meta[' . static::xpathContains('@content', 'url') . ']');
         static::removeNodes($metaTags);
 
         // Remove data or JavaScript iFrames
-        $badIframes = $xPath->query('//*[contains(@src, \'data:\')] | //*[contains(@src, \'javascript:\')] | //*[@srcdoc]');
+        $badIframes = $xPath->query('//*[' . static::xpathContains('@src', 'data:') . '] | //*[' . static::xpathContains('@src', 'javascript:') . '] | //*[@srcdoc]');
         static::removeNodes($badIframes);
 
+        // Remove elements with a xlink:href attribute
+        // Used in SVG but deprecated anyway, so we'll be a bit more heavy-handed here.
+        $xlinkHrefAttributes = $xPath->query('//@*[contains(name(), \'xlink:href\')]');
+        static::removeAttributes($xlinkHrefAttributes);
+
         // Remove 'on*' attributes
         $onAttributes = $xPath->query('//@*[starts-with(name(), \'on\')]');
-        foreach ($onAttributes as $attr) {
-            /** @var \DOMAttr $attr */
-            $attrName = $attr->nodeName;
-            $attr->parentNode->removeAttribute($attrName);
-        }
+        static::removeAttributes($onAttributes);
 
         $html = '';
         $topElems = $doc->documentElement->childNodes->item(0)->childNodes;
@@ -61,7 +63,19 @@ class HtmlContentFilter
     }
 
     /**
-     * Removed all of the given DOMNodes.
+     * Create a xpath contains statement with a translation automatically built within
+     * to affectively search in a cases-insensitive manner.
+     */
+    protected static function xpathContains(string $property, string $value): string
+    {
+        $value = strtolower($value);
+        $upperVal = strtoupper($value);
+
+        return 'contains(translate(' . $property . ', \'' . $upperVal . '\', \'' . $value . '\'), \'' . $value . '\')';
+    }
+
+    /**
+     * Remove all the given DOMNodes.
      */
     protected static function removeNodes(DOMNodeList $nodes): void
     {
@@ -69,4 +83,16 @@ class HtmlContentFilter
             $node->parentNode->removeChild($node);
         }
     }
+
+    /**
+     * Remove all the given attribute nodes.
+     */
+    protected static function removeAttributes(DOMNodeList $attrs): void
+    {
+        /** @var DOMAttr $attr */
+        foreach ($attrs as $attr) {
+            $attrName = $attr->nodeName;
+            $attr->parentNode->removeAttribute($attrName);
+        }
+    }
 }
diff --git a/app/Util/HtmlNonceApplicator.php b/app/Util/HtmlNonceApplicator.php
new file mode 100644 (file)
index 0000000..0729857
--- /dev/null
@@ -0,0 +1,63 @@
+<?php
+
+namespace BookStack\Util;
+
+use DOMDocument;
+use DOMElement;
+use DOMNodeList;
+use DOMXPath;
+
+class HtmlNonceApplicator
+{
+    protected static $placeholder = '[CSP_NONCE_VALUE]';
+
+    /**
+     * Prepare the given HTML content with nonce attributes including a placeholder
+     * value which we can target later.
+     */
+    public static function prepare(string $html): string
+    {
+        if (empty($html)) {
+            return $html;
+        }
+
+        $html = '<?xml encoding="utf-8" ?><body>' . $html . '</body>';
+        libxml_use_internal_errors(true);
+        $doc = new DOMDocument();
+        $doc->loadHTML($html, LIBXML_SCHEMA_CREATE);
+        $xPath = new DOMXPath($doc);
+
+        // Apply to scripts
+        $scriptElems = $xPath->query('//script');
+        static::addNonceAttributes($scriptElems, static::$placeholder);
+
+        // Apply to styles
+        $styleElems = $xPath->query('//style');
+        static::addNonceAttributes($styleElems, static::$placeholder);
+
+        $returnHtml = '';
+        $topElems = $doc->documentElement->childNodes->item(0)->childNodes;
+        foreach ($topElems as $child) {
+            $content = $doc->saveHTML($child);
+            $returnHtml .= $content;
+        }
+
+        return $returnHtml;
+    }
+
+    /**
+     * Apply the give nonce value to the given prepared HTML.
+     */
+    public static function apply(string $html, string $nonce): string
+    {
+        return str_replace(static::$placeholder, $nonce, $html);
+    }
+
+    protected static function addNonceAttributes(DOMNodeList $nodes, string $attrValue): void
+    {
+        /** @var DOMElement $node */
+        foreach ($nodes as $node) {
+            $node->setAttribute('nonce', $attrValue);
+        }
+    }
+}
index bbd689454b9891714549ae4b3c9a79dc55b7e31b..31ecbef84d54c2a3c9900daea88bf12a1bd28cbb 100644 (file)
         "ext-json": "*",
         "ext-mbstring": "*",
         "ext-xml": "*",
+        "bacon/bacon-qr-code": "^2.0",
         "barryvdh/laravel-dompdf": "^0.9.0",
         "barryvdh/laravel-snappy": "^0.4.8",
         "doctrine/dbal": "^2.12.1",
         "facade/ignition": "^1.16.4",
         "fideloper/proxy": "^4.4.1",
         "intervention/image": "^2.5.1",
-        "laravel/framework": "^6.20.16",
+        "laravel/framework": "^6.20.33",
         "laravel/socialite": "^5.1",
         "league/commonmark": "^1.5",
         "league/flysystem-aws-s3-v3": "^1.0.29",
         "league/html-to-markdown": "^5.0.0",
         "nunomaduro/collision": "^3.1",
         "onelogin/php-saml": "^4.0",
+        "pragmarx/google2fa": "^8.0",
         "predis/predis": "^1.1.6",
         "socialiteproviders/discord": "^4.1",
         "socialiteproviders/gitlab": "^4.1",
@@ -39,9 +41,9 @@
         "barryvdh/laravel-debugbar": "^3.5.1",
         "barryvdh/laravel-ide-helper": "^2.8.2",
         "fakerphp/faker": "^1.13.0",
-        "laravel/browser-kit-testing": "^5.2",
         "mockery/mockery": "^1.3.3",
-        "phpunit/phpunit": "^9.5.3"
+        "phpunit/phpunit": "^9.5.3",
+        "symfony/dom-crawler": "^5.3"
     },
     "autoload": {
         "classmap": [
index a9ba1b0a46233c52e6b083d5b68f0339854f9f77..d267d13d65d6257988fdb0c4a6d51e28ad190299 100644 (file)
@@ -4,23 +4,74 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "d1109d0dc4a6ab525cdbf64ed21f6dd4",
+    "content-hash": "10825887b8f66d1d412b92bcc0ca864f",
     "packages": [
+        {
+            "name": "aws/aws-crt-php",
+            "version": "v1.0.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/awslabs/aws-crt-php.git",
+                "reference": "3942776a8c99209908ee0b287746263725685732"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/3942776a8c99209908ee0b287746263725685732",
+                "reference": "3942776a8c99209908ee0b287746263725685732",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.5"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^4.8.35|^5.4.3"
+            },
+            "type": "library",
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "Apache-2.0"
+            ],
+            "authors": [
+                {
+                    "name": "AWS SDK Common Runtime Team",
+                    "email": "[email protected]"
+                }
+            ],
+            "description": "AWS Common Runtime for PHP",
+            "homepage": "http://aws.amazon.com/sdkforphp",
+            "keywords": [
+                "amazon",
+                "aws",
+                "crt",
+                "sdk"
+            ],
+            "support": {
+                "issues": "https://github.com/awslabs/aws-crt-php/issues",
+                "source": "https://github.com/awslabs/aws-crt-php/tree/v1.0.2"
+            },
+            "time": "2021-09-03T22:57:30+00:00"
+        },
         {
             "name": "aws/aws-sdk-php",
-            "version": "3.187.2",
+            "version": "3.194.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/aws/aws-sdk-php.git",
-                "reference": "0ec4ae120cfae758efa3c283dc56eb20602f094c"
+                "reference": "67bdee05acef9e8ad60098090996690b49babd09"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/0ec4ae120cfae758efa3c283dc56eb20602f094c",
-                "reference": "0ec4ae120cfae758efa3c283dc56eb20602f094c",
+                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/67bdee05acef9e8ad60098090996690b49babd09",
+                "reference": "67bdee05acef9e8ad60098090996690b49babd09",
                 "shasum": ""
             },
             "require": {
+                "aws/aws-crt-php": "^1.0.2",
                 "ext-json": "*",
                 "ext-pcre": "*",
                 "ext-simplexml": "*",
             "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.187.2"
+                "source": "https://github.com/aws/aws-sdk-php/tree/3.194.1"
+            },
+            "time": "2021-09-17T18:15:42+00:00"
+        },
+        {
+            "name": "bacon/bacon-qr-code",
+            "version": "2.0.4",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/Bacon/BaconQrCode.git",
+                "reference": "f73543ac4e1def05f1a70bcd1525c8a157a1ad09"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/f73543ac4e1def05f1a70bcd1525c8a157a1ad09",
+                "reference": "f73543ac4e1def05f1a70bcd1525c8a157a1ad09",
+                "shasum": ""
+            },
+            "require": {
+                "dasprid/enum": "^1.0.3",
+                "ext-iconv": "*",
+                "php": "^7.1 || ^8.0"
+            },
+            "require-dev": {
+                "phly/keep-a-changelog": "^1.4",
+                "phpunit/phpunit": "^7 | ^8 | ^9",
+                "squizlabs/php_codesniffer": "^3.4"
+            },
+            "suggest": {
+                "ext-imagick": "to generate QR code images"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "BaconQrCode\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-2-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Ben Scholzen 'DASPRiD'",
+                    "email": "[email protected]",
+                    "homepage": "https://dasprids.de/",
+                    "role": "Developer"
+                }
+            ],
+            "description": "BaconQrCode is a QR code generator for PHP.",
+            "homepage": "https://github.com/Bacon/BaconQrCode",
+            "support": {
+                "issues": "https://github.com/Bacon/BaconQrCode/issues",
+                "source": "https://github.com/Bacon/BaconQrCode/tree/2.0.4"
             },
-            "time": "2021-08-04T18:12:21+00:00"
+            "time": "2021-06-18T13:26:35+00:00"
         },
         {
             "name": "barryvdh/laravel-dompdf",
             },
             "time": "2020-09-07T12:33:10+00:00"
         },
+        {
+            "name": "dasprid/enum",
+            "version": "1.0.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/DASPRiD/Enum.git",
+                "reference": "5abf82f213618696dda8e3bf6f64dd042d8542b2"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/5abf82f213618696dda8e3bf6f64dd042d8542b2",
+                "reference": "5abf82f213618696dda8e3bf6f64dd042d8542b2",
+                "shasum": ""
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^7 | ^8 | ^9",
+                "squizlabs/php_codesniffer": "^3.4"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "DASPRiD\\Enum\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-2-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Ben Scholzen 'DASPRiD'",
+                    "email": "[email protected]",
+                    "homepage": "https://dasprids.de/",
+                    "role": "Developer"
+                }
+            ],
+            "description": "PHP 7.1 enum implementation",
+            "keywords": [
+                "enum",
+                "map"
+            ],
+            "support": {
+                "issues": "https://github.com/DASPRiD/Enum/issues",
+                "source": "https://github.com/DASPRiD/Enum/tree/1.0.3"
+            },
+            "time": "2020-10-02T16:03:48+00:00"
+        },
         {
             "name": "doctrine/cache",
             "version": "2.1.1",
         },
         {
             "name": "doctrine/dbal",
-            "version": "2.13.2",
+            "version": "2.13.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/doctrine/dbal.git",
-                "reference": "8dd39d2ead4409ce652fd4f02621060f009ea5e4"
+                "reference": "0d7adf4cadfee6f70850e5b163e6cdd706417838"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/doctrine/dbal/zipball/8dd39d2ead4409ce652fd4f02621060f009ea5e4",
-                "reference": "8dd39d2ead4409ce652fd4f02621060f009ea5e4",
+                "url": "https://api.github.com/repos/doctrine/dbal/zipball/0d7adf4cadfee6f70850e5b163e6cdd706417838",
+                "reference": "0d7adf4cadfee6f70850e5b163e6cdd706417838",
                 "shasum": ""
             },
             "require": {
             },
             "require-dev": {
                 "doctrine/coding-standard": "9.0.0",
-                "jetbrains/phpstorm-stubs": "2020.2",
-                "phpstan/phpstan": "0.12.81",
+                "jetbrains/phpstorm-stubs": "2021.1",
+                "phpstan/phpstan": "0.12.96",
                 "phpunit/phpunit": "^7.5.20|^8.5|9.5.5",
+                "psalm/plugin-phpunit": "0.16.1",
                 "squizlabs/php_codesniffer": "3.6.0",
                 "symfony/cache": "^4.4",
                 "symfony/console": "^2.0.5|^3.0|^4.0|^5.0",
-                "vimeo/psalm": "4.6.4"
+                "vimeo/psalm": "4.10.0"
             },
             "suggest": {
                 "symfony/console": "For helpful console commands such as SQL execution and import of files."
             ],
             "support": {
                 "issues": "https://github.com/doctrine/dbal/issues",
-                "source": "https://github.com/doctrine/dbal/tree/2.13.2"
+                "source": "https://github.com/doctrine/dbal/tree/2.13.3"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2021-06-18T21:48:39+00:00"
+            "time": "2021-09-12T19:11:48+00:00"
         },
         {
             "name": "doctrine/deprecations",
         },
         {
             "name": "facade/flare-client-php",
-            "version": "1.8.1",
+            "version": "1.9.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/facade/flare-client-php.git",
-                "reference": "47b639dc02bcfdfc4ebb83de703856fa01e35f5f"
+                "reference": "b2adf1512755637d0cef4f7d1b54301325ac78ed"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/facade/flare-client-php/zipball/47b639dc02bcfdfc4ebb83de703856fa01e35f5f",
-                "reference": "47b639dc02bcfdfc4ebb83de703856fa01e35f5f",
+                "url": "https://api.github.com/repos/facade/flare-client-php/zipball/b2adf1512755637d0cef4f7d1b54301325ac78ed",
+                "reference": "b2adf1512755637d0cef4f7d1b54301325ac78ed",
                 "shasum": ""
             },
             "require": {
             ],
             "support": {
                 "issues": "https://github.com/facade/flare-client-php/issues",
-                "source": "https://github.com/facade/flare-client-php/tree/1.8.1"
+                "source": "https://github.com/facade/flare-client-php/tree/1.9.1"
             },
             "funding": [
                 {
                     "type": "github"
                 }
             ],
-            "time": "2021-05-31T19:23:29+00:00"
+            "time": "2021-09-13T12:16:46+00:00"
         },
         {
             "name": "facade/ignition",
         },
         {
             "name": "filp/whoops",
-            "version": "2.14.0",
+            "version": "2.14.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/filp/whoops.git",
-                "reference": "fdf92f03e150ed84d5967a833ae93abffac0315b"
+                "reference": "15ead64e9828f0fc90932114429c4f7923570cb1"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/filp/whoops/zipball/fdf92f03e150ed84d5967a833ae93abffac0315b",
-                "reference": "fdf92f03e150ed84d5967a833ae93abffac0315b",
+                "url": "https://api.github.com/repos/filp/whoops/zipball/15ead64e9828f0fc90932114429c4f7923570cb1",
+                "reference": "15ead64e9828f0fc90932114429c4f7923570cb1",
                 "shasum": ""
             },
             "require": {
             ],
             "support": {
                 "issues": "https://github.com/filp/whoops/issues",
-                "source": "https://github.com/filp/whoops/tree/2.14.0"
+                "source": "https://github.com/filp/whoops/tree/2.14.1"
             },
             "funding": [
                 {
                     "type": "github"
                 }
             ],
-            "time": "2021-07-13T12:00:00+00:00"
+            "time": "2021-08-29T12:00:00+00:00"
         },
         {
             "name": "guzzlehttp/guzzle",
         },
         {
             "name": "laravel/framework",
-            "version": "v6.20.31",
+            "version": "v6.20.34",
             "source": {
                 "type": "git",
                 "url": "https://github.com/laravel/framework.git",
-                "reference": "15cd96b48fb4f652ba996041cdb9a2a3b03e00f5"
+                "reference": "72a6da88c90cee793513b3fe49cf0fcb368eefa0"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/laravel/framework/zipball/15cd96b48fb4f652ba996041cdb9a2a3b03e00f5",
-                "reference": "15cd96b48fb4f652ba996041cdb9a2a3b03e00f5",
+                "url": "https://api.github.com/repos/laravel/framework/zipball/72a6da88c90cee793513b3fe49cf0fcb368eefa0",
+                "reference": "72a6da88c90cee793513b3fe49cf0fcb368eefa0",
                 "shasum": ""
             },
             "require": {
                 "issues": "https://github.com/laravel/framework/issues",
                 "source": "https://github.com/laravel/framework"
             },
-            "time": "2021-08-03T14:36:18+00:00"
+            "time": "2021-09-07T13:28:55+00:00"
         },
         {
             "name": "laravel/socialite",
-            "version": "v5.2.3",
+            "version": "v5.2.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/laravel/socialite.git",
-                "reference": "1960802068f81e44b2ae9793932181cf1cb91b5c"
+                "reference": "fd0f6a3dd963ca480b598649b54f92d81a43617f"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/laravel/socialite/zipball/1960802068f81e44b2ae9793932181cf1cb91b5c",
-                "reference": "1960802068f81e44b2ae9793932181cf1cb91b5c",
+                "url": "https://api.github.com/repos/laravel/socialite/zipball/fd0f6a3dd963ca480b598649b54f92d81a43617f",
+                "reference": "fd0f6a3dd963ca480b598649b54f92d81a43617f",
                 "shasum": ""
             },
             "require": {
                 "issues": "https://github.com/laravel/socialite/issues",
                 "source": "https://github.com/laravel/socialite"
             },
-            "time": "2021-04-06T14:38:16+00:00"
+            "time": "2021-08-31T15:16:26+00:00"
         },
         {
             "name": "league/commonmark",
         },
         {
             "name": "league/flysystem",
-            "version": "1.1.4",
+            "version": "1.1.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/thephpleague/flysystem.git",
-                "reference": "f3ad69181b8afed2c9edf7be5a2918144ff4ea32"
+                "reference": "18634df356bfd4119fe3d6156bdb990c414c14ea"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/f3ad69181b8afed2c9edf7be5a2918144ff4ea32",
-                "reference": "f3ad69181b8afed2c9edf7be5a2918144ff4ea32",
+                "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/18634df356bfd4119fe3d6156bdb990c414c14ea",
+                "reference": "18634df356bfd4119fe3d6156bdb990c414c14ea",
                 "shasum": ""
             },
             "require": {
             ],
             "support": {
                 "issues": "https://github.com/thephpleague/flysystem/issues",
-                "source": "https://github.com/thephpleague/flysystem/tree/1.1.4"
+                "source": "https://github.com/thephpleague/flysystem/tree/1.1.5"
             },
             "funding": [
                 {
                     "type": "other"
                 }
             ],
-            "time": "2021-06-23T21:56:05+00:00"
+            "time": "2021-08-17T13:49:42+00:00"
         },
         {
             "name": "league/flysystem-aws-s3-v3",
         },
         {
             "name": "league/html-to-markdown",
-            "version": "5.0.0",
+            "version": "5.0.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/thephpleague/html-to-markdown.git",
-                "reference": "c4dbebbebe0fe454b6b38e6c683a977615bd7dc2"
+                "reference": "e5600a2c5ce7b7571b16732c7086940f56f7abec"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/thephpleague/html-to-markdown/zipball/c4dbebbebe0fe454b6b38e6c683a977615bd7dc2",
-                "reference": "c4dbebbebe0fe454b6b38e6c683a977615bd7dc2",
+                "url": "https://api.github.com/repos/thephpleague/html-to-markdown/zipball/e5600a2c5ce7b7571b16732c7086940f56f7abec",
+                "reference": "e5600a2c5ce7b7571b16732c7086940f56f7abec",
                 "shasum": ""
             },
             "require": {
             ],
             "support": {
                 "issues": "https://github.com/thephpleague/html-to-markdown/issues",
-                "source": "https://github.com/thephpleague/html-to-markdown/tree/5.0.0"
+                "source": "https://github.com/thephpleague/html-to-markdown/tree/5.0.1"
             },
             "funding": [
                 {
                     "type": "github"
                 },
                 {
-                    "url": "https://www.patreon.com/colinodell",
-                    "type": "patreon"
+                    "url": "https://tidelift.com/funding/github/packagist/league/html-to-markdown",
+                    "type": "tidelift"
                 }
             ],
-            "time": "2021-03-29T01:29:08+00:00"
+            "time": "2021-09-17T20:00:27+00:00"
         },
         {
             "name": "league/mime-type-detection",
         },
         {
             "name": "league/oauth1-client",
-            "version": "1.9.2",
+            "version": "v1.10.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/thephpleague/oauth1-client.git",
-                "reference": "6247ffbf6b74fcc7f21313315b79e9b2560e68b0"
+                "reference": "88dd16b0cff68eb9167bfc849707d2c40ad91ddc"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/thephpleague/oauth1-client/zipball/6247ffbf6b74fcc7f21313315b79e9b2560e68b0",
-                "reference": "6247ffbf6b74fcc7f21313315b79e9b2560e68b0",
+                "url": "https://api.github.com/repos/thephpleague/oauth1-client/zipball/88dd16b0cff68eb9167bfc849707d2c40ad91ddc",
+                "reference": "88dd16b0cff68eb9167bfc849707d2c40ad91ddc",
                 "shasum": ""
             },
             "require": {
             ],
             "support": {
                 "issues": "https://github.com/thephpleague/oauth1-client/issues",
-                "source": "https://github.com/thephpleague/oauth1-client/tree/1.9.2"
+                "source": "https://github.com/thephpleague/oauth1-client/tree/v1.10.0"
             },
-            "time": "2021-08-03T23:29:01+00:00"
+            "time": "2021-08-15T23:05:49+00:00"
         },
         {
             "name": "monolog/monolog",
-            "version": "2.3.2",
+            "version": "2.3.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/Seldaek/monolog.git",
-                "reference": "71312564759a7db5b789296369c1a264efc43aad"
+                "reference": "437e7a1c50044b92773b361af77620efb76fff59"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/Seldaek/monolog/zipball/71312564759a7db5b789296369c1a264efc43aad",
-                "reference": "71312564759a7db5b789296369c1a264efc43aad",
+                "url": "https://api.github.com/repos/Seldaek/monolog/zipball/437e7a1c50044b92773b361af77620efb76fff59",
+                "reference": "437e7a1c50044b92773b361af77620efb76fff59",
                 "shasum": ""
             },
             "require": {
                 "php": ">=7.2",
-                "psr/log": "^1.0.1"
+                "psr/log": "^1.0.1 || ^2.0 || ^3.0"
             },
             "provide": {
-                "psr/log-implementation": "1.0.0"
+                "psr/log-implementation": "1.0.0 || 2.0.0 || 3.0.0"
             },
             "require-dev": {
                 "aws/aws-sdk-php": "^2.4.9 || ^3.0",
                 "phpunit/phpunit": "^8.5",
                 "predis/predis": "^1.1",
                 "rollbar/rollbar": "^1.3",
-                "ruflin/elastica": ">=0.90 <7.0.1",
+                "ruflin/elastica": ">=0.90@dev",
                 "swiftmailer/swiftmailer": "^5.3|^6.0"
             },
             "suggest": {
                 "doctrine/couchdb": "Allow sending log messages to a CouchDB server",
                 "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client",
                 "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)",
+                "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler",
                 "ext-mbstring": "Allow to work properly with unicode symbols",
                 "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)",
+                "ext-openssl": "Required to send log messages using SSL",
+                "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)",
                 "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server",
                 "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)",
                 "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib",
             ],
             "support": {
                 "issues": "https://github.com/Seldaek/monolog/issues",
-                "source": "https://github.com/Seldaek/monolog/tree/2.3.2"
+                "source": "https://github.com/Seldaek/monolog/tree/2.3.4"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2021-07-23T07:42:52+00:00"
+            "time": "2021-09-15T11:27:21+00:00"
         },
         {
             "name": "mtdowling/jmespath.php",
         },
         {
             "name": "nesbot/carbon",
-            "version": "2.51.1",
+            "version": "2.53.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/briannesbitt/Carbon.git",
-                "reference": "8619c299d1e0d4b344e1f98ca07a1ce2cfbf1922"
+                "reference": "f4655858a784988f880c1b8c7feabbf02dfdf045"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/8619c299d1e0d4b344e1f98ca07a1ce2cfbf1922",
-                "reference": "8619c299d1e0d4b344e1f98ca07a1ce2cfbf1922",
+                "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/f4655858a784988f880c1b8c7feabbf02dfdf045",
+                "reference": "f4655858a784988f880c1b8c7feabbf02dfdf045",
                 "shasum": ""
             },
             "require": {
             },
             "require-dev": {
                 "doctrine/orm": "^2.7",
-                "friendsofphp/php-cs-fixer": "^2.14 || ^3.0",
+                "friendsofphp/php-cs-fixer": "^3.0",
                 "kylekatarnls/multi-tester": "^2.0",
                 "phpmd/phpmd": "^2.9",
                 "phpstan/extension-installer": "^1.0",
                     "type": "tidelift"
                 }
             ],
-            "time": "2021-07-28T13:16:28+00:00"
+            "time": "2021-09-06T09:29:23+00:00"
         },
         {
             "name": "nunomaduro/collision",
             },
             "time": "2021-04-09T13:42:10+00:00"
         },
+        {
+            "name": "paragonie/constant_time_encoding",
+            "version": "v2.4.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/paragonie/constant_time_encoding.git",
+                "reference": "f34c2b11eb9d2c9318e13540a1dbc2a3afbd939c"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/f34c2b11eb9d2c9318e13540a1dbc2a3afbd939c",
+                "reference": "f34c2b11eb9d2c9318e13540a1dbc2a3afbd939c",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7|^8"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^6|^7|^8|^9",
+                "vimeo/psalm": "^1|^2|^3|^4"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "ParagonIE\\ConstantTime\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Paragon Initiative Enterprises",
+                    "email": "[email protected]",
+                    "homepage": "https://paragonie.com",
+                    "role": "Maintainer"
+                },
+                {
+                    "name": "Steve 'Sc00bz' Thomas",
+                    "email": "[email protected]",
+                    "homepage": "https://www.tobtu.com",
+                    "role": "Original Developer"
+                }
+            ],
+            "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)",
+            "keywords": [
+                "base16",
+                "base32",
+                "base32_decode",
+                "base32_encode",
+                "base64",
+                "base64_decode",
+                "base64_encode",
+                "bin2hex",
+                "encoding",
+                "hex",
+                "hex2bin",
+                "rfc4648"
+            ],
+            "support": {
+                "email": "[email protected]",
+                "issues": "https://github.com/paragonie/constant_time_encoding/issues",
+                "source": "https://github.com/paragonie/constant_time_encoding"
+            },
+            "time": "2020-12-06T15:14:20+00:00"
+        },
         {
             "name": "paragonie/random_compat",
-            "version": "v9.99.99",
+            "version": "v9.99.100",
             "source": {
                 "type": "git",
                 "url": "https://github.com/paragonie/random_compat.git",
-                "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95"
+                "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/paragonie/random_compat/zipball/84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95",
-                "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95",
+                "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a",
+                "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a",
                 "shasum": ""
             },
             "require": {
-                "php": "^7"
+                "php": ">= 7"
             },
             "require-dev": {
                 "phpunit/phpunit": "4.*|5.*",
                 "issues": "https://github.com/paragonie/random_compat/issues",
                 "source": "https://github.com/paragonie/random_compat"
             },
-            "time": "2018-07-02T15:55:56+00:00"
+            "time": "2020-10-15T08:29:30+00:00"
         },
         {
             "name": "phenx/php-font-lib",
         },
         {
             "name": "phpoption/phpoption",
-            "version": "1.7.5",
+            "version": "1.8.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/schmittjoh/php-option.git",
-                "reference": "994ecccd8f3283ecf5ac33254543eb0ac946d525"
+                "reference": "5455cb38aed4523f99977c4a12ef19da4bfe2a28"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/994ecccd8f3283ecf5ac33254543eb0ac946d525",
-                "reference": "994ecccd8f3283ecf5ac33254543eb0ac946d525",
+                "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/5455cb38aed4523f99977c4a12ef19da4bfe2a28",
+                "reference": "5455cb38aed4523f99977c4a12ef19da4bfe2a28",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.5.9 || ^7.0 || ^8.0"
+                "php": "^7.0 || ^8.0"
             },
             "require-dev": {
                 "bamarni/composer-bin-plugin": "^1.4.1",
-                "phpunit/phpunit": "^4.8.35 || ^5.7.27 || ^6.5.6 || ^7.0 || ^8.0 || ^9.0"
+                "phpunit/phpunit": "^6.5.14 || ^7.0.20 || ^8.5.19 || ^9.5.8"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.7-dev"
+                    "dev-master": "1.8-dev"
                 }
             },
             "autoload": {
                 },
                 {
                     "name": "Graham Campbell",
-                    "email": "[email protected]"
+                    "email": "[email protected]"
                 }
             ],
             "description": "Option Type for PHP",
             ],
             "support": {
                 "issues": "https://github.com/schmittjoh/php-option/issues",
-                "source": "https://github.com/schmittjoh/php-option/tree/1.7.5"
+                "source": "https://github.com/schmittjoh/php-option/tree/1.8.0"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-07-20T17:29:33+00:00"
+            "time": "2021-08-28T21:27:29+00:00"
+        },
+        {
+            "name": "pragmarx/google2fa",
+            "version": "8.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/antonioribeiro/google2fa.git",
+                "reference": "26c4c5cf30a2844ba121760fd7301f8ad240100b"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/26c4c5cf30a2844ba121760fd7301f8ad240100b",
+                "reference": "26c4c5cf30a2844ba121760fd7301f8ad240100b",
+                "shasum": ""
+            },
+            "require": {
+                "paragonie/constant_time_encoding": "^1.0|^2.0",
+                "php": "^7.1|^8.0"
+            },
+            "require-dev": {
+                "phpstan/phpstan": "^0.12.18",
+                "phpunit/phpunit": "^7.5.15|^8.5|^9.0"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "PragmaRX\\Google2FA\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Antonio Carlos Ribeiro",
+                    "email": "[email protected]",
+                    "role": "Creator & Designer"
+                }
+            ],
+            "description": "A One Time Password Authentication package, compatible with Google Authenticator.",
+            "keywords": [
+                "2fa",
+                "Authentication",
+                "Two Factor Authentication",
+                "google2fa"
+            ],
+            "support": {
+                "issues": "https://github.com/antonioribeiro/google2fa/issues",
+                "source": "https://github.com/antonioribeiro/google2fa/tree/8.0.0"
+            },
+            "time": "2020-04-05T10:47:18+00:00"
         },
         {
             "name": "predis/predis",
         },
         {
             "name": "ramsey/uuid",
-            "version": "3.9.3",
+            "version": "3.9.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/ramsey/uuid.git",
-                "reference": "7e1633a6964b48589b142d60542f9ed31bd37a92"
+                "reference": "be2451bef8147b7352a28fb4cddb08adc497ada3"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/ramsey/uuid/zipball/7e1633a6964b48589b142d60542f9ed31bd37a92",
-                "reference": "7e1633a6964b48589b142d60542f9ed31bd37a92",
+                "url": "https://api.github.com/repos/ramsey/uuid/zipball/be2451bef8147b7352a28fb4cddb08adc497ada3",
+                "reference": "be2451bef8147b7352a28fb4cddb08adc497ada3",
                 "shasum": ""
             },
             "require": {
                 "ext-json": "*",
-                "paragonie/random_compat": "^1 | ^2 | 9.99.99",
+                "paragonie/random_compat": "^1 | ^2 | ^9.99.99",
                 "php": "^5.4 | ^7 | ^8",
                 "symfony/polyfill-ctype": "^1.8"
             },
                 "source": "https://github.com/ramsey/uuid",
                 "wiki": "https://github.com/ramsey/uuid/wiki"
             },
-            "time": "2020-02-21T04:36:14+00:00"
+            "funding": [
+                {
+                    "url": "https://github.com/ramsey",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/ramsey/uuid",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2021-08-06T20:32:15+00:00"
         },
         {
             "name": "robrichards/xmlseclibs",
         },
         {
             "name": "symfony/console",
-            "version": "v4.4.29",
+            "version": "v4.4.30",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/console.git",
-                "reference": "8baf0bbcfddfde7d7225ae8e04705cfd1081cd7b"
+                "reference": "a3f7189a0665ee33b50e9e228c46f50f5acbed22"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/console/zipball/8baf0bbcfddfde7d7225ae8e04705cfd1081cd7b",
-                "reference": "8baf0bbcfddfde7d7225ae8e04705cfd1081cd7b",
+                "url": "https://api.github.com/repos/symfony/console/zipball/a3f7189a0665ee33b50e9e228c46f50f5acbed22",
+                "reference": "a3f7189a0665ee33b50e9e228c46f50f5acbed22",
                 "shasum": ""
             },
             "require": {
             "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.29"
+                "source": "https://github.com/symfony/console/tree/v4.4.30"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2021-07-27T19:04:53+00:00"
+            "time": "2021-08-25T19:27:26+00:00"
         },
         {
             "name": "symfony/css-selector",
-            "version": "v4.4.27",
+            "version": "v5.3.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/css-selector.git",
-                "reference": "5194f18bd80d106f11efa8f7cd0fbdcc3af96ce6"
+                "reference": "7fb120adc7f600a59027775b224c13a33530dd90"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/css-selector/zipball/5194f18bd80d106f11efa8f7cd0fbdcc3af96ce6",
-                "reference": "5194f18bd80d106f11efa8f7cd0fbdcc3af96ce6",
+                "url": "https://api.github.com/repos/symfony/css-selector/zipball/7fb120adc7f600a59027775b224c13a33530dd90",
+                "reference": "7fb120adc7f600a59027775b224c13a33530dd90",
                 "shasum": ""
             },
             "require": {
-                "php": ">=7.1.3",
+                "php": ">=7.2.5",
                 "symfony/polyfill-php80": "^1.16"
             },
             "type": "library",
             "description": "Converts CSS selectors to XPath expressions",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/css-selector/tree/v4.4.27"
+                "source": "https://github.com/symfony/css-selector/tree/v5.3.4"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2021-07-21T12:19:41+00:00"
+            "time": "2021-07-21T12:38:00+00:00"
         },
         {
             "name": "symfony/debug",
         },
         {
             "name": "symfony/error-handler",
-            "version": "v4.4.27",
+            "version": "v4.4.30",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/error-handler.git",
-                "reference": "16ac2be1c0f49d6d9eb9d3ce9324bde268717905"
+                "reference": "51f98f7aa99f00f3b1da6bafe934e67ae6ba6dc5"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/error-handler/zipball/16ac2be1c0f49d6d9eb9d3ce9324bde268717905",
-                "reference": "16ac2be1c0f49d6d9eb9d3ce9324bde268717905",
+                "url": "https://api.github.com/repos/symfony/error-handler/zipball/51f98f7aa99f00f3b1da6bafe934e67ae6ba6dc5",
+                "reference": "51f98f7aa99f00f3b1da6bafe934e67ae6ba6dc5",
                 "shasum": ""
             },
             "require": {
             "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.27"
+                "source": "https://github.com/symfony/error-handler/tree/v4.4.30"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2021-07-23T15:41:52+00:00"
+            "time": "2021-08-27T17:42:48+00:00"
         },
         {
             "name": "symfony/event-dispatcher",
-            "version": "v4.4.27",
+            "version": "v4.4.30",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/event-dispatcher.git",
-                "reference": "958a128b184fcf0ba45ec90c0e88554c9327c2e9"
+                "reference": "2fe81680070043c4c80e7cedceb797e34f377bac"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/958a128b184fcf0ba45ec90c0e88554c9327c2e9",
-                "reference": "958a128b184fcf0ba45ec90c0e88554c9327c2e9",
+                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/2fe81680070043c4c80e7cedceb797e34f377bac",
+                "reference": "2fe81680070043c4c80e7cedceb797e34f377bac",
                 "shasum": ""
             },
             "require": {
             "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.27"
+                "source": "https://github.com/symfony/event-dispatcher/tree/v4.4.30"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2021-07-23T15:41:52+00:00"
+            "time": "2021-08-04T20:31:23+00:00"
         },
         {
             "name": "symfony/event-dispatcher-contracts",
         },
         {
             "name": "symfony/finder",
-            "version": "v4.4.27",
+            "version": "v4.4.30",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/finder.git",
-                "reference": "42414d7ac96fc2880a783b872185789dea0d4262"
+                "reference": "70362f1e112280d75b30087c7598b837c1b468b6"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/finder/zipball/42414d7ac96fc2880a783b872185789dea0d4262",
-                "reference": "42414d7ac96fc2880a783b872185789dea0d4262",
+                "url": "https://api.github.com/repos/symfony/finder/zipball/70362f1e112280d75b30087c7598b837c1b468b6",
+                "reference": "70362f1e112280d75b30087c7598b837c1b468b6",
                 "shasum": ""
             },
             "require": {
             "description": "Finds files and directories via an intuitive fluent interface",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/finder/tree/v4.4.27"
+                "source": "https://github.com/symfony/finder/tree/v4.4.30"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2021-07-23T15:41:52+00:00"
+            "time": "2021-08-04T20:31:23+00:00"
         },
         {
             "name": "symfony/http-client-contracts",
         },
         {
             "name": "symfony/http-foundation",
-            "version": "v4.4.29",
+            "version": "v4.4.30",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/http-foundation.git",
-                "reference": "7016057b01f0ed3ec3ba1f31a580b6661667c2e1"
+                "reference": "09b3202651ab23ac8dcf455284a48a3500e56731"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/http-foundation/zipball/7016057b01f0ed3ec3ba1f31a580b6661667c2e1",
-                "reference": "7016057b01f0ed3ec3ba1f31a580b6661667c2e1",
+                "url": "https://api.github.com/repos/symfony/http-foundation/zipball/09b3202651ab23ac8dcf455284a48a3500e56731",
+                "reference": "09b3202651ab23ac8dcf455284a48a3500e56731",
                 "shasum": ""
             },
             "require": {
             "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.29"
+                "source": "https://github.com/symfony/http-foundation/tree/v4.4.30"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2021-07-27T14:32:23+00:00"
+            "time": "2021-08-26T15:51:23+00:00"
         },
         {
             "name": "symfony/http-kernel",
-            "version": "v4.4.29",
+            "version": "v4.4.30",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/http-kernel.git",
-                "reference": "752b170e1ba0dd4104e7fa17c1cef1ec8a7fc506"
+                "reference": "87f7ea4a8a7a30c967e26001de99f12943bf57ae"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/http-kernel/zipball/752b170e1ba0dd4104e7fa17c1cef1ec8a7fc506",
-                "reference": "752b170e1ba0dd4104e7fa17c1cef1ec8a7fc506",
+                "url": "https://api.github.com/repos/symfony/http-kernel/zipball/87f7ea4a8a7a30c967e26001de99f12943bf57ae",
+                "reference": "87f7ea4a8a7a30c967e26001de99f12943bf57ae",
                 "shasum": ""
             },
             "require": {
                 "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/http-foundation": "^4.4.30|^5.3.7",
                 "symfony/polyfill-ctype": "^1.8",
                 "symfony/polyfill-php73": "^1.9",
                 "symfony/polyfill-php80": "^1.16"
             "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.29"
+                "source": "https://github.com/symfony/http-kernel/tree/v4.4.30"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2021-07-29T06:45:05+00:00"
+            "time": "2021-08-30T12:27:20+00:00"
         },
         {
             "name": "symfony/mime",
-            "version": "v5.3.4",
+            "version": "v5.3.7",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/mime.git",
-                "reference": "633e4e8afe9e529e5599d71238849a4218dd497b"
+                "reference": "ae887cb3b044658676129f5e97aeb7e9eb69c2d8"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/mime/zipball/633e4e8afe9e529e5599d71238849a4218dd497b",
-                "reference": "633e4e8afe9e529e5599d71238849a4218dd497b",
+                "url": "https://api.github.com/repos/symfony/mime/zipball/ae887cb3b044658676129f5e97aeb7e9eb69c2d8",
+                "reference": "ae887cb3b044658676129f5e97aeb7e9eb69c2d8",
                 "shasum": ""
             },
             "require": {
                 "mime-type"
             ],
             "support": {
-                "source": "https://github.com/symfony/mime/tree/v5.3.4"
+                "source": "https://github.com/symfony/mime/tree/v5.3.7"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2021-07-21T12:40:44+00:00"
+            "time": "2021-08-20T11:40:01+00:00"
         },
         {
             "name": "symfony/polyfill-ctype",
         },
         {
             "name": "symfony/process",
-            "version": "v4.4.27",
+            "version": "v4.4.30",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/process.git",
-                "reference": "0b7dc5599ac4aa6d7b936c8f7d10abae64f6cf7f"
+                "reference": "13d3161ef63a8ec21eeccaaf9a4d7f784a87a97d"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/process/zipball/0b7dc5599ac4aa6d7b936c8f7d10abae64f6cf7f",
-                "reference": "0b7dc5599ac4aa6d7b936c8f7d10abae64f6cf7f",
+                "url": "https://api.github.com/repos/symfony/process/zipball/13d3161ef63a8ec21eeccaaf9a4d7f784a87a97d",
+                "reference": "13d3161ef63a8ec21eeccaaf9a4d7f784a87a97d",
                 "shasum": ""
             },
             "require": {
             "description": "Executes commands in sub-processes",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/process/tree/v4.4.27"
+                "source": "https://github.com/symfony/process/tree/v4.4.30"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2021-07-23T15:41:52+00:00"
+            "time": "2021-08-04T20:31:23+00:00"
         },
         {
             "name": "symfony/routing",
-            "version": "v4.4.27",
+            "version": "v4.4.30",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/routing.git",
-                "reference": "244609821beece97167fa7ba4eef49d2a31862db"
+                "reference": "9ddf033927ad9f30ba2bfd167a7b342cafa13e8e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/routing/zipball/244609821beece97167fa7ba4eef49d2a31862db",
-                "reference": "244609821beece97167fa7ba4eef49d2a31862db",
+                "url": "https://api.github.com/repos/symfony/routing/zipball/9ddf033927ad9f30ba2bfd167a7b342cafa13e8e",
+                "reference": "9ddf033927ad9f30ba2bfd167a7b342cafa13e8e",
                 "shasum": ""
             },
             "require": {
                 "url"
             ],
             "support": {
-                "source": "https://github.com/symfony/routing/tree/v4.4.27"
+                "source": "https://github.com/symfony/routing/tree/v4.4.30"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2021-07-23T15:41:52+00:00"
+            "time": "2021-08-04T21:41:01+00:00"
         },
         {
             "name": "symfony/service-contracts",
         },
         {
             "name": "symfony/translation",
-            "version": "v4.4.27",
+            "version": "v4.4.30",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/translation.git",
-                "reference": "2e3c0f2bf704d635ba862e7198d72331a62d82ba"
+                "reference": "db0ba1e85280d8ff11e38d53c70f8814d4d740f5"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/translation/zipball/2e3c0f2bf704d635ba862e7198d72331a62d82ba",
-                "reference": "2e3c0f2bf704d635ba862e7198d72331a62d82ba",
+                "url": "https://api.github.com/repos/symfony/translation/zipball/db0ba1e85280d8ff11e38d53c70f8814d4d740f5",
+                "reference": "db0ba1e85280d8ff11e38d53c70f8814d4d740f5",
                 "shasum": ""
             },
             "require": {
             "description": "Provides tools to internationalize your application",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/translation/tree/v4.4.27"
+                "source": "https://github.com/symfony/translation/tree/v4.4.30"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2021-07-21T13:12:00+00:00"
+            "time": "2021-08-26T05:57:13+00:00"
         },
         {
             "name": "symfony/translation-contracts",
         },
         {
             "name": "symfony/var-dumper",
-            "version": "v4.4.27",
+            "version": "v4.4.30",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/var-dumper.git",
-                "reference": "391d6d0e7a06ab54eb7c38fab29b8d174471b3ba"
+                "reference": "7f65c44c2ce80d3a0fcdb6385ee0ad535e45660c"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/var-dumper/zipball/391d6d0e7a06ab54eb7c38fab29b8d174471b3ba",
-                "reference": "391d6d0e7a06ab54eb7c38fab29b8d174471b3ba",
+                "url": "https://api.github.com/repos/symfony/var-dumper/zipball/7f65c44c2ce80d3a0fcdb6385ee0ad535e45660c",
+                "reference": "7f65c44c2ce80d3a0fcdb6385ee0ad535e45660c",
                 "shasum": ""
             },
             "require": {
                 "dump"
             ],
             "support": {
-                "source": "https://github.com/symfony/var-dumper/tree/v4.4.27"
+                "source": "https://github.com/symfony/var-dumper/tree/v4.4.30"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2021-07-23T15:41:52+00:00"
+            "time": "2021-08-04T20:31:23+00:00"
         },
         {
             "name": "tijsverkoyen/css-to-inline-styles",
         },
         {
             "name": "composer/composer",
-            "version": "2.1.5",
+            "version": "2.1.8",
             "source": {
                 "type": "git",
                 "url": "https://github.com/composer/composer.git",
-                "reference": "ac679902e9f66b85a8f9d8c1c88180f609a8745d"
+                "reference": "24d38e9686092de05214cafa187dc282a5d89497"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/composer/composer/zipball/ac679902e9f66b85a8f9d8c1c88180f609a8745d",
-                "reference": "ac679902e9f66b85a8f9d8c1c88180f609a8745d",
+                "url": "https://api.github.com/repos/composer/composer/zipball/24d38e9686092de05214cafa187dc282a5d89497",
+                "reference": "24d38e9686092de05214cafa187dc282a5d89497",
                 "shasum": ""
             },
             "require": {
                 "package"
             ],
             "support": {
-                "irc": "irc://irc.freenode.org/composer",
+                "irc": "ircs://irc.libera.chat:6697/composer",
                 "issues": "https://github.com/composer/composer/issues",
-                "source": "https://github.com/composer/composer/tree/2.1.5"
+                "source": "https://github.com/composer/composer/tree/2.1.8"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2021-07-23T08:35:47+00:00"
+            "time": "2021-09-15T11:55:15+00:00"
         },
         {
             "name": "composer/metadata-minifier",
         },
         {
             "name": "fakerphp/faker",
-            "version": "v1.15.0",
+            "version": "v1.16.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/FakerPHP/Faker.git",
-                "reference": "89c6201c74db25fa759ff16e78a4d8f32547770e"
+                "reference": "271d384d216e5e5c468a6b28feedf95d49f83b35"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/89c6201c74db25fa759ff16e78a4d8f32547770e",
-                "reference": "89c6201c74db25fa759ff16e78a4d8f32547770e",
+                "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/271d384d216e5e5c468a6b28feedf95d49f83b35",
+                "reference": "271d384d216e5e5c468a6b28feedf95d49f83b35",
                 "shasum": ""
             },
             "require": {
                 "php": "^7.1 || ^8.0",
-                "psr/container": "^1.0",
+                "psr/container": "^1.0 || ^2.0",
                 "symfony/deprecation-contracts": "^2.2"
             },
             "conflict": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-main": "v1.15-dev"
+                    "dev-main": "v1.16-dev"
                 }
             },
             "autoload": {
             ],
             "support": {
                 "issues": "https://github.com/FakerPHP/Faker/issues",
-                "source": "https://github.com/FakerPHP/Faker/tree/v1.15.0"
+                "source": "https://github.com/FakerPHP/Faker/tree/v1.16.0"
             },
-            "time": "2021-07-06T20:39:40+00:00"
+            "time": "2021-09-06T14:53:37+00:00"
         },
         {
             "name": "hamcrest/hamcrest-php",
             },
             "time": "2021-07-22T09:24:00+00:00"
         },
-        {
-            "name": "laravel/browser-kit-testing",
-            "version": "v5.2.0",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/laravel/browser-kit-testing.git",
-                "reference": "fa0efb279c009e2a276f934f8aff946caf66edc7"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/laravel/browser-kit-testing/zipball/fa0efb279c009e2a276f934f8aff946caf66edc7",
-                "reference": "fa0efb279c009e2a276f934f8aff946caf66edc7",
-                "shasum": ""
-            },
-            "require": {
-                "ext-dom": "*",
-                "ext-json": "*",
-                "illuminate/contracts": "~5.7.0|~5.8.0|^6.0",
-                "illuminate/database": "~5.7.0|~5.8.0|^6.0",
-                "illuminate/http": "~5.7.0|~5.8.0|^6.0",
-                "illuminate/support": "~5.7.0|~5.8.0|^6.0",
-                "mockery/mockery": "^1.0",
-                "php": "^7.1.3|^8.0",
-                "phpunit/phpunit": "^7.5|^8.0|^9.3",
-                "symfony/console": "^4.2",
-                "symfony/css-selector": "^4.2",
-                "symfony/dom-crawler": "^4.2",
-                "symfony/http-foundation": "^4.2",
-                "symfony/http-kernel": "^4.2"
-            },
-            "require-dev": {
-                "laravel/framework": "~5.7.0|~5.8.0|^6.0"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "5.x-dev"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "Laravel\\BrowserKitTesting\\": "src/"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Taylor Otwell",
-                    "email": "[email protected]"
-                }
-            ],
-            "description": "Provides backwards compatibility for BrowserKit testing in the latest Laravel release.",
-            "keywords": [
-                "laravel",
-                "testing"
-            ],
-            "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.17.1",
         },
         {
             "name": "mockery/mockery",
-            "version": "1.4.3",
+            "version": "1.4.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/mockery/mockery.git",
-                "reference": "d1339f64479af1bee0e82a0413813fe5345a54ea"
+                "reference": "e01123a0e847d52d186c5eb4b9bf58b0c6d00346"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/mockery/mockery/zipball/d1339f64479af1bee0e82a0413813fe5345a54ea",
-                "reference": "d1339f64479af1bee0e82a0413813fe5345a54ea",
+                "url": "https://api.github.com/repos/mockery/mockery/zipball/e01123a0e847d52d186c5eb4b9bf58b0c6d00346",
+                "reference": "e01123a0e847d52d186c5eb4b9bf58b0c6d00346",
                 "shasum": ""
             },
             "require": {
             ],
             "support": {
                 "issues": "https://github.com/mockery/mockery/issues",
-                "source": "https://github.com/mockery/mockery/tree/1.4.3"
+                "source": "https://github.com/mockery/mockery/tree/1.4.4"
             },
-            "time": "2021-02-24T09:51:49+00:00"
+            "time": "2021-09-13T15:28:59+00:00"
         },
         {
             "name": "myclabs/deep-copy",
         },
         {
             "name": "phpdocumentor/type-resolver",
-            "version": "1.4.0",
+            "version": "1.5.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpDocumentor/TypeResolver.git",
-                "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0"
+                "reference": "30f38bffc6f24293dadd1823936372dfa9e86e2f"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0",
-                "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0",
+                "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/30f38bffc6f24293dadd1823936372dfa9e86e2f",
+                "reference": "30f38bffc6f24293dadd1823936372dfa9e86e2f",
                 "shasum": ""
             },
             "require": {
                 "phpdocumentor/reflection-common": "^2.0"
             },
             "require-dev": {
-                "ext-tokenizer": "*"
+                "ext-tokenizer": "*",
+                "psalm/phar": "^4.8"
             },
             "type": "library",
             "extra": {
             "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names",
             "support": {
                 "issues": "https://github.com/phpDocumentor/TypeResolver/issues",
-                "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.4.0"
+                "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.5.0"
             },
-            "time": "2020-09-17T18:55:26+00:00"
+            "time": "2021-09-17T15:28:14+00:00"
         },
         {
             "name": "phpspec/prophecy",
-            "version": "1.13.0",
+            "version": "1.14.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpspec/prophecy.git",
-                "reference": "be1996ed8adc35c3fd795488a653f4b518be70ea"
+                "reference": "d86dfc2e2a3cd366cee475e52c6bb3bbc371aa0e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpspec/prophecy/zipball/be1996ed8adc35c3fd795488a653f4b518be70ea",
-                "reference": "be1996ed8adc35c3fd795488a653f4b518be70ea",
+                "url": "https://api.github.com/repos/phpspec/prophecy/zipball/d86dfc2e2a3cd366cee475e52c6bb3bbc371aa0e",
+                "reference": "d86dfc2e2a3cd366cee475e52c6bb3bbc371aa0e",
                 "shasum": ""
             },
             "require": {
                 "doctrine/instantiator": "^1.2",
-                "php": "^7.2 || ~8.0, <8.1",
+                "php": "^7.2 || ~8.0, <8.2",
                 "phpdocumentor/reflection-docblock": "^5.2",
                 "sebastian/comparator": "^3.0 || ^4.0",
                 "sebastian/recursion-context": "^3.0 || ^4.0"
             },
             "require-dev": {
-                "phpspec/phpspec": "^6.0",
+                "phpspec/phpspec": "^6.0 || ^7.0",
                 "phpunit/phpunit": "^8.0 || ^9.0"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.11.x-dev"
+                    "dev-master": "1.x-dev"
                 }
             },
             "autoload": {
             ],
             "support": {
                 "issues": "https://github.com/phpspec/prophecy/issues",
-                "source": "https://github.com/phpspec/prophecy/tree/1.13.0"
+                "source": "https://github.com/phpspec/prophecy/tree/1.14.0"
             },
-            "time": "2021-03-17T13:42:18+00:00"
+            "time": "2021-09-10T09:02:12+00:00"
         },
         {
             "name": "phpunit/php-code-coverage",
-            "version": "9.2.6",
+            "version": "9.2.7",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
-                "reference": "f6293e1b30a2354e8428e004689671b83871edde"
+                "reference": "d4c798ed8d51506800b441f7a13ecb0f76f12218"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/f6293e1b30a2354e8428e004689671b83871edde",
-                "reference": "f6293e1b30a2354e8428e004689671b83871edde",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/d4c798ed8d51506800b441f7a13ecb0f76f12218",
+                "reference": "d4c798ed8d51506800b441f7a13ecb0f76f12218",
                 "shasum": ""
             },
             "require": {
                 "ext-dom": "*",
                 "ext-libxml": "*",
                 "ext-xmlwriter": "*",
-                "nikic/php-parser": "^4.10.2",
+                "nikic/php-parser": "^4.12.0",
                 "php": ">=7.3",
                 "phpunit/php-file-iterator": "^3.0.3",
                 "phpunit/php-text-template": "^2.0.2",
             ],
             "support": {
                 "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
-                "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.6"
+                "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.7"
             },
             "funding": [
                 {
                     "type": "github"
                 }
             ],
-            "time": "2021-03-28T07:26:59+00:00"
+            "time": "2021-09-17T05:39:03+00:00"
         },
         {
             "name": "phpunit/php-file-iterator",
         },
         {
             "name": "phpunit/phpunit",
-            "version": "9.5.8",
+            "version": "9.5.9",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/phpunit.git",
-                "reference": "191768ccd5c85513b4068bdbe99bb6390c7d54fb"
+                "reference": "ea8c2dfb1065eb35a79b3681eee6e6fb0a6f273b"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/191768ccd5c85513b4068bdbe99bb6390c7d54fb",
-                "reference": "191768ccd5c85513b4068bdbe99bb6390c7d54fb",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/ea8c2dfb1065eb35a79b3681eee6e6fb0a6f273b",
+                "reference": "ea8c2dfb1065eb35a79b3681eee6e6fb0a6f273b",
                 "shasum": ""
             },
             "require": {
             ],
             "support": {
                 "issues": "https://github.com/sebastianbergmann/phpunit/issues",
-                "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.8"
+                "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.9"
             },
             "funding": [
                 {
                     "type": "github"
                 }
             ],
-            "time": "2021-07-31T15:17:34+00:00"
+            "time": "2021-08-31T06:47:40+00:00"
         },
         {
             "name": "react/promise",
         },
         {
             "name": "seld/phar-utils",
-            "version": "1.1.1",
+            "version": "1.1.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/Seldaek/phar-utils.git",
-                "reference": "8674b1d84ffb47cc59a101f5d5a3b61e87d23796"
+                "reference": "749042a2315705d2dfbbc59234dd9ceb22bf3ff0"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/Seldaek/phar-utils/zipball/8674b1d84ffb47cc59a101f5d5a3b61e87d23796",
-                "reference": "8674b1d84ffb47cc59a101f5d5a3b61e87d23796",
+                "url": "https://api.github.com/repos/Seldaek/phar-utils/zipball/749042a2315705d2dfbbc59234dd9ceb22bf3ff0",
+                "reference": "749042a2315705d2dfbbc59234dd9ceb22bf3ff0",
                 "shasum": ""
             },
             "require": {
             ],
             "support": {
                 "issues": "https://github.com/Seldaek/phar-utils/issues",
-                "source": "https://github.com/Seldaek/phar-utils/tree/master"
+                "source": "https://github.com/Seldaek/phar-utils/tree/1.1.2"
             },
-            "time": "2020-07-07T18:42:57+00:00"
+            "time": "2021-08-19T21:01:38+00:00"
         },
         {
             "name": "symfony/dom-crawler",
-            "version": "v4.4.27",
+            "version": "v5.3.7",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/dom-crawler.git",
-                "reference": "86aa075c9e0b13ac7db8d73d1f9d8b656143881a"
+                "reference": "c7eef3a60ccfdd8eafe07f81652e769ac9c7146c"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/86aa075c9e0b13ac7db8d73d1f9d8b656143881a",
-                "reference": "86aa075c9e0b13ac7db8d73d1f9d8b656143881a",
+                "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/c7eef3a60ccfdd8eafe07f81652e769ac9c7146c",
+                "reference": "c7eef3a60ccfdd8eafe07f81652e769ac9c7146c",
                 "shasum": ""
             },
             "require": {
-                "php": ">=7.1.3",
+                "php": ">=7.2.5",
+                "symfony/deprecation-contracts": "^2.1",
                 "symfony/polyfill-ctype": "~1.8",
                 "symfony/polyfill-mbstring": "~1.0",
                 "symfony/polyfill-php80": "^1.16"
             },
             "require-dev": {
                 "masterminds/html5": "^2.6",
-                "symfony/css-selector": "^3.4|^4.0|^5.0"
+                "symfony/css-selector": "^4.4|^5.0"
             },
             "suggest": {
                 "symfony/css-selector": ""
             "description": "Eases DOM navigation for HTML and XML documents",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/dom-crawler/tree/v4.4.27"
+                "source": "https://github.com/symfony/dom-crawler/tree/v5.3.7"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2021-07-23T15:41:52+00:00"
+            "time": "2021-08-29T19:32:13+00:00"
         },
         {
             "name": "symfony/filesystem",
     "platform-overrides": {
         "php": "7.3.0"
     },
-    "plugin-api-version": "2.0.0"
+    "plugin-api-version": "2.1.0"
 }
index 5b43c7d549de1265fd5fee0eea91f4fd5b499ebd..8c3d9124c75e4067b9041288b3eb11ac856a32c4 100644 (file)
@@ -2,6 +2,7 @@
 
 use Illuminate\Database\Migrations\Migration;
 use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Str;
 
 class CreateJointPermissionsTable extends Migration
 {
@@ -53,7 +54,7 @@ class CreateJointPermissionsTable extends Migration
 
         // Ensure unique name
         while (DB::table('roles')->where('name', '=', $publicRoleData['display_name'])->count() > 0) {
-            $publicRoleData['display_name'] = $publicRoleData['display_name'] . str_random(2);
+            $publicRoleData['display_name'] = $publicRoleData['display_name'] . Str::random(2);
         }
         $publicRoleId = DB::table('roles')->insertGetId($publicRoleData);
 
diff --git a/database/migrations/2021_06_30_173111_create_mfa_values_table.php b/database/migrations/2021_06_30_173111_create_mfa_values_table.php
new file mode 100644 (file)
index 0000000..937fd31
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class CreateMfaValuesTable extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::create('mfa_values', function (Blueprint $table) {
+            $table->increments('id');
+            $table->integer('user_id')->index();
+            $table->string('method', 20)->index();
+            $table->text('value');
+            $table->timestamps();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::dropIfExists('mfa_values');
+    }
+}
diff --git a/database/migrations/2021_07_03_085038_add_mfa_enforced_to_roles_table.php b/database/migrations/2021_07_03_085038_add_mfa_enforced_to_roles_table.php
new file mode 100644 (file)
index 0000000..c14d47e
--- /dev/null
@@ -0,0 +1,32 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class AddMfaEnforcedToRolesTable extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::table('roles', function (Blueprint $table) {
+            $table->boolean('mfa_enforced');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::table('roles', function (Blueprint $table) {
+            $table->dropColumn('mfa_enforced');
+        });
+    }
+}
diff --git a/database/migrations/2021_08_28_161743_add_export_role_permission.php b/database/migrations/2021_08_28_161743_add_export_role_permission.php
new file mode 100644 (file)
index 0000000..1da6076
--- /dev/null
@@ -0,0 +1,49 @@
+<?php
+
+use Carbon\Carbon;
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Support\Facades\DB;
+
+class AddExportRolePermission extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        // Create new templates-manage permission and assign to admin role
+        $roles = DB::table('roles')->get('id');
+        $permissionId = DB::table('role_permissions')->insertGetId([
+            'name'         => 'content-export',
+            'display_name' => 'Export Content',
+            'created_at'   => Carbon::now()->toDateTimeString(),
+            'updated_at'   => Carbon::now()->toDateTimeString(),
+        ]);
+
+        $permissionRoles = $roles->map(function ($role) use ($permissionId) {
+            return [
+                'role_id'       => $role->id,
+                'permission_id' => $permissionId,
+            ];
+        })->values()->toArray();
+
+        DB::table('permission_role')->insert($permissionRoles);
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        // Remove content-export permission
+        $contentExportPermission = DB::table('role_permissions')
+            ->where('name', '=', 'content-export')->first();
+
+        DB::table('permission_role')->where('permission_id', '=', $contentExportPermission->id)->delete();
+        DB::table('role_permissions')->where('id', '=', 'content-export')->delete();
+    }
+}
diff --git a/database/migrations/2021_09_26_044614_add_activities_ip_column.php b/database/migrations/2021_09_26_044614_add_activities_ip_column.php
new file mode 100644 (file)
index 0000000..68391b1
--- /dev/null
@@ -0,0 +1,32 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class AddActivitiesIpColumn extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::table('activities', function (Blueprint $table) {
+            $table->string('ip', 45)->after('user_id');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::table('activities', function (Blueprint $table) {
+            $table->dropColumn('ip');
+        });
+    }
+}
index 7d9318363a51f5e8da1d8d811859efa24473416a..97d4d82871afc800db0b9508962ad376956d96f9 100644 (file)
@@ -6,19 +6,19 @@
     "": {
       "dependencies": {
         "clipboard": "^2.0.8",
-        "codemirror": "^5.61.1",
+        "codemirror": "^5.62.3",
         "dropzone": "^5.9.2",
-        "markdown-it": "^12.0.6",
+        "markdown-it": "^12.2.0",
         "markdown-it-task-lists": "^2.1.1",
-        "sortablejs": "^1.13.0"
+        "sortablejs": "^1.14.0"
       },
       "devDependencies": {
-        "chokidar-cli": "^2.1.0",
-        "esbuild": "0.12.8",
+        "chokidar-cli": "^3.0.0",
+        "esbuild": "0.12.22",
         "livereload": "^0.9.3",
         "npm-run-all": "^4.1.5",
         "punycode": "^2.1.1",
-        "sass": "^1.34.1"
+        "sass": "^1.38.0"
       }
     },
     "node_modules/ansi-regex": {
@@ -43,9 +43,9 @@
       }
     },
     "node_modules/anymatch": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz",
-      "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==",
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
+      "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
       "dev": true,
       "dependencies": {
         "normalize-path": "^3.0.0",
       }
     },
     "node_modules/chokidar": {
-      "version": "3.4.2",
-      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.2.tgz",
-      "integrity": "sha512-IZHaDeBeI+sZJRX7lGcXsdzgvZqKv6sECqsbErJA4mHWfpRrD8B97kSFN4cQz6nGBGiuFia1MKR4d6c1o8Cv7A==",
+      "version": "3.5.2",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz",
+      "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==",
       "dev": true,
       "dependencies": {
-        "anymatch": "~3.1.1",
+        "anymatch": "~3.1.2",
         "braces": "~3.0.2",
-        "glob-parent": "~5.1.0",
+        "glob-parent": "~5.1.2",
         "is-binary-path": "~2.1.0",
         "is-glob": "~4.0.1",
         "normalize-path": "~3.0.0",
-        "readdirp": "~3.4.0"
+        "readdirp": "~3.6.0"
       },
       "engines": {
         "node": ">= 8.10.0"
       },
       "optionalDependencies": {
-        "fsevents": "~2.1.2"
+        "fsevents": "~2.3.2"
       }
     },
     "node_modules/chokidar-cli": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/chokidar-cli/-/chokidar-cli-2.1.0.tgz",
-      "integrity": "sha512-6n21AVpW6ywuEPoxJcLXMA2p4T+SLjWsXKny/9yTWFz0kKxESI3eUylpeV97LylING/27T/RVTY0f2/0QaWq9Q==",
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/chokidar-cli/-/chokidar-cli-3.0.0.tgz",
+      "integrity": "sha512-xVW+Qeh7z15uZRxHOkP93Ux8A0xbPzwK4GaqD8dQOYc34TlkqUhVSS59fK36DOp5WdJlrRzlYSy02Ht99FjZqQ==",
       "dev": true,
       "dependencies": {
-        "chokidar": "^3.2.3",
+        "chokidar": "^3.5.2",
         "lodash.debounce": "^4.0.8",
         "lodash.throttle": "^4.1.1",
         "yargs": "^13.3.0"
       }
     },
     "node_modules/codemirror": {
-      "version": "5.61.1",
-      "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.61.1.tgz",
-      "integrity": "sha512-+D1NZjAucuzE93vJGbAaXzvoBHwp9nJZWWWF9utjv25+5AZUiah6CIlfb4ikG4MoDsFsCG8niiJH5++OO2LgIQ=="
+      "version": "5.62.3",
+      "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.62.3.tgz",
+      "integrity": "sha512-zZAyOfN8TU67ngqrxhOgtkSAGV9jSpN1snbl8elPtnh9Z5A11daR405+dhLzLnuXrwX0WCShWlybxPN3QC/9Pg=="
     },
     "node_modules/color-convert": {
       "version": "1.9.3",
       }
     },
     "node_modules/esbuild": {
-      "version": "0.12.8",
-      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.12.8.tgz",
-      "integrity": "sha512-sx/LwlP/SWTGsd9G4RlOPrXnIihAJ2xwBUmzoqe2nWwbXORMQWtAGNJNYLBJJqa3e9PWvVzxdrtyFZJcr7D87g==",
+      "version": "0.12.22",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.12.22.tgz",
+      "integrity": "sha512-yWCr9RoFehpqoe/+MwZXJpYOEIt7KOEvNnjIeMZpMSyQt+KCBASM3y7yViiN5dJRphf1wGdUz1+M4rTtWd/ulA==",
       "dev": true,
       "hasInstallScript": true,
       "bin": {
       }
     },
     "node_modules/fsevents": {
-      "version": "2.1.3",
-      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz",
-      "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==",
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+      "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
       "dev": true,
+      "hasInstallScript": true,
       "optional": true,
       "os": [
         "darwin"
       "integrity": "sha512-w677WnINxFkuixAoUEXOStewzLYGI76XVag+0JWMMEyjJQKs0ibWZMxkTlB96Lm3EjZ7IeOxVziBEbtxVQqQZA==",
       "dev": true
     },
-    "node_modules/livereload/node_modules/chokidar": {
-      "version": "3.5.1",
-      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz",
-      "integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==",
-      "dev": true,
-      "dependencies": {
-        "anymatch": "~3.1.1",
-        "braces": "~3.0.2",
-        "glob-parent": "~5.1.0",
-        "is-binary-path": "~2.1.0",
-        "is-glob": "~4.0.1",
-        "normalize-path": "~3.0.0",
-        "readdirp": "~3.5.0"
-      },
-      "engines": {
-        "node": ">= 8.10.0"
-      },
-      "optionalDependencies": {
-        "fsevents": "~2.3.1"
-      }
-    },
-    "node_modules/livereload/node_modules/fsevents": {
-      "version": "2.3.2",
-      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
-      "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
-      "dev": true,
-      "optional": true,
-      "os": [
-        "darwin"
-      ],
-      "engines": {
-        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
-      }
-    },
-    "node_modules/livereload/node_modules/readdirp": {
-      "version": "3.5.0",
-      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz",
-      "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==",
-      "dev": true,
-      "dependencies": {
-        "picomatch": "^2.2.1"
-      },
-      "engines": {
-        "node": ">=8.10.0"
-      }
-    },
     "node_modules/load-json-file": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz",
       "dev": true
     },
     "node_modules/markdown-it": {
-      "version": "12.0.6",
-      "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.0.6.tgz",
-      "integrity": "sha512-qv3sVLl4lMT96LLtR7xeRJX11OUFjsaD5oVat2/SNBIb21bJXwal2+SklcRbTwGwqWpWH/HRtYavOoJE+seL8w==",
+      "version": "12.2.0",
+      "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.2.0.tgz",
+      "integrity": "sha512-Wjws+uCrVQRqOoJvze4HCqkKl1AsSh95iFAeQDwnyfxM09divCBSXlDR1uTvyUP3Grzpn4Ru8GeCxYPM8vkCQg==",
       "dependencies": {
         "argparse": "^2.0.1",
         "entities": "~2.1.0",
       }
     },
     "node_modules/path-parse": {
-      "version": "1.0.6",
-      "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
-      "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+      "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
       "dev": true
     },
     "node_modules/path-type": {
       }
     },
     "node_modules/readdirp": {
-      "version": "3.4.0",
-      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz",
-      "integrity": "sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==",
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+      "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
       "dev": true,
       "dependencies": {
         "picomatch": "^2.2.1"
       }
     },
     "node_modules/sass": {
-      "version": "1.34.1",
-      "resolved": "https://registry.npmjs.org/sass/-/sass-1.34.1.tgz",
-      "integrity": "sha512-scLA7EIZM+MmYlej6sdVr0HRbZX5caX5ofDT9asWnUJj21oqgsC+1LuNfm0eg+vM0fCTZHhwImTiCU0sx9h9CQ==",
+      "version": "1.38.0",
+      "resolved": "https://registry.npmjs.org/sass/-/sass-1.38.0.tgz",
+      "integrity": "sha512-WBccZeMigAGKoI+NgD7Adh0ab1HUq+6BmyBUEaGxtErbUtWUevEbdgo5EZiJQofLUGcKtlNaO2IdN73AHEua5g==",
       "dev": true,
       "dependencies": {
         "chokidar": ">=3.0.0 <4.0.0"
       "dev": true
     },
     "node_modules/sortablejs": {
-      "version": "1.13.0",
-      "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.13.0.tgz",
-      "integrity": "sha512-RBJirPY0spWCrU5yCmWM1eFs/XgX2J5c6b275/YyxFRgnzPhKl/TDeU2hNR8Dt7ITq66NRPM4UlOt+e5O4CFHg=="
+      "version": "1.14.0",
+      "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.14.0.tgz",
+      "integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w=="
     },
     "node_modules/spdx-correct": {
       "version": "3.1.1",
       }
     },
     "anymatch": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz",
-      "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==",
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
+      "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
       "dev": true,
       "requires": {
         "normalize-path": "^3.0.0",
       }
     },
     "chokidar": {
-      "version": "3.4.2",
-      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.2.tgz",
-      "integrity": "sha512-IZHaDeBeI+sZJRX7lGcXsdzgvZqKv6sECqsbErJA4mHWfpRrD8B97kSFN4cQz6nGBGiuFia1MKR4d6c1o8Cv7A==",
+      "version": "3.5.2",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz",
+      "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==",
       "dev": true,
       "requires": {
-        "anymatch": "~3.1.1",
+        "anymatch": "~3.1.2",
         "braces": "~3.0.2",
-        "fsevents": "~2.1.2",
-        "glob-parent": "~5.1.0",
+        "fsevents": "~2.3.2",
+        "glob-parent": "~5.1.2",
         "is-binary-path": "~2.1.0",
         "is-glob": "~4.0.1",
         "normalize-path": "~3.0.0",
-        "readdirp": "~3.4.0"
+        "readdirp": "~3.6.0"
       }
     },
     "chokidar-cli": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/chokidar-cli/-/chokidar-cli-2.1.0.tgz",
-      "integrity": "sha512-6n21AVpW6ywuEPoxJcLXMA2p4T+SLjWsXKny/9yTWFz0kKxESI3eUylpeV97LylING/27T/RVTY0f2/0QaWq9Q==",
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/chokidar-cli/-/chokidar-cli-3.0.0.tgz",
+      "integrity": "sha512-xVW+Qeh7z15uZRxHOkP93Ux8A0xbPzwK4GaqD8dQOYc34TlkqUhVSS59fK36DOp5WdJlrRzlYSy02Ht99FjZqQ==",
       "dev": true,
       "requires": {
-        "chokidar": "^3.2.3",
+        "chokidar": "^3.5.2",
         "lodash.debounce": "^4.0.8",
         "lodash.throttle": "^4.1.1",
         "yargs": "^13.3.0"
       }
     },
     "codemirror": {
-      "version": "5.61.1",
-      "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.61.1.tgz",
-      "integrity": "sha512-+D1NZjAucuzE93vJGbAaXzvoBHwp9nJZWWWF9utjv25+5AZUiah6CIlfb4ikG4MoDsFsCG8niiJH5++OO2LgIQ=="
+      "version": "5.62.3",
+      "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.62.3.tgz",
+      "integrity": "sha512-zZAyOfN8TU67ngqrxhOgtkSAGV9jSpN1snbl8elPtnh9Z5A11daR405+dhLzLnuXrwX0WCShWlybxPN3QC/9Pg=="
     },
     "color-convert": {
       "version": "1.9.3",
       }
     },
     "esbuild": {
-      "version": "0.12.8",
-      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.12.8.tgz",
-      "integrity": "sha512-sx/LwlP/SWTGsd9G4RlOPrXnIihAJ2xwBUmzoqe2nWwbXORMQWtAGNJNYLBJJqa3e9PWvVzxdrtyFZJcr7D87g==",
+      "version": "0.12.22",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.12.22.tgz",
+      "integrity": "sha512-yWCr9RoFehpqoe/+MwZXJpYOEIt7KOEvNnjIeMZpMSyQt+KCBASM3y7yViiN5dJRphf1wGdUz1+M4rTtWd/ulA==",
       "dev": true
     },
     "escape-string-regexp": {
       }
     },
     "fsevents": {
-      "version": "2.1.3",
-      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz",
-      "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==",
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+      "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
       "dev": true,
       "optional": true
     },
         "livereload-js": "^3.3.1",
         "opts": ">= 1.2.0",
         "ws": "^7.4.3"
-      },
-      "dependencies": {
-        "chokidar": {
-          "version": "3.5.1",
-          "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz",
-          "integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==",
-          "dev": true,
-          "requires": {
-            "anymatch": "~3.1.1",
-            "braces": "~3.0.2",
-            "fsevents": "~2.3.1",
-            "glob-parent": "~5.1.0",
-            "is-binary-path": "~2.1.0",
-            "is-glob": "~4.0.1",
-            "normalize-path": "~3.0.0",
-            "readdirp": "~3.5.0"
-          }
-        },
-        "fsevents": {
-          "version": "2.3.2",
-          "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
-          "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
-          "dev": true,
-          "optional": true
-        },
-        "readdirp": {
-          "version": "3.5.0",
-          "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz",
-          "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==",
-          "dev": true,
-          "requires": {
-            "picomatch": "^2.2.1"
-          }
-        }
       }
     },
     "livereload-js": {
       "dev": true
     },
     "markdown-it": {
-      "version": "12.0.6",
-      "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.0.6.tgz",
-      "integrity": "sha512-qv3sVLl4lMT96LLtR7xeRJX11OUFjsaD5oVat2/SNBIb21bJXwal2+SklcRbTwGwqWpWH/HRtYavOoJE+seL8w==",
+      "version": "12.2.0",
+      "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.2.0.tgz",
+      "integrity": "sha512-Wjws+uCrVQRqOoJvze4HCqkKl1AsSh95iFAeQDwnyfxM09divCBSXlDR1uTvyUP3Grzpn4Ru8GeCxYPM8vkCQg==",
       "requires": {
         "argparse": "^2.0.1",
         "entities": "~2.1.0",
       "dev": true
     },
     "path-parse": {
-      "version": "1.0.6",
-      "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
-      "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+      "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
       "dev": true
     },
     "path-type": {
       }
     },
     "readdirp": {
-      "version": "3.4.0",
-      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz",
-      "integrity": "sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==",
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+      "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
       "dev": true,
       "requires": {
         "picomatch": "^2.2.1"
       }
     },
     "sass": {
-      "version": "1.34.1",
-      "resolved": "https://registry.npmjs.org/sass/-/sass-1.34.1.tgz",
-      "integrity": "sha512-scLA7EIZM+MmYlej6sdVr0HRbZX5caX5ofDT9asWnUJj21oqgsC+1LuNfm0eg+vM0fCTZHhwImTiCU0sx9h9CQ==",
+      "version": "1.38.0",
+      "resolved": "https://registry.npmjs.org/sass/-/sass-1.38.0.tgz",
+      "integrity": "sha512-WBccZeMigAGKoI+NgD7Adh0ab1HUq+6BmyBUEaGxtErbUtWUevEbdgo5EZiJQofLUGcKtlNaO2IdN73AHEua5g==",
       "dev": true,
       "requires": {
         "chokidar": ">=3.0.0 <4.0.0"
       "dev": true
     },
     "sortablejs": {
-      "version": "1.13.0",
-      "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.13.0.tgz",
-      "integrity": "sha512-RBJirPY0spWCrU5yCmWM1eFs/XgX2J5c6b275/YyxFRgnzPhKl/TDeU2hNR8Dt7ITq66NRPM4UlOt+e5O4CFHg=="
+      "version": "1.14.0",
+      "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.14.0.tgz",
+      "integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w=="
     },
     "spdx-correct": {
       "version": "3.1.1",
index f4b7bf5dc6f0358a3fc895e62999c1ee7aa08ab0..d2740cd815fee3fd802b99d7a268712597640562 100644 (file)
     "permissions": "chown -R $USER:$USER bootstrap/cache storage public/uploads"
   },
   "devDependencies": {
-    "chokidar-cli": "^2.1.0",
-    "esbuild": "0.12.8",
+    "chokidar-cli": "^3.0.0",
+    "esbuild": "0.12.22",
     "livereload": "^0.9.3",
     "npm-run-all": "^4.1.5",
     "punycode": "^2.1.1",
-    "sass": "^1.34.1"
+    "sass": "^1.38.0"
   },
   "dependencies": {
     "clipboard": "^2.0.8",
-    "codemirror": "^5.61.1",
+    "codemirror": "^5.62.3",
     "dropzone": "^5.9.2",
-    "markdown-it": "^12.0.6",
+    "markdown-it": "^12.2.0",
     "markdown-it-task-lists": "^2.1.1",
-    "sortablejs": "^1.13.0"
+    "sortablejs": "^1.14.0"
   }
 }
index 75c89ec335fb8cb1e7e3b42d5a483edf0b77dd25..7e0da05d42fb33a8a401af585609c53ee1af6659 100644 (file)
@@ -37,6 +37,7 @@
     <server name="LOG_CHANNEL" value="single"/>
     <server name="AUTH_METHOD" value="standard"/>
     <server name="DISABLE_EXTERNAL_SERVICES" value="true"/>
+    <server name="ALLOW_UNTRUSTED_SERVER_FETCHING" value="false"/>
     <server name="AVATAR_URL" value=""/>
     <server name="LDAP_START_TLS" value="false"/>
     <server name="LDAP_VERSION" value="3"/>
index 1b8c6606192bf3b21ec0898ee6e3fdbbc930c56b..cb17a1aae478819e7c92ad1b21df36d2a0ae6183 100644 (file)
--- a/readme.md
+++ b/readme.md
@@ -3,9 +3,10 @@
 [![GitHub release](https://img.shields.io/github/release/BookStackApp/BookStack.svg)](https://github.com/BookStackApp/BookStack/releases/latest)
 [![license](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/BookStackApp/BookStack/blob/master/LICENSE)
 [![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)
 [![Repo Stats](https://img.shields.io/static/v1?label=GitHub+project&message=stats&color=f27e3f)](https://gh-stats.bookstackapp.com/)
+[![Build Status](https://github.com/BookStackApp/BookStack/workflows/phpunit/badge.svg)](https://github.com/BookStackApp/BookStack/actions)
+[![StyleCI](https://github.styleci.io/repos/41589337/shield?style=flat)](https://github.styleci.io/repos/41589337)
 
 A platform for storing and organising information and documentation. Details for BookStack can be found on the official website at https://www.bookstackapp.com/.
 
@@ -189,4 +190,6 @@ These are the great open-source projects used to help build BookStack:
 * [OneLogin's SAML PHP Toolkit](https://github.com/onelogin/php-saml)
 * [League/CommonMark](https://commonmark.thephpleague.com/)
 * [League/Flysystem](https://flysystem.thephpleague.com)
-* [StyleCI](https://styleci.io/)
\ No newline at end of file
+* [StyleCI](https://styleci.io/)
+* [pragmarx/google2fa](https://github.com/antonioribeiro/google2fa)
+* [Bacon/BaconQrCode](https://github.com/Bacon/BaconQrCode)
\ No newline at end of file
index 05055d1bccbc6aa9442c87228506e76e1b384e60..18ca165718f782a8f7cc083e6a42561e0b52605f 100644 (file)
@@ -47,6 +47,10 @@ return [
     'favourite_add_notification' => '":name" has been added to your favourites',
     'favourite_remove_notification' => '":name" has been removed from your favourites',
 
+    // MFA
+    'mfa_setup_method_notification' => 'Multi-factor method successfully configured',
+    'mfa_remove_method_notification' => 'Multi-factor method successfully removed',
+
     // Other
     'commented_on'                => 'تم التعليق',
     'permissions_update'          => 'تحديث الأذونات',
index 363310bba914b78e54ea4b2da260ec3457d801fe..c0cc8bbc0e76bfa948d67a4c66a8f63c58b7ff04 100644 (file)
@@ -73,5 +73,40 @@ return [
     'user_invite_page_welcome' => 'مرحبا بكم في :appName!',
     'user_invite_page_text' => 'لإكمال حسابك والحصول على حق الوصول تحتاج إلى تعيين كلمة مرور سيتم استخدامها لتسجيل الدخول إلى :appName في الزيارات المستقبلية.',
     'user_invite_page_confirm_button' => 'تأكيد كلمة المرور',
-    'user_invite_success' => 'مجموعة كلمات المرور، لديك الآن حق الوصول إلى :appName!'
+    'user_invite_success' => 'مجموعة كلمات المرور، لديك الآن حق الوصول إلى :appName!',
+
+    // Multi-factor Authentication
+    'mfa_setup' => 'Setup Multi-Factor Authentication',
+    'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
+    'mfa_setup_configured' => 'Already configured',
+    'mfa_setup_reconfigure' => 'Reconfigure',
+    'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?',
+    'mfa_setup_action' => 'Setup',
+    'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.',
+    'mfa_option_totp_title' => 'Mobile App',
+    'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
+    'mfa_option_backup_codes_title' => 'Backup Codes',
+    'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.',
+    'mfa_gen_confirm_and_enable' => 'Confirm and Enable',
+    'mfa_gen_backup_codes_title' => 'Backup Codes Setup',
+    'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.',
+    'mfa_gen_backup_codes_download' => 'Download Codes',
+    'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once',
+    'mfa_gen_totp_title' => 'Mobile App Setup',
+    'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
+    'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.',
+    'mfa_gen_totp_verify_setup' => 'Verify Setup',
+    'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:',
+    'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here',
+    'mfa_verify_access' => 'Verify Access',
+    'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.',
+    'mfa_verify_no_methods' => 'No Methods Configured',
+    'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.',
+    'mfa_verify_use_totp' => 'Verify using a mobile app',
+    'mfa_verify_use_backup_codes' => 'Verify using a backup code',
+    'mfa_verify_backup_code' => 'Backup Code',
+    'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:',
+    'mfa_verify_backup_code_enter_here' => 'Enter backup code here',
+    'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:',
+    'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.',
 ];
\ No newline at end of file
index b9b2032344683ff104edc4652f3bd118651e38b7..de40e583e1461fadc6787e4c2689e757d139e302 100644 (file)
@@ -39,6 +39,7 @@ return [
     'reset' => 'إعادة تعيين',
     'remove' => 'إزالة',
     'add' => 'إضافة',
+    'configure' => 'Configure',
     'fullscreen' => 'شاشة كاملة',
     'favourite' => 'Favourite',
     'unfavourite' => 'Unfavourite',
@@ -60,6 +61,7 @@ return [
     'no_activity' => 'لا يوجد نشاط لعرضه',
     'no_items' => 'لا توجد عناصر متوفرة',
     'back_to_top' => 'العودة إلى الأعلى',
+    'skip_to_main_content' => 'Skip to main content',
     'toggle_details' => 'عرض / إخفاء التفاصيل',
     'toggle_thumbnails' => 'عرض / إخفاء الصور المصغرة',
     'details' => 'التفاصيل',
index dec4522f43bffc28e94a103f97ad98107c95f6f5..7e6dfc7a18649069a58bf6bbec6ed9bab2efa2af 100644 (file)
@@ -36,6 +36,7 @@ return [
     'export_html' => 'صفحة ويب',
     'export_pdf' => 'ملف PDF',
     'export_text' => 'ملف نص عادي',
+    'export_md' => 'Markdown File',
 
     // Permissions and restrictions
     'permissions' => 'الأذونات',
@@ -98,6 +99,7 @@ return [
     'shelves_permissions' => 'أذونات رف الكتب',
     'shelves_permissions_updated' => 'تم تحديث أذونات رف الكتب',
     'shelves_permissions_active' => 'أذونات رف الكتب نشطة',
+    'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',
     'shelves_copy_permissions_to_books' => 'نسخ أذونات الوصول إلى الكتب',
     'shelves_copy_permissions' => 'نسخ الأذونات',
     'shelves_copy_permissions_explain' => 'سيؤدي هذا إلى تطبيق إعدادات الأذونات الحالية لهذا الرف على جميع الكتب المتضمنة فيه. قبل التفعيل، تأكد من حفظ أي تغييرات في أذونات هذا الرف.',
index 88f68a2f8568c0540a7881c3c70592c5b3772044..2ceb849bc256e1b1a3be4dae8d17bfecf58ed425 100755 (executable)
@@ -92,6 +92,7 @@ return [
     'recycle_bin' => 'سلة المحذوفات',
     'recycle_bin_desc' => 'هنا يمكنك استعادة العناصر التي تم حذفها أو اختيار إزالتها نهائيا من النظام. هذه القائمة غير مصفاة خلافاً لقوائم الأنشطة المماثلة في النظام حيث يتم تطبيق عوامل تصفية الأذونات.',
     'recycle_bin_deleted_item' => 'عنصر محذوف',
+    'recycle_bin_deleted_parent' => 'Parent',
     'recycle_bin_deleted_by' => 'حُذف بواسطة',
     'recycle_bin_deleted_at' => 'وقت الحذف',
     'recycle_bin_permanently_delete' => 'حُذف نهائيًا',
@@ -104,6 +105,7 @@ return [
     'recycle_bin_restore_list' => 'العناصر المراد استرجاعها',
     'recycle_bin_restore_confirm' => 'سيعيد هذا الإجراء العنصر المحذوف ، بما في ذلك أي عناصر فرعية ، إلى موقعه الأصلي. إذا تم حذف الموقع الأصلي منذ ذلك الحين ، وهو الآن في سلة المحذوفات ، فسيلزم أيضًا استعادة العنصر الأصلي.',
     'recycle_bin_restore_deleted_parent' => 'تم حذف أصل هذا العنصر أيضًا. سيبقى حذفه حتى يتم استعادة ذلك الأصل أيضًا.',
+    'recycle_bin_restore_parent' => 'Restore Parent',
     'recycle_bin_destroy_notification' => 'المحذوف: قُم بعد إجمالي العناصر من سلة المحذوفات.',
     'recycle_bin_restore_notification' => 'المرتجع: قُم بعد إجمالي العناصر من سلة المحذوفات.',
 
@@ -117,6 +119,7 @@ return [
     'audit_table_user' => 'المستخدم',
     'audit_table_event' => 'الحدث',
     'audit_table_related' => 'العنصر أو التفاصيل ذات الصلة',
+    'audit_table_ip' => 'IP Address',
     'audit_table_date' => 'تاريخ النشاط',
     'audit_date_from' => 'نطاق التاريخ من',
     'audit_date_to' => 'نطاق التاريخ إلى',
@@ -136,6 +139,7 @@ return [
     'role_details' => 'تفاصيل الدور',
     'role_name' => 'اسم الدور',
     'role_desc' => 'وصف مختصر للدور',
+    'role_mfa_enforced' => 'Requires Multi-Factor Authentication',
     'role_external_auth_id' => 'ربط الحساب بمواقع التواصل',
     'role_system' => 'أذونات النظام',
     'role_manage_users' => 'إدارة المستخدمين',
@@ -145,6 +149,7 @@ return [
     'role_manage_page_templates' => 'إدارة قوالب الصفحة',
     'role_access_api' => 'الوصول إلى واجهة برمجة تطبيقات النظام API',
     'role_manage_settings' => 'إدارة إعدادات التطبيق',
+    'role_export_content' => 'Export content',
     'role_asset' => 'أذونات الأصول',
     'roles_system_warning' => 'اعلم أن الوصول إلى أي من الأذونات الثلاثة المذكورة أعلاه يمكن أن يسمح للمستخدم بتغيير امتيازاته الخاصة أو امتيازات الآخرين في النظام. قم بتعيين الأدوار مع هذه الأذونات فقط للمستخدمين الموثوق بهم.',
     'role_asset_desc' => 'تتحكم هذه الأذونات في الوصول الافتراضي إلى الأصول داخل النظام. ستتجاوز الأذونات الخاصة بالكتب والفصول والصفحات هذه الأذونات.',
@@ -202,6 +207,10 @@ return [
     'users_api_tokens_create' => 'قم بإنشاء رمز مميز',
     'users_api_tokens_expires' => 'انتهاء مدة الصلاحية',
     'users_api_tokens_docs' => 'وثائق API',
+    'users_mfa' => 'Multi-Factor Authentication',
+    'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
+    'users_mfa_x_methods' => ':count method configured|:count methods configured',
+    'users_mfa_configure' => 'Configure Methods',
 
     // API Tokens
     'user_api_token_create' => 'قم بإنشاء رمز API',
@@ -247,6 +256,7 @@ return [
         'it' => 'Italian',
         'ja' => '日本語',
         'ko' => '한국어',
+        'lt' => 'Lietuvių Kalba',
         'lv' => 'Latviešu Valoda',
         'nl' => 'Nederlands',
         'nb' => 'Norsk (Bokmål)',
index 3c03f2efd7269e68f72e8e5f96cc921b828abd30..d4d3aaf26b1f2f4a658c10ea1de911c405549dca 100644 (file)
@@ -15,6 +15,7 @@ return [
     'alpha_dash'           => 'يجب أن يقتصر :attribute على حروف أو أرقام أو شرطات فقط.',
     'alpha_num'            => 'يجب أن يقتصر :attribute على الحروف والأرقام فقط.',
     'array'                => 'يجب أن تكون السمة مصفوفة.',
+    'backup_codes'         => 'The provided code is not valid or has already been used.',
     'before'               => 'يجب أن يكون التاريخ :attribute قبل :date.',
     'between'              => [
         'numeric' => 'يجب أن يكون :attribute بين :min و :max.',
@@ -98,6 +99,7 @@ return [
     ],
     'string'               => 'يجب أن تكون السمة: سلسلة.',
     'timezone'             => 'يجب أن تكون :attribute منطقة صالحة.',
+    'totp'                 => 'The provided code is not valid or has expired.',
     'unique'               => 'تم حجز :attribute من قبل.',
     'url'                  => 'صيغة :attribute غير صالحة.',
     'uploaded'             => 'تعذر تحميل الملف. قد لا يقبل الخادم ملفات بهذا الحجم.',
index 90218a30ac00ed60f511c4b620aa3ddff4cdf329..15715baf9d5353e907e6bfc980be5c08b44f37eb 100644 (file)
@@ -47,6 +47,10 @@ return [
     'favourite_add_notification' => '":name" has been added to your favourites',
     'favourite_remove_notification' => '":name" has been removed from your favourites',
 
+    // MFA
+    'mfa_setup_method_notification' => 'Multi-factor method successfully configured',
+    'mfa_remove_method_notification' => 'Multi-factor method successfully removed',
+
     // Other
     'commented_on'                => 'коментирано на',
     'permissions_update'          => 'updated permissions',
index 018e871e108ddd821fb882e22d52ac2b42bad48d..d04796cfe2e8fc639dcbedc5c7ea95fc9a9f701b 100644 (file)
@@ -73,5 +73,40 @@ return [
     'user_invite_page_welcome' => 'Добре дошли в :appName!',
     'user_invite_page_text' => 'За да финализирате вашият акаунт и да получите достъп трябва да определите парола, която да бъде използвана за следващия влизания в :appName.',
     'user_invite_page_confirm_button' => 'Потвърди паролата',
-    'user_invite_success' => 'Паролата е потвърдена и вече имате достъп до :appName!'
+    'user_invite_success' => 'Паролата е потвърдена и вече имате достъп до :appName!',
+
+    // Multi-factor Authentication
+    'mfa_setup' => 'Setup Multi-Factor Authentication',
+    'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
+    'mfa_setup_configured' => 'Already configured',
+    'mfa_setup_reconfigure' => 'Reconfigure',
+    'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?',
+    'mfa_setup_action' => 'Setup',
+    'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.',
+    'mfa_option_totp_title' => 'Mobile App',
+    'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
+    'mfa_option_backup_codes_title' => 'Backup Codes',
+    'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.',
+    'mfa_gen_confirm_and_enable' => 'Confirm and Enable',
+    'mfa_gen_backup_codes_title' => 'Backup Codes Setup',
+    'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.',
+    'mfa_gen_backup_codes_download' => 'Download Codes',
+    'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once',
+    'mfa_gen_totp_title' => 'Mobile App Setup',
+    'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
+    'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.',
+    'mfa_gen_totp_verify_setup' => 'Verify Setup',
+    'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:',
+    'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here',
+    'mfa_verify_access' => 'Verify Access',
+    'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.',
+    'mfa_verify_no_methods' => 'No Methods Configured',
+    'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.',
+    'mfa_verify_use_totp' => 'Verify using a mobile app',
+    'mfa_verify_use_backup_codes' => 'Verify using a backup code',
+    'mfa_verify_backup_code' => 'Backup Code',
+    'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:',
+    'mfa_verify_backup_code_enter_here' => 'Enter backup code here',
+    'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:',
+    'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.',
 ];
\ No newline at end of file
index 7c833bd0d21855605bd2da5fe44c013fbdd22986..4b0f72a60ce7583d52d3d18dedff853ee5f11f98 100644 (file)
@@ -39,6 +39,7 @@ return [
     'reset' => 'Нулирай',
     'remove' => 'Премахване',
     'add' => 'Добави',
+    'configure' => 'Configure',
     'fullscreen' => 'Пълен екран',
     'favourite' => 'Favourite',
     'unfavourite' => 'Unfavourite',
@@ -60,6 +61,7 @@ return [
     'no_activity' => 'Няма активност за показване',
     'no_items' => 'Няма налични артикули',
     'back_to_top' => 'Върнете се в началото',
+    'skip_to_main_content' => 'Skip to main content',
     'toggle_details' => 'Активирай детайли',
     'toggle_thumbnails' => 'Активирай миниатюри',
     'details' => 'Подробности',
index accf2157a409f7ad1018cf9aae2b2381fbddc018..4880ad25d9886a5e061c0be1c0d081234509444d 100644 (file)
@@ -36,6 +36,7 @@ return [
     'export_html' => 'Прикачени уеб файлове',
     'export_pdf' => 'PDF файл',
     'export_text' => 'Обикновен текстов файл',
+    'export_md' => 'Markdown File',
 
     // Permissions and restrictions
     'permissions' => 'Права',
@@ -98,6 +99,7 @@ return [
     'shelves_permissions' => 'Настройки за достъп до рафта с книги',
     'shelves_permissions_updated' => 'Настройките за достъп до рафта с книги е обновен',
     'shelves_permissions_active' => 'Настройките за достъп до рафта с книги е активен',
+    'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',
     'shelves_copy_permissions_to_books' => 'Копирай настойките за достъп към книгите',
     'shelves_copy_permissions' => 'Копирай настройките за достъп',
     'shelves_copy_permissions_explain' => 'Това ще приложи настоящите настройки за достъп на този рафт с книги за всички книги, съдържащи се в него. Преди да активирате, уверете се, че всички промени в настройките за достъп на този рафт са запазени.',
index c32d3a5dd9d05f9681f5771a1ac4717d0922f90a..5c1e1c9033d5bcf2ed6a725287645045f668e9d3 100644 (file)
@@ -92,6 +92,7 @@ return [
     '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' => 'Изтрит предмет',
+    'recycle_bin_deleted_parent' => 'Parent',
     'recycle_bin_deleted_by' => 'Изтрит от',
     'recycle_bin_deleted_at' => 'Час на изтриване',
     'recycle_bin_permanently_delete' => 'Permanently Delete',
@@ -104,6 +105,7 @@ return [
     '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_restore_parent' => 'Restore Parent',
     'recycle_bin_destroy_notification' => 'Deleted :count total items from the recycle bin.',
     'recycle_bin_restore_notification' => 'Restored :count total items from the recycle bin.',
 
@@ -117,6 +119,7 @@ return [
     'audit_table_user' => 'Потребител',
     'audit_table_event' => 'Събитие',
     'audit_table_related' => 'Related Item or Detail',
+    'audit_table_ip' => 'IP Address',
     'audit_table_date' => 'Дата на активност',
     'audit_date_from' => 'Време от',
     'audit_date_to' => 'Време до',
@@ -136,6 +139,7 @@ return [
     'role_details' => 'Детайли на роля',
     'role_name' => 'Име на ролята',
     'role_desc' => 'Кратко описание на ролята',
+    'role_mfa_enforced' => 'Requires Multi-Factor Authentication',
     'role_external_auth_id' => 'Външни ауторизиращи ID-a',
     'role_system' => 'Настойки за достъп на системата',
     'role_manage_users' => 'Управление на потребители',
@@ -145,6 +149,7 @@ return [
     'role_manage_page_templates' => 'Управление на шаблони на страници',
     'role_access_api' => 'Достъп до API на системата',
     'role_manage_settings' => 'Управление на настройките на приложението',
+    'role_export_content' => 'Export content',
     'role_asset' => 'Настройки за достъп до активи',
     'roles_system_warning' => 'Важно: Добавянето на потребител в някое от горните три роли може да му позволи да промени собствените си права или правата на другите в системата. Възлагайте тези роли само на доверени потребители.',
     'role_asset_desc' => 'Тези настройки за достъп контролират достъпа по подразбиране до активите в системата. Настройките за достъп до книги, глави и страници ще отменят тези настройки.',
@@ -202,6 +207,10 @@ return [
     'users_api_tokens_create' => 'Create Token',
     'users_api_tokens_expires' => 'Expires',
     'users_api_tokens_docs' => 'API Documentation',
+    'users_mfa' => 'Multi-Factor Authentication',
+    'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
+    'users_mfa_x_methods' => ':count method configured|:count methods configured',
+    'users_mfa_configure' => 'Configure Methods',
 
     // API Tokens
     'user_api_token_create' => 'Create API Token',
@@ -247,6 +256,7 @@ return [
         'it' => 'Italian',
         'ja' => '日本語',
         'ko' => '한국어',
+        'lt' => 'Lietuvių Kalba',
         'lv' => 'Latviešu Valoda',
         'nl' => 'Nederlands',
         'nb' => 'Norsk (Bokmål)',
index 3f22751dd567a0cb1a35df2b20020e824fcbf426..0a5b81d92a99deccc961e69658a7b9da10c7dab7 100644 (file)
@@ -15,6 +15,7 @@ return [
     'alpha_dash'           => ':attribute може да съдържа само букви, числа, тире и долна черта.',
     'alpha_num'            => ':attribute може да съдържа само букви и числа.',
     'array'                => ':attribute трябва да е масив (array).',
+    'backup_codes'         => 'The provided code is not valid or has already been used.',
     'before'               => ':attribute трябва да е дата след :date.',
     'between'              => [
         'numeric' => ':attribute трябва да е между :min и :max.',
@@ -98,6 +99,7 @@ return [
     ],
     'string'               => 'The :attribute must be a string.',
     'timezone'             => 'The :attribute must be a valid zone.',
+    'totp'                 => 'The provided code is not valid or has expired.',
     'unique'               => 'The :attribute has already been taken.',
     'url'                  => 'The :attribute format is invalid.',
     'uploaded'             => 'The file could not be uploaded. The server may not accept files of this size.',
index c4bb5a27e75f2fc1926fff1cb0ace0144af6e5bd..9136e651bed46200284a629211af005d289ec67a 100644 (file)
@@ -44,8 +44,12 @@ return [
     'bookshelf_delete_notification'    => 'Polica za knjige Uspješno Izbrisana',
 
     // Favourites
-    'favourite_add_notification' => '":name" has been added to your favourites',
-    'favourite_remove_notification' => '":name" has been removed from your favourites',
+    'favourite_add_notification' => '":name" je dodan u tvoje favorite',
+    'favourite_remove_notification' => '":name" je uklonjen iz tvojih favorita',
+
+    // MFA
+    'mfa_setup_method_notification' => 'Multi-factor method successfully configured',
+    'mfa_remove_method_notification' => 'Multi-factor method successfully removed',
 
     // Other
     'commented_on'                => 'je komentarisao/la na',
index 526a8612fb9749650d8a147f96feda6727716ab0..a5926fa2b6d335ac8a32ca122016d09cc602bffc 100644 (file)
@@ -73,5 +73,40 @@ return [
     'user_invite_page_welcome' => 'Dobrodošli na :appName!',
     'user_invite_page_text' => 'Da biste završili vaš račun i dobili pristup morate postaviti lozinku koju ćete koristiti da se prijavite na :appName tokom budućih posjeta.',
     'user_invite_page_confirm_button' => 'Potvrdi lozinku',
-    'user_invite_success' => 'Lozinka postavljena, sada imate pristup :sppName!'
+    'user_invite_success' => 'Lozinka postavljena, sada imate pristup :sppName!',
+
+    // Multi-factor Authentication
+    'mfa_setup' => 'Setup Multi-Factor Authentication',
+    'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
+    'mfa_setup_configured' => 'Already configured',
+    'mfa_setup_reconfigure' => 'Reconfigure',
+    'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?',
+    'mfa_setup_action' => 'Setup',
+    'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.',
+    'mfa_option_totp_title' => 'Mobile App',
+    'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
+    'mfa_option_backup_codes_title' => 'Backup Codes',
+    'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.',
+    'mfa_gen_confirm_and_enable' => 'Confirm and Enable',
+    'mfa_gen_backup_codes_title' => 'Backup Codes Setup',
+    'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.',
+    'mfa_gen_backup_codes_download' => 'Download Codes',
+    'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once',
+    'mfa_gen_totp_title' => 'Mobile App Setup',
+    'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
+    'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.',
+    'mfa_gen_totp_verify_setup' => 'Verify Setup',
+    'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:',
+    'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here',
+    'mfa_verify_access' => 'Verify Access',
+    'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.',
+    'mfa_verify_no_methods' => 'No Methods Configured',
+    'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.',
+    'mfa_verify_use_totp' => 'Verify using a mobile app',
+    'mfa_verify_use_backup_codes' => 'Verify using a backup code',
+    'mfa_verify_backup_code' => 'Backup Code',
+    'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:',
+    'mfa_verify_backup_code_enter_here' => 'Enter backup code here',
+    'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:',
+    'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.',
 ];
\ No newline at end of file
index 4a1af9de083992c090aec20cb9b1abba763754ca..17ec0b765c7447dd78a9541f9ad62e0a8c7acc76 100644 (file)
@@ -39,11 +39,12 @@ return [
     'reset' => 'Resetuj',
     'remove' => 'Ukloni',
     'add' => 'Dodaj',
+    'configure' => 'Configure',
     'fullscreen' => 'Prikaz preko čitavog ekrana',
-    'favourite' => 'Favourite',
-    'unfavourite' => 'Unfavourite',
-    'next' => 'Next',
-    'previous' => 'Previous',
+    'favourite' => 'Favorit',
+    'unfavourite' => 'Ukloni favorit',
+    'next' => 'Sljedeće',
+    'previous' => 'Prethodno',
 
     // Sort Options
     'sort_options' => 'Opcije sortiranja',
@@ -51,7 +52,7 @@ return [
     'sort_ascending' => 'Sortiraj uzlazno',
     'sort_descending' => 'Sortiraj silazno',
     'sort_name' => 'Ime',
-    'sort_default' => 'Default',
+    'sort_default' => 'Početne postavke',
     'sort_created_at' => 'Datum kreiranja',
     'sort_updated_at' => 'Datum ažuriranja',
 
@@ -60,6 +61,7 @@ return [
     'no_activity' => 'Nema aktivnosti za prikazivanje',
     'no_items' => 'Nema dostupnih stavki',
     'back_to_top' => 'Povratak na vrh',
+    'skip_to_main_content' => 'Idi odmah na glavni sadržaj',
     'toggle_details' => 'Vidi detalje',
     'toggle_thumbnails' => 'Vidi prikaze slika',
     'details' => 'Detalji',
@@ -69,7 +71,7 @@ return [
     'breadcrumb' => 'Navigacijske stavke',
 
     // Header
-    'header_menu_expand' => 'Expand Header Menu',
+    'header_menu_expand' => 'Otvori meni u zaglavlju',
     'profile_menu' => 'Meni profila',
     'view_profile' => 'Pogledaj profil',
     'edit_profile' => 'Izmjeni profil',
@@ -78,9 +80,9 @@ return [
 
     // Layout tabs
     'tab_info' => 'Informacije',
-    'tab_info_label' => 'Tab: Show Secondary Information',
+    'tab_info_label' => 'Kartica: Prikaži dodatnu informaciju',
     'tab_content' => 'Sadržaj',
-    'tab_content_label' => 'Tab: Show Primary Content',
+    'tab_content_label' => 'Kartica: Prikaži glavni sadržaj',
 
     // Email Content
     'email_action_help' => 'Ukoliko imate poteškoća sa pritiskom na ":actionText" dugme, kopirajte i zaljepite URL koji se nalazi ispod u vaš web pretraživač:',
index 14f4e351cfb8e82fdde2ae89aaf9adf96444e526..52924d96fcb6133eebae9de3e525c528a0562ce5 100644 (file)
@@ -36,6 +36,7 @@ return [
     'export_html' => 'Sadržani web fajl',
     'export_pdf' => 'PDF fajl',
     'export_text' => 'Plain Text fajl',
+    'export_md' => 'Markdown File',
 
     // Permissions and restrictions
     'permissions' => 'Dozvole',
@@ -98,6 +99,7 @@ return [
     'shelves_permissions' => 'Bookshelf Permissions',
     'shelves_permissions_updated' => 'Bookshelf Permissions Updated',
     'shelves_permissions_active' => 'Bookshelf Permissions Active',
+    'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',
     '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.',
index 8a7946b1281269a0f62d17d6dacea2afd466f56b..0ab168b66998bca0de4807f94d181b9b12ed1683 100644 (file)
@@ -92,6 +92,7 @@ return [
     '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_parent' => 'Parent',
     'recycle_bin_deleted_by' => 'Deleted By',
     'recycle_bin_deleted_at' => 'Deletion Time',
     'recycle_bin_permanently_delete' => 'Permanently Delete',
@@ -104,6 +105,7 @@ return [
     '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_restore_parent' => 'Restore Parent',
     'recycle_bin_destroy_notification' => 'Deleted :count total items from the recycle bin.',
     'recycle_bin_restore_notification' => 'Restored :count total items from the recycle bin.',
 
@@ -117,6 +119,7 @@ return [
     'audit_table_user' => 'User',
     'audit_table_event' => 'Event',
     'audit_table_related' => 'Related Item or Detail',
+    'audit_table_ip' => 'IP Address',
     'audit_table_date' => 'Activity Date',
     'audit_date_from' => 'Date Range From',
     'audit_date_to' => 'Date Range To',
@@ -136,6 +139,7 @@ return [
     'role_details' => 'Role Details',
     'role_name' => 'Role Name',
     'role_desc' => 'Short Description of Role',
+    'role_mfa_enforced' => 'Requires Multi-Factor Authentication',
     'role_external_auth_id' => 'External Authentication IDs',
     'role_system' => 'System Permissions',
     'role_manage_users' => 'Manage users',
@@ -145,6 +149,7 @@ return [
     'role_manage_page_templates' => 'Manage page templates',
     'role_access_api' => 'Access system API',
     'role_manage_settings' => 'Manage app settings',
+    'role_export_content' => 'Export content',
     '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.',
@@ -202,6 +207,10 @@ return [
     'users_api_tokens_create' => 'Create Token',
     'users_api_tokens_expires' => 'Expires',
     'users_api_tokens_docs' => 'API Documentation',
+    'users_mfa' => 'Multi-Factor Authentication',
+    'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
+    'users_mfa_x_methods' => ':count method configured|:count methods configured',
+    'users_mfa_configure' => 'Configure Methods',
 
     // API Tokens
     'user_api_token_create' => 'Create API Token',
@@ -247,6 +256,7 @@ return [
         'it' => 'Italian',
         'ja' => '日本語',
         'ko' => '한국어',
+        'lt' => 'Lietuvių Kalba',
         'lv' => 'Latviešu Valoda',
         'nl' => 'Nederlands',
         'nb' => 'Norsk (Bokmål)',
index f3af075fba83007c208f7600012a8ef36866dbba..d6887ccc7f93723550fe10dc66863f0963f93e59 100644 (file)
@@ -15,6 +15,7 @@ return [
     'alpha_dash'           => ':attribute može sadržavati samo slova, brojeve, crtice i donje crtice.',
     'alpha_num'            => ':attribute može sadržavati samo slova i brojeve.',
     'array'                => ':attribute mora biti niz.',
+    'backup_codes'         => 'The provided code is not valid or has already been used.',
     'before'               => ':attribute mora biti datum prije :date.',
     'between'              => [
         'numeric' => ':attribute mora biti između :min i :max.',
@@ -98,6 +99,7 @@ return [
     ],
     'string'               => ':attribute mora biti string.',
     'timezone'             => ':attribute mora biti ispravna zona.',
+    'totp'                 => 'The provided code is not valid or has expired.',
     'unique'               => ':attribute je zauzet.',
     'url'                  => 'Format :attribute je neispravan.',
     'uploaded'             => 'Fajl nije učitan. Server ne prihvata fajlove ove veličine.',
index 604eaeda4f172af52fe049d3568e57c017456050..18878c2a6d6220757a6ea3e2b1ab23c902f3cfc1 100644 (file)
@@ -47,6 +47,10 @@ return [
     'favourite_add_notification' => '":name" has been added to your favourites',
     'favourite_remove_notification' => '":name" has been removed from your favourites',
 
+    // MFA
+    'mfa_setup_method_notification' => 'Multi-factor method successfully configured',
+    'mfa_remove_method_notification' => 'Multi-factor method successfully removed',
+
     // Other
     'commented_on'                => 'ha comentat a',
     'permissions_update'          => 'ha actualitzat els permisos',
index a66f75f94ee8e96d19816457fcef07e8219d0f00..9febe36b75ab08860a07b998a727c76aed26e2fc 100644 (file)
@@ -73,5 +73,40 @@ return [
     'user_invite_page_welcome' => 'Us donem la benvinguda a :appName!',
     'user_invite_page_text' => 'Per a enllestir el vostre compte i obtenir-hi accés, cal que definiu una contrasenya, que es farà servir per a iniciar la sessió a :appName en futures visites.',
     'user_invite_page_confirm_button' => 'Confirma la contrasenya',
-    'user_invite_success' => 'S\'ha establert la contrasenya, ara ja teniu accés a :appName!'
+    'user_invite_success' => 'S\'ha establert la contrasenya, ara ja teniu accés a :appName!',
+
+    // Multi-factor Authentication
+    'mfa_setup' => 'Setup Multi-Factor Authentication',
+    'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
+    'mfa_setup_configured' => 'Already configured',
+    'mfa_setup_reconfigure' => 'Reconfigure',
+    'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?',
+    'mfa_setup_action' => 'Setup',
+    'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.',
+    'mfa_option_totp_title' => 'Mobile App',
+    'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
+    'mfa_option_backup_codes_title' => 'Backup Codes',
+    'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.',
+    'mfa_gen_confirm_and_enable' => 'Confirm and Enable',
+    'mfa_gen_backup_codes_title' => 'Backup Codes Setup',
+    'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.',
+    'mfa_gen_backup_codes_download' => 'Download Codes',
+    'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once',
+    'mfa_gen_totp_title' => 'Mobile App Setup',
+    'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
+    'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.',
+    'mfa_gen_totp_verify_setup' => 'Verify Setup',
+    'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:',
+    'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here',
+    'mfa_verify_access' => 'Verify Access',
+    'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.',
+    'mfa_verify_no_methods' => 'No Methods Configured',
+    'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.',
+    'mfa_verify_use_totp' => 'Verify using a mobile app',
+    'mfa_verify_use_backup_codes' => 'Verify using a backup code',
+    'mfa_verify_backup_code' => 'Backup Code',
+    'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:',
+    'mfa_verify_backup_code_enter_here' => 'Enter backup code here',
+    'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:',
+    'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.',
 ];
\ No newline at end of file
index 03d38a2d3d2d0a548ab4cff1134e5dfe2a15bbd1..49739506000d171c0f044071a4087e6e4040f8cf 100644 (file)
@@ -39,6 +39,7 @@ return [
     'reset' => 'Reinicialitza',
     'remove' => 'Elimina',
     'add' => 'Afegeix',
+    'configure' => 'Configure',
     'fullscreen' => 'Pantalla completa',
     'favourite' => 'Favourite',
     'unfavourite' => 'Unfavourite',
@@ -60,6 +61,7 @@ return [
     'no_activity' => 'No hi ha activitat',
     'no_items' => 'No hi ha cap element',
     'back_to_top' => 'Torna a dalt',
+    'skip_to_main_content' => 'Skip to main content',
     'toggle_details' => 'Commuta els detalls',
     'toggle_thumbnails' => 'Commuta les miniatures',
     'details' => 'Detalls',
index 55921f3b619954debf061ff6c01d206b2d2fcfac..d3abdeae702ad8457de0d7a82d994e2e185ab0fc 100644 (file)
@@ -36,6 +36,7 @@ return [
     'export_html' => 'Fitxer web independent',
     'export_pdf' => 'Fitxer PDF',
     'export_text' => 'Fitxer de text sense format',
+    'export_md' => 'Markdown File',
 
     // Permissions and restrictions
     'permissions' => 'Permisos',
@@ -98,6 +99,7 @@ return [
     'shelves_permissions' => 'Permisos del prestatge',
     'shelves_permissions_updated' => 'S\'han actualitzat els permisos del prestatge',
     'shelves_permissions_active' => 'S\'han activat els permisos del prestatge',
+    'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',
     'shelves_copy_permissions_to_books' => 'Copia els permisos als llibres',
     'shelves_copy_permissions' => 'Copia els permisos',
     'shelves_copy_permissions_explain' => 'Això aplicarà la configuració de permisos actual d\'aquest prestatge a tots els llibres que contingui. Abans d\'activar-ho, assegureu-vos que hàgiu desat qualsevol canvi als permisos d\'aquest prestatge.',
index 165c014b4df250601d756255a89f5d931354999d..3a3fdddc1aff5a60194dda551858821c8a3b565e 100755 (executable)
@@ -92,6 +92,7 @@ return [
     'recycle_bin' => 'Paperera de reciclatge',
     'recycle_bin_desc' => 'Aquí podeu restaurar els elements que hàgiu suprimit o triar suprimir-los del sistema de manera permanent. Aquesta llista no té cap filtre, al contrari que altres llistes d\'activitat similars en què es tenen en compte els filtres de permisos.',
     'recycle_bin_deleted_item' => 'Element suprimit',
+    'recycle_bin_deleted_parent' => 'Parent',
     'recycle_bin_deleted_by' => 'Suprimit per',
     'recycle_bin_deleted_at' => 'Moment de la supressió',
     'recycle_bin_permanently_delete' => 'Suprimeix permanentment',
@@ -104,6 +105,7 @@ return [
     'recycle_bin_restore_list' => 'Elements que es restauraran',
     'recycle_bin_restore_confirm' => 'Aquesta acció restaurarà l\'element suprimit, incloent-hi tots els elements fills, a la seva ubicació original. Si la ubicació original ha estat suprimida, i ara és a la paperera de reciclatge, caldrà que també en restaureu l\'element pare.',
     'recycle_bin_restore_deleted_parent' => 'El pare d\'aquest element també ha estat suprimit. L\'element es mantindrà suprimit fins que el pare també es restauri.',
+    'recycle_bin_restore_parent' => 'Restore Parent',
     'recycle_bin_destroy_notification' => 'S\'han suprimit :count elements en total de la paperera de reciclatge.',
     'recycle_bin_restore_notification' => 'S\'han restaurat :count elements en total de la paperera de reciclatge.',
 
@@ -117,6 +119,7 @@ return [
     'audit_table_user' => 'Usuari',
     'audit_table_event' => 'Esdeveniment',
     'audit_table_related' => 'Element relacionat o detall',
+    'audit_table_ip' => 'IP Address',
     'audit_table_date' => 'Data de l\'activitat',
     'audit_date_from' => 'Rang de dates a partir de',
     'audit_date_to' => 'Rang de rates fins a',
@@ -136,6 +139,7 @@ return [
     'role_details' => 'Detalls del rol',
     'role_name' => 'Nom del rol',
     'role_desc' => 'Descripció curta del rol',
+    'role_mfa_enforced' => 'Requires Multi-Factor Authentication',
     'role_external_auth_id' => 'Identificadors d\'autenticació externa',
     'role_system' => 'Permisos del sistema',
     'role_manage_users' => 'Gestiona usuaris',
@@ -145,6 +149,7 @@ return [
     'role_manage_page_templates' => 'Gestiona les plantilles de pàgines',
     'role_access_api' => 'Accedeix a l\'API del sistema',
     'role_manage_settings' => 'Gestiona la configuració de l\'aplicació',
+    'role_export_content' => 'Export content',
     'role_asset' => 'Permisos de recursos',
     'roles_system_warning' => 'Tingueu en compte que l\'accés a qualsevol dels tres permisos de dalt pot permetre que un usuari alteri els seus propis permisos o els privilegis d\'altres usuaris del sistema. Assigneu rols amb aquests permisos només a usuaris de confiança.',
     'role_asset_desc' => 'Aquests permisos controlen l\'accés per defecte als recursos del sistema. Els permisos de llibres, capítols i pàgines tindran més importància que aquests permisos.',
@@ -202,6 +207,10 @@ return [
     'users_api_tokens_create' => 'Crea un testimoni',
     'users_api_tokens_expires' => 'Caducitat',
     'users_api_tokens_docs' => 'Documentació de l\'API',
+    'users_mfa' => 'Multi-Factor Authentication',
+    'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
+    'users_mfa_x_methods' => ':count method configured|:count methods configured',
+    'users_mfa_configure' => 'Configure Methods',
 
     // API Tokens
     'user_api_token_create' => 'Crea un testimoni d\'API',
@@ -247,6 +256,7 @@ return [
         'it' => 'Italian',
         'ja' => '日本語',
         'ko' => '한국어',
+        'lt' => 'Lietuvių Kalba',
         'lv' => 'Latviešu Valoda',
         'nl' => 'Nederlands',
         'nb' => 'Norsk (Bokmål)',
index c134c36c6621ad28ec6bf3c51b00715c84cd0a58..603182c1a2e8c249c5d1ed66dcb4efd8313103ba 100644 (file)
@@ -15,6 +15,7 @@ return [
     'alpha_dash'           => 'El camp :attribute només pot contenir lletres, números, guions i guions baixos.',
     'alpha_num'            => 'El camp :attribute només pot contenir lletres i números.',
     'array'                => 'El camp :attribute ha de ser un vector.',
+    'backup_codes'         => 'The provided code is not valid or has already been used.',
     'before'               => 'El camp :attribute ha de ser una data anterior a :date.',
     'between'              => [
         'numeric' => 'El camp :attribute ha d\'estar entre :min i :max.',
@@ -98,6 +99,7 @@ return [
     ],
     'string'               => 'El camp :attribute ha de ser una cadena.',
     'timezone'             => 'El camp :attribute ha de ser una zona vàlida.',
+    'totp'                 => 'The provided code is not valid or has expired.',
     'unique'               => 'El camp :attribute ja està ocupat.',
     'url'                  => 'El format del camp :attribute no és vàlid.',
     'uploaded'             => 'No s\'ha pogut pujar el fitxer. És possible que el servidor no accepti fitxers d\'aquesta mida.',
index e444399cc5a849d89f1c1654342cbb0b9e22bdf1..7178147025868db5a2878b1240c84f793e261da8 100644 (file)
@@ -11,7 +11,7 @@ return [
     'page_update'                 => 'aktualizoval/a stránku',
     'page_update_notification'    => 'Stránka byla úspěšně aktualizována',
     'page_delete'                 => 'odstranil/a stránku',
-    'page_delete_notification'    => 'Stránka byla úspěšně smazána',
+    'page_delete_notification'    => 'Stránka byla odstraněna',
     'page_restore'                => 'obnovil/a stránku',
     'page_restore_notification'   => 'Stránka byla úspěšně obnovena',
     'page_move'                   => 'přesunul/a stránku',
@@ -21,19 +21,19 @@ return [
     'chapter_create_notification' => 'Kapitola byla úspěšně vytvořena',
     'chapter_update'              => 'aktualizoval/a kapitolu',
     'chapter_update_notification' => 'Kapitola byla úspěšně aktualizována',
-    'chapter_delete'              => 'smazal/a kapitolu',
-    'chapter_delete_notification' => 'Kapitola byla úspěšně smazána',
+    'chapter_delete'              => 'odstranila/a kapitolu',
+    'chapter_delete_notification' => 'Kapitola byla odstraněna',
     'chapter_move'                => 'přesunul/a kapitolu',
 
     // Books
     'book_create'                 => 'vytvořil/a knihu',
-    'book_create_notification'    => 'Kniha byla úspěšně vytvořena',
+    'book_create_notification'    => 'Kniha byla vytvořena',
     'book_update'                 => 'aktualizoval/a knihu',
-    'book_update_notification'    => 'Kniha byla úspěšně aktualizována',
-    'book_delete'                 => 'smazal/a knihu',
-    'book_delete_notification'    => 'Kniha byla úspěšně smazána',
+    'book_update_notification'    => 'Kniha byla aktualizována',
+    'book_delete'                 => 'odstranil/a knihu',
+    'book_delete_notification'    => 'Kniha byla odstraněna',
     'book_sort'                   => 'seřadil/a knihu',
-    'book_sort_notification'      => 'Kniha byla úspěšně seřazena',
+    'book_sort_notification'      => 'Kniha byla seřazena',
 
     // Bookshelves
     'bookshelf_create'            => 'vytvořil/a knihovnu',
@@ -41,13 +41,17 @@ return [
     'bookshelf_update'                 => 'aktualizoval/a knihovnu',
     'bookshelf_update_notification'    => 'Knihovna byla úspěšně aktualizována',
     'bookshelf_delete'                 => 'odstranil/a knihovnu',
-    'bookshelf_delete_notification'    => 'Knihovna byla úspěšně odstraněna',
+    'bookshelf_delete_notification'    => 'Knihovna byla odstraněna',
 
     // Favourites
-    'favourite_add_notification' => '":name" has been added to your favourites',
-    'favourite_remove_notification' => '":name" has been removed from your favourites',
+    'favourite_add_notification' => '":name" byla přidána do Vašich oblíbených',
+    'favourite_remove_notification' => '":name" byla odstraněna z Vašich oblíbených',
+
+    // MFA
+    'mfa_setup_method_notification' => 'Multi-factor method successfully configured',
+    'mfa_remove_method_notification' => 'Multi-factor method successfully removed',
 
     // Other
     'commented_on'                => 'okomentoval/a',
-    'permissions_update'          => 'updated permissions',
+    'permissions_update'          => 'oprávnění upravena',
 ];
index a9f36c390b9cff31c109da8256f95574e8569932..f8cdb77479d3da618dec296cb5eb484c0d4b73db 100644 (file)
@@ -6,45 +6,45 @@
  */
 return [
 
-    'failed' => 'Tyto přihlašovací údaje neodpovídají našim záznamům.',
+    'failed' => 'Neplatné přihlašovací údaje.',
     'throttle' => 'Příliš mnoho pokusů o přihlášení. Zkuste to prosím znovu za :seconds sekund.',
 
     // Login & Register
     'sign_up' => 'Registrace',
     'log_in' => 'Přihlášení',
-    'log_in_with' => 'Přihlásit se pomocí :socialDriver',
-    'sign_up_with' => 'Registrovat se pomocí :socialDriver',
+    'log_in_with' => 'Přihlásit se přes :socialDriver',
+    'sign_up_with' => 'Registrovat se přes :socialDriver',
     'logout' => 'Odhlásit',
 
     'name' => 'Jméno',
     'username' => 'Uživatelské jméno',
     'email' => 'E-mail',
     'password' => 'Heslo',
-    'password_confirm' => 'Potvrdit heslo',
-    'password_hint' => 'Musí mít více než 7 znaků',
-    'forgot_password' => 'Zapomněli jste heslo?',
+    'password_confirm' => 'Potvrzení hesla',
+    'password_hint' => 'Musí mít víc než 7 znaků',
+    'forgot_password' => 'Zapomenuté heslo?',
     'remember_me' => 'Zapamatovat si mě',
     'ldap_email_hint' => 'Zadejte email, který chcete přiřadit k tomuto účtu.',
     'create_account' => 'Vytvořit účet',
     'already_have_account' => 'Již máte účet?',
-    'dont_have_account' => 'Nemáte účet?',
-    'social_login' => 'Přihlášení pomocí sociálních sítí',
-    'social_registration' => 'Přihlášení pomocí sociálních sítí',
-    'social_registration_text' => 'Registrovat a přihlásit se pomocí jiné služby.',
+    'dont_have_account' => 'Nemáte učet?',
+    'social_login' => 'Přihlášení přes sociální sítě',
+    'social_registration' => 'Registrace přes sociální sítě',
+    'social_registration_text' => 'Registrovat a přihlásit se přes jinou službu',
 
     'register_thanks' => 'Děkujeme za registraci!',
     'register_confirm' => 'Zkontrolujte prosím svůj e-mail a klikněte na potvrzovací tlačítko pro přístup do :appName.',
-    'registrations_disabled' => 'Registrace jsou aktuálně zakázány',
-    'registration_email_domain_invalid' => 'Tato e-mailová doména nemá přístup k této aplikaci',
+    'registrations_disabled' => 'Registrace jsou momentálně pozastaveny',
+    'registration_email_domain_invalid' => 'Registrace z této e-mailové domény nejsou povoleny',
     'register_success' => 'Děkujeme za registraci! Nyní jste zaregistrováni a přihlášeni.',
 
 
     // Password Reset
     'reset_password' => 'Obnovit heslo',
-    'reset_password_send_instructions' => 'Níže zadejte svou e-mailovou adresu a bude vám zaslán e-mail s odkazem pro obnovení hesla.',
-    'reset_password_send_button' => 'Zaslat odkaz pro obnovení',
+    'reset_password_send_instructions' => 'Níže zadejte svou e-mailovou adresu a bude vám zaslán e-mail s odkazem na obnovení hesla.',
+    'reset_password_send_button' => 'Zaslat odkaz na obnovení hesla',
     'reset_password_sent' => 'Odkaz pro obnovení hesla bude odeslán na :email, pokud bude tato e-mailová adresa nalezena v systému.',
-    'reset_password_success' => 'Vaše heslo bylo úspěšně obnoveno.',
+    'reset_password_success' => 'Vaše heslo bylo obnoveno.',
     'email_reset_subject' => 'Obnovit heslo do :appName',
     'email_reset_text' => 'Tento e-mail jste obdrželi, protože jsme obdrželi žádost o obnovení hesla k vašemu účtu.',
     'email_reset_not_requested' => 'Pokud jste o obnovení hesla nežádali, není vyžadována žádná další akce.',
@@ -66,12 +66,47 @@ return [
     'email_not_confirmed_resend_button' => 'Znovu odeslat potvrzovací e-mail',
 
     // User Invite
-    'user_invite_email_subject' => 'Byli jste pozváni přidat se do :appName!',
+    'user_invite_email_subject' => 'Byli jste pozváni do :appName!',
     'user_invite_email_greeting' => 'Byl pro vás vytvořen účet na :appName.',
     'user_invite_email_text' => 'Klikněte na níže uvedené tlačítko pro nastavení hesla k účtu a získání přístupu:',
     'user_invite_email_action' => 'Nastavit heslo k účtu',
     'user_invite_page_welcome' => 'Vítejte v :appName!',
-    'user_invite_page_text' => 'Pro dokončení vašeho účtu a získání přístupu musíte nastavit heslo, které bude použito k přihlášení do :appName při budoucích návštěvách.',
+    'user_invite_page_text' => 'Pro dokončení vašeho účtu a získání přístupu musíte nastavit heslo, které bude použito k přihlášení do :appName při dalších návštěvách.',
     'user_invite_page_confirm_button' => 'Potvrdit heslo',
-    'user_invite_success' => 'Heslo nastaveno, nyní máte přístup k :appName!'
+    'user_invite_success' => 'Heslo nastaveno, nyní máte přístup k :appName!',
+
+    // Multi-factor Authentication
+    'mfa_setup' => 'Setup Multi-Factor Authentication',
+    'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
+    'mfa_setup_configured' => 'Already configured',
+    'mfa_setup_reconfigure' => 'Reconfigure',
+    'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?',
+    'mfa_setup_action' => 'Setup',
+    'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.',
+    'mfa_option_totp_title' => 'Mobile App',
+    'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
+    'mfa_option_backup_codes_title' => 'Backup Codes',
+    'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.',
+    'mfa_gen_confirm_and_enable' => 'Confirm and Enable',
+    'mfa_gen_backup_codes_title' => 'Backup Codes Setup',
+    'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.',
+    'mfa_gen_backup_codes_download' => 'Download Codes',
+    'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once',
+    'mfa_gen_totp_title' => 'Mobile App Setup',
+    'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
+    'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.',
+    'mfa_gen_totp_verify_setup' => 'Verify Setup',
+    'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:',
+    'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here',
+    'mfa_verify_access' => 'Verify Access',
+    'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.',
+    'mfa_verify_no_methods' => 'No Methods Configured',
+    'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.',
+    'mfa_verify_use_totp' => 'Verify using a mobile app',
+    'mfa_verify_use_backup_codes' => 'Verify using a backup code',
+    'mfa_verify_backup_code' => 'Backup Code',
+    'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:',
+    'mfa_verify_backup_code_enter_here' => 'Enter backup code here',
+    'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:',
+    'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.',
 ];
\ No newline at end of file
index b76fa32e4aeb9ff9fbf6f9f03305192a968cdfc6..88821ecf9b9616962296d003f873e137cccc49fa 100644 (file)
@@ -39,11 +39,12 @@ return [
     'reset' => 'Obnovit',
     'remove' => 'Odebrat',
     'add' => 'Přidat',
+    'configure' => 'Configure',
     'fullscreen' => 'Celá obrazovka',
-    'favourite' => 'Favourite',
-    'unfavourite' => 'Unfavourite',
-    'next' => 'Next',
-    'previous' => 'Previous',
+    'favourite' => 'Přidat do oblíbených',
+    'unfavourite' => 'Odebrat z oblíbených',
+    'next' => 'Další',
+    'previous' => 'Předchozí',
 
     // Sort Options
     'sort_options' => 'Možnosti řazení',
@@ -51,7 +52,7 @@ return [
     'sort_ascending' => 'Řadit vzestupně',
     'sort_descending' => 'Řadit sestupně',
     'sort_name' => 'Název',
-    'sort_default' => 'Default',
+    'sort_default' => 'Výchozí',
     'sort_created_at' => 'Datum vytvoření',
     'sort_updated_at' => 'Datum aktualizace',
 
@@ -60,6 +61,7 @@ return [
     'no_activity' => 'Žádná aktivita k zobrazení',
     'no_items' => 'Žádné položky k dispozici',
     'back_to_top' => 'Zpět na začátek',
+    'skip_to_main_content' => 'Přeskočit na hlavní obsah',
     'toggle_details' => 'Přepnout podrobnosti',
     'toggle_thumbnails' => 'Přepnout náhledy',
     'details' => 'Podrobnosti',
@@ -69,7 +71,7 @@ return [
     'breadcrumb' => 'Drobečková navigace',
 
     // Header
-    'header_menu_expand' => 'Expand Header Menu',
+    'header_menu_expand' => 'Rozbalit menu v záhlaví',
     'profile_menu' => 'Nabídka profilu',
     'view_profile' => 'Zobrazit profil',
     'edit_profile' => 'Upravit profil',
@@ -78,9 +80,9 @@ return [
 
     // Layout tabs
     'tab_info' => 'Informace',
-    'tab_info_label' => 'Tab: Show Secondary Information',
+    'tab_info_label' => 'Tab: Zobrazit podružné informace',
     'tab_content' => 'Obsah',
-    'tab_content_label' => 'Tab: Show Primary Content',
+    'tab_content_label' => 'Tab: Zobrazit hlavní obsah',
 
     // Email Content
     'email_action_help' => 'Pokud se vám nedaří kliknout na tlačítko „:actionText“, zkopírujte a vložte níže uvedenou URL do vašeho webového prohlížeče:',
@@ -88,6 +90,6 @@ return [
 
     // Footer Link Options
     // Not directly used but available for convenience to users.
-    'privacy_policy' => 'Privacy Policy',
-    'terms_of_service' => 'Terms of Service',
+    'privacy_policy' => 'Zásady ochrany osobních údajů',
+    'terms_of_service' => 'Podmínky služby',
 ];
index e06462b0021c9f9a493cc6ef52cbae494e97caf7..20df62e36066092af0f12f53691ccd55a1f2a8bb 100644 (file)
@@ -16,13 +16,13 @@ return [
     'image_image_name' => 'Název obrázku',
     'image_delete_used' => 'Tento obrázek je použit na níže uvedených stránkách.',
     'image_delete_confirm_text' => 'Opravdu chcete odstranit tento obrázek?',
-    'image_select_image' => 'Vyberte obrázek',
+    'image_select_image' => 'Zvolte obrázek',
     'image_dropzone' => 'Přetáhněte obrázky nebo klikněte sem pro nahrání',
     'images_deleted' => 'Obrázky odstraněny',
     'image_preview' => 'Náhled obrázku',
-    'image_upload_success' => 'Obrázek byl úspěšně nahrán',
-    'image_update_success' => 'Podrobnosti o obrázku byly úspěšně aktualizovány',
-    'image_delete_success' => 'Obrázek byl úspěšně odstraněn',
+    'image_upload_success' => 'Obrázek byl nahrán',
+    'image_update_success' => 'Podrobnosti o obrázku byly aktualizovány',
+    'image_delete_success' => 'Obrázek byl odstraněn',
     'image_upload_remove' => 'Odebrat',
 
     // Code Editor
index 1a843c93dc3935cf8e466ae02f22c5d1adb10513..97823a708cc5e268d4758b6dc1a298f90a5c6cc7 100644 (file)
@@ -15,38 +15,39 @@ return [
     'recently_update' => 'Nedávno aktualizované',
     'recently_viewed' => 'Nedávno zobrazené',
     'recent_activity' => 'Nedávné aktivity',
-    'create_now' => 'Vytvořte ji nyní',
+    'create_now' => 'Vytvořit nyní',
     'revisions' => 'Revize',
     'meta_revision' => 'Revize č. :revisionCount',
     'meta_created' => 'Vytvořeno :timeLength',
     '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',
+    'meta_owned_name' => 'Vlastník :user',
     'entity_select' => 'Výběr entity',
     'images' => 'Obrázky',
     'my_recent_drafts' => 'Mé nedávné koncepty',
     'my_recently_viewed' => 'Mé nedávno zobrazené',
-    'my_most_viewed_favourites' => 'My Most Viewed Favourites',
-    'my_favourites' => 'My Favourites',
+    'my_most_viewed_favourites' => 'Mé nejčastěji zobrazené oblíbené',
+    'my_favourites' => 'Mé oblíbené',
     'no_pages_viewed' => 'Nezobrazili jste žádné stránky',
-    'no_pages_recently_created' => 'Nedávno nebyly vytvořeny žádné stránky',
-    'no_pages_recently_updated' => 'Nedávno nebyly aktualizovány žádné stránky',
+    'no_pages_recently_created' => 'Žádné nedávno vytvořené stránky',
+    'no_pages_recently_updated' => 'Žádné nedávno aktualizované stránky',
     'export' => 'Exportovat',
-    'export_html' => 'Konsolidovaný webový soubor',
-    'export_pdf' => 'Soubor PDF',
+    'export_html' => 'HTML stránka s celým obsahem',
+    'export_pdf' => 'PDF dokument',
     'export_text' => 'Textový soubor',
+    'export_md' => 'Markdown',
 
     // Permissions and restrictions
     'permissions' => 'Oprávnění',
     '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',
+    'permissions_owner' => 'Vlastník',
 
     // Search
     'search_results' => 'Výsledky hledání',
-    'search_total_results_found' => 'Nalezen :count výsledek|Nalezeny :count výsledky|Nalezeny :count výsledky|Nalezeny :count výsledky|Nalezeno :count výsledků',
+    'search_total_results_found' => '{1}Nalezen :count výsledek|[2,4]Nalezeny :count výsledky|[5,*]Nalezeno :count výsledků',
     'search_clear' => 'Vymazat hledání',
     'search_no_pages' => 'Tomuto hledání neodpovídají žádné stránky',
     'search_for_term' => 'Hledat :term',
@@ -62,7 +63,7 @@ return [
     'search_permissions_set' => 'Sada oprávnění',
     'search_created_by_me' => 'Vytvořeno mnou',
     'search_updated_by_me' => 'Aktualizováno mnou',
-    'search_owned_by_me' => 'Owned by me',
+    'search_owned_by_me' => 'Patřící mně',
     'search_date_options' => 'Možnosti data',
     'search_updated_before' => 'Aktualizováno před',
     'search_updated_after' => 'Aktualizováno po',
@@ -82,25 +83,26 @@ return [
     'shelves_new' => 'Nové knihovny',
     'shelves_new_action' => 'Nová Knihovna',
     'shelves_popular_empty' => 'Nejpopulárnější knihovny se objeví zde.',
-    'shelves_new_empty' => 'Zde se objeví nejnověji vytvořené knihovny.',
+    'shelves_new_empty' => 'Zde se zobrazí nejnověji vytvořené knihovny.',
     'shelves_save' => 'Uložit knihovnu',
     'shelves_books' => 'Knihy v této knihovně',
     'shelves_add_books' => 'Přidat knihy do knihovny',
-    'shelves_drag_books' => 'Knihu přidáte jejím přetažením sem.',
+    'shelves_drag_books' => 'Knihu přidáte jejím přetažením sem',
     'shelves_empty_contents' => 'Tato knihovna neobsahuje žádné knihy',
-    'shelves_edit_and_assign' => 'Pro přidáni knih do knihovny stiskněte úprvy.',
+    'shelves_edit_and_assign' => 'Upravit knihovnu a přiřadit knihy',
     'shelves_edit_named' => 'Upravit knihovnu :name',
     'shelves_edit' => 'Upravit knihovnu',
     'shelves_delete' => 'Odstranit knihovnu',
     'shelves_delete_named' => 'Odstranit knihovnu :name',
-    'shelves_delete_explain' => "Toto odstraní knihovnu s názvem ‚:name‘. Obsažené knihy nebudou odstraněny.",
+    'shelves_delete_explain' => "Toto odstraní knihovnu ‚:name‘. Vložené knihy nebudou odstraněny.",
     'shelves_delete_confirmation' => 'Opravdu chcete odstranit tuto knihovnu?',
     'shelves_permissions' => 'Oprávnění knihovny',
     'shelves_permissions_updated' => 'Oprávnění knihovny byla aktualizována',
-    'shelves_permissions_active' => 'Oprávnění knihovny jsou aktivní',
+    'shelves_permissions_active' => 'Oprávnění knihovny byla aktivována',
+    'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',
     'shelves_copy_permissions_to_books' => 'Kopírovat oprávnění na knihy',
     'shelves_copy_permissions' => 'Kopírovat oprávnění',
-    'shelves_copy_permissions_explain' => 'Toto použije aktuální nastavení oprávnění této knihovny na všechny knihy v ní obsažené. Před aktivací se ujistěte, že byly uloženy všechny změny oprávnění této knihovny.',
+    'shelves_copy_permissions_explain' => 'Toto použije aktuální nastavení oprávnění knihovny na všechny knihy v ní obsažené. Před aktivací se ujistěte, že byly uloženy všechny změny oprávnění této knihovny.',
     'shelves_copy_permission_success' => 'Oprávnění knihovny byla zkopírována na :count knih',
 
     // Books
@@ -112,24 +114,24 @@ return [
     'books_recent' => 'Nedávné knihy',
     'books_new' => 'Nové knihy',
     'books_new_action' => 'Nová kniha',
-    'books_popular_empty' => 'Zde se objeví nejoblíbenější knihy.',
-    'books_new_empty' => 'Zde se objeví nejnověji vytvořené knihy.',
+    'books_popular_empty' => 'Zde se zobrazí nejoblíbenější knihy.',
+    'books_new_empty' => 'Zde se zobrazí nejnověji vytvořené knihy.',
     'books_create' => 'Vytvořit novou knihu',
     'books_delete' => 'Odstranit knihu',
     'books_delete_named' => 'Odstranit knihu :bookName',
-    'books_delete_explain' => 'Toto odstraní knihu s názvem ‚:bookName‘. Všechny stránky a kapitoly budou odebrány.',
+    'books_delete_explain' => 'Toto odstraní knihu ‚:bookName‘. Všechny stránky a kapitoly v této knize budou také odstraněny.',
     'books_delete_confirmation' => 'Opravdu chcete odstranit tuto knihu?',
     'books_edit' => 'Upravit knihu',
     'books_edit_named' => 'Upravit knihu :bookName',
     'books_form_book_name' => 'Název knihy',
     'books_save' => 'Uložit knihu',
     'books_permissions' => 'Oprávnění knihy',
-    'books_permissions_updated' => 'Oprávnění knihy aktualizována',
-    'books_empty_contents' => 'Pro tuto knihu nebyly vytvořeny žádné stránky nebo kapitoly.',
+    'books_permissions_updated' => 'Oprávnění knihy byla aktualizována',
+    'books_empty_contents' => 'Pro tuto knihu nebyly vytvořeny žádné stránky ani kapitoly.',
     'books_empty_create_page' => 'Vytvořit novou stránku',
     'books_empty_sort_current_book' => 'Seřadit aktuální knihu',
     'books_empty_add_chapter' => 'Přidat kapitolu',
-    'books_permissions_active' => 'Oprávnění knihy jsou aktivní',
+    'books_permissions_active' => 'Oprávnění knihy byla aktivována',
     'books_search_this' => 'Prohledat tuto knihu',
     'books_navigation' => 'Navigace knihy',
     'books_sort' => 'Seřadit obsah knihy',
@@ -137,8 +139,8 @@ return [
     'books_sort_name' => 'Seřadit podle názvu',
     'books_sort_created' => 'Seřadit podle data vytvoření',
     'books_sort_updated' => 'Seřadit podle data aktualizace',
-    'books_sort_chapters_first' => 'Kapitoly první',
-    'books_sort_chapters_last' => 'Kapitoly poslední',
+    'books_sort_chapters_first' => 'Kapitoly jako první',
+    'books_sort_chapters_last' => 'Kapitoly jako poslední',
     'books_sort_show_other' => 'Zobrazit ostatní knihy',
     'books_sort_save' => 'Uložit nové pořadí',
 
@@ -149,20 +151,20 @@ return [
     'chapters_popular' => 'Populární kapitoly',
     'chapters_new' => 'Nová kapitola',
     'chapters_create' => 'Vytvořit novou kapitolu',
-    'chapters_delete' => 'Smazat kapitolu',
-    'chapters_delete_named' => 'Smazat kapitolu :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' => 'Opravdu chcete tuto kapitolu smazat?',
+    'chapters_delete' => 'Odstranit kapitolu',
+    'chapters_delete_named' => 'Odstranit kapitolu :chapterName',
+    'chapters_delete_explain' => 'Toto odstraní kapitolu ‚:chapterName‘. Všechny stránky v této kapitole budou také odstraněny.',
+    'chapters_delete_confirm' => 'Opravdu chcete odstranit tuto kapitolu?',
     'chapters_edit' => 'Upravit kapitolu',
     'chapters_edit_named' => 'Upravit kapitolu :chapterName',
     'chapters_save' => 'Uložit kapitolu',
     'chapters_move' => 'Přesunout kapitolu',
     'chapters_move_named' => 'Přesunout kapitolu :chapterName',
     'chapter_move_success' => 'Kapitola přesunuta do knihy :bookName',
-    'chapters_permissions' => 'Práva kapitoly',
+    'chapters_permissions' => 'Oprávnění kapitoly',
     'chapters_empty' => 'Tato kapitola neobsahuje žádné stránky',
-    'chapters_permissions_active' => 'Účinná práva kapitoly',
-    'chapters_permissions_success' => 'Práva kapitoly aktualizována',
+    'chapters_permissions_active' => 'Oprávnění kapitoly byla aktivována',
+    'chapters_permissions_success' => 'Oprávnění kapitoly byla aktualizována',
     'chapters_search_this' => 'Prohledat tuto kapitolu',
 
     // Pages
@@ -173,9 +175,9 @@ return [
     'pages_new' => 'Nová stránka',
     'pages_attachments' => 'Přílohy',
     'pages_navigation' => 'Obsah stránky',
-    'pages_delete' => 'Smazat stránku',
-    'pages_delete_named' => 'Smazat stránku :pageName',
-    'pages_delete_draft_named' => 'Smazat koncept stránky :pageName',
+    'pages_delete' => 'Odstranit stránku',
+    'pages_delete_named' => 'Odstranit stránku :pageName',
+    'pages_delete_draft_named' => 'Odstranit koncept stránky :pageName',
     'pages_delete_draft' => 'Odstranit koncept stránky',
     'pages_delete_success' => 'Stránka odstraněna',
     'pages_delete_draft_success' => 'Koncept stránky odstraněn',
@@ -206,17 +208,17 @@ return [
     'pages_move_success' => 'Stránka přesunuta do ":parentName"',
     'pages_copy' => 'Kopírovat stránku',
     'pages_copy_desination' => 'Cíl kopírování',
-    'pages_copy_success' => 'Stránka byla úspěšně zkopírována',
+    'pages_copy_success' => 'Stránka byla zkopírována',
     'pages_permissions' => 'Oprávnění stránky',
-    'pages_permissions_success' => 'Oprávnění stránky aktualizována',
+    'pages_permissions_success' => 'Oprávnění stránky byla aktualizována',
     'pages_revision' => 'Revize',
     '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_revision_restored_from' => 'Obnoveno z #:id; :summary',
     'pages_revisions_created_by' => 'Vytvořeno uživatelem',
     'pages_revisions_date' => 'Datum revize',
-    'pages_revisions_number' => 'Č.',
+    'pages_revisions_number' => 'Č. ',
     'pages_revisions_numbered' => 'Revize č. :id',
     'pages_revisions_numbered_changes' => 'Změny revize č. :id',
     'pages_revisions_changelog' => 'Protokol změn',
@@ -227,7 +229,7 @@ return [
     'pages_revisions_none' => 'Tato stránka nemá žádné revize',
     'pages_copy_link' => 'Kopírovat odkaz',
     'pages_edit_content_link' => 'Upravit obsah',
-    'pages_permissions_active' => 'Účinná práva stránky',
+    'pages_permissions_active' => 'Oprávnění stránky byla aktivována',
     'pages_initial_revision' => 'První vydání',
     'pages_initial_name' => 'Nová stránka',
     'pages_editing_draft_notification' => 'Právě upravujete koncept, který byl uložen před :timeDiff.',
@@ -263,8 +265,8 @@ return [
     'attachments_link' => 'Připojit odkaz',
     'attachments_set_link' => 'Nastavit odkaz',
     'attachments_delete' => 'Jste si jisti, že chcete odstranit tuto přílohu?',
-    'attachments_dropzone' => 'Přetáhněte sem soubory myší nebo sem kliknětě pro vybrání souboru.',
-    'attachments_no_files' => 'Žádné soubory nebyli nahrány',
+    'attachments_dropzone' => 'Přetáhněte sem soubory myší nebo sem klikněte pro vybrání souboru',
+    'attachments_no_files' => 'Žádné soubory nebyly nahrány',
     'attachments_explain_link' => 'Můžete pouze připojit odkaz pokud nechcete nahrávat soubor přímo. Může to být odkaz na jinou stránku nebo na soubor v cloudu.',
     'attachments_link_name' => 'Název odkazu',
     'attachment_link' => 'Odkaz na přílohu',
@@ -274,13 +276,13 @@ return [
     'attachments_insert_link' => 'Přidat odkaz na přílohu do stránky',
     'attachments_edit_file' => 'Upravit soubor',
     'attachments_edit_file_name' => 'Název souboru',
-    'attachments_edit_drop_upload' => 'Přetáhněte sem soubor myší nebo klikněte pro nahrání nového a následné přepsání starého.',
+    'attachments_edit_drop_upload' => 'Přetáhněte sem soubor myší nebo klikněte pro nahrání nového souboru a následné přepsání starého',
     'attachments_order_updated' => 'Pořadí příloh aktualizováno',
     'attachments_updated_success' => 'Podrobnosti příloh aktualizovány',
-    'attachments_deleted' => 'Příloha byla smazána',
-    'attachments_file_uploaded' => 'Soubor byl úspěšně nahrán',
-    'attachments_file_updated' => 'Soubor byl úspěšně aktualizován',
-    'attachments_link_attached' => 'Odkaz úspěšně přiložen ke stránce',
+    'attachments_deleted' => 'Příloha byla odstraněna',
+    'attachments_file_uploaded' => 'Soubor byl nahrán',
+    'attachments_file_updated' => 'Soubor byl aktualizován',
+    'attachments_link_attached' => 'Odkaz byl přiložen ke stránce',
     'templates' => 'Šablony',
     'templates_set_as_template' => 'Tato stránka je šablona',
     'templates_explain_set_as_template' => 'Tuto stránku můžete nastavit jako šablonu, aby byl její obsah využit při vytváření dalších stránek. Ostatní uživatelé budou moci použít tuto šablonu, pokud mají oprávnění k zobrazení této stránky.',
@@ -300,7 +302,7 @@ return [
     'comment' => 'Komentář',
     'comments' => 'Komentáře',
     'comment_add' => 'Přidat komentář',
-    'comment_placeholder' => 'Zanechat komentář zde',
+    'comment_placeholder' => 'Zde zadejte komentář',
     'comment_count' => '{0} Bez komentářů|{1} 1 komentář|[2,4] :count komentáře|[5,*] :count komentářů',
     'comment_save' => 'Uložit komentář',
     'comment_saving' => 'Ukládání komentáře...',
@@ -308,15 +310,15 @@ return [
     'comment_new' => 'Nový komentář',
     'comment_created' => 'komentováno :createDiff',
     'comment_updated' => 'Aktualizováno :updateDiff uživatelem :username',
-    'comment_deleted_success' => 'Komentář smazán',
+    'comment_deleted_success' => 'Komentář odstraněn',
     'comment_created_success' => 'Komentář přidán',
     'comment_updated_success' => 'Komentář aktualizován',
-    'comment_delete_confirm' => 'Opravdu chcete smazat tento komentář?',
+    'comment_delete_confirm' => 'Opravdu chcete odstranit tento komentář?',
     'comment_in_reply_to' => 'Odpověď na :commentId',
 
     // Revision
-    'revision_delete_confirm' => 'Opravdu chcete smazat tuto revizi?',
+    'revision_delete_confirm' => 'Opravdu chcete odstranit tuto revizi?',
     'revision_restore_confirm' => 'Jste si jisti, že chcete obnovit tuto revizi? Aktuální obsah stránky bude nahrazen.',
-    'revision_delete_success' => 'Revize smazána',
-    'revision_cannot_delete_latest' => 'Nelze smazat poslední revizi.'
+    'revision_delete_success' => 'Revize odstraněna',
+    'revision_cannot_delete_latest' => 'Nelze odstranit poslední revizi.'
 ];
index 102a1f88b4f09301124a254bcad0d3efb0fcd9dd..c948cafd1095aa138a9ffc8e9b9265517a38a6e2 100644 (file)
@@ -5,14 +5,14 @@
 return [
 
     // Permissions
-    'permission' => 'Nemáte povolení přistupovat na dotazovanou stránku.',
+    'permission' => 'Nemáte povolení přistupovat na požadovanou stránku.',
     'permissionJson' => 'Nemáte povolení k provedení požadované akce.',
 
     // Auth
     'error_user_exists_different_creds' => 'Uživatel s emailem :email již existuje ale s jinými přihlašovacími údaji.',
     'email_already_confirmed' => 'Emailová adresa již byla potvrzena. Zkuste se přihlásit.',
     'email_confirmation_invalid' => 'Tento potvrzovací odkaz již neplatí nebo už byl použit. Zkuste prosím registraci znovu.',
-    'email_confirmation_expired' => 'Potvrzovací odkaz už neplatí, email s novým odkazem už byl poslán.',
+    'email_confirmation_expired' => 'Tento potvrzovací odkaz již neplatí, byl Vám odeslán nový potvrzovací e-mail.',
     'email_confirmation_awaiting' => 'E-mailová adresa pro používaný účet musí být potvrzena',
     'ldap_fail_anonymous' => 'Přístup k adresáři LDAP jako anonymní uživatel (anonymous bind) selhal',
     'ldap_fail_authed' => 'Přístup k adresáři LDAP pomocí zadaného jména (dn) a hesla selhal',
@@ -50,7 +50,7 @@ return [
 
     // Pages
     'page_draft_autosave_fail' => 'Nepovedlo se uložit koncept. Než stránku uložíte, ujistěte se, že jste připojeni k internetu.',
-    'page_custom_home_deletion' => 'Nelze smazat tuto stránku, protože je nastavena jako uvítací stránka.',
+    'page_custom_home_deletion' => 'Nelze odstranit tuto stránku, protože je nastavena jako uvítací stránka',
 
     // Entities
     'entity_not_found' => 'Prvek nenalezen',
@@ -60,39 +60,39 @@ return [
     'chapter_not_found' => 'Kapitola nenalezena',
     'selected_book_not_found' => 'Vybraná kniha nebyla nalezena',
     'selected_book_chapter_not_found' => 'Zvolená kniha nebo kapitola nebyla nalezena',
-    'guests_cannot_save_drafts' => 'Návštěvníci z řad veřejnosti nemohou ukládat koncepty.',
+    'guests_cannot_save_drafts' => 'Nepřihlášení návštěvníci nemohou ukládat koncepty',
 
     // Users
-    'users_cannot_delete_only_admin' => 'Nemůžete smazat posledního administrátora',
-    'users_cannot_delete_guest' => 'Uživatele host není možno smazat',
+    'users_cannot_delete_only_admin' => 'Nemůžete odstranit posledního administrátora',
+    'users_cannot_delete_guest' => 'Uživatele Host není možno odstranit',
 
     // Roles
     'role_cannot_be_edited' => 'Tuto roli nelze editovat',
-    'role_system_cannot_be_deleted' => 'Toto je systémová role a nelze jí smazat.',
-    'role_registration_default_cannot_delete' => 'Tuto roli nelze smazat dokud je nastavená jako výchozí role pro registraci nových uživatelů.',
+    'role_system_cannot_be_deleted' => 'Toto je systémová role a nelze jí odstranit',
+    'role_registration_default_cannot_delete' => 'Tuto roli nelze odstranit dokud je nastavená jako výchozí role pro registraci nových uživatelů',
     'role_cannot_remove_only_admin' => 'Tento uživatel má roli administrátora. Přiřaďte roli administrátora někomu jinému než jí odeberete zde.',
 
     // Comments
-    'comment_list' => 'Při dotahování komentářů nastala chyba.',
+    'comment_list' => 'Při načítání komentářů nastala chyba.',
     'cannot_add_comment_to_draft' => 'Nemůžete přidávat komentáře ke konceptu.',
     'comment_add' => 'Při přidávání / aktualizaci komentáře nastala chyba.',
-    'comment_delete' => 'Při mazání komentáře nastala chyba.',
+    'comment_delete' => 'Při odstraňování komentáře nastala chyba.',
     'empty_comment' => 'Nemůžete přidat prázdný komentář.',
 
     // Error pages
     '404_page_not_found' => 'Stránka nenalezena',
-    'sorry_page_not_found' => 'Omlouváme se, ale stránka, kterou hledáte nebyla nalezena.',
+    'sorry_page_not_found' => 'Omlouváme se, ale stránka, kterou hledáte, nebyla nalezena.',
     'sorry_page_not_found_permission_warning' => 'Pokud očekáváte, že by stránka měla existovat, možná jen nemáte oprávnění pro její zobrazení.',
-    'image_not_found' => 'Image Not Found',
-    'image_not_found_subtitle' => 'Sorry, The image file you were looking for could not be found.',
-    'image_not_found_details' => 'If you expected this image to exist it might have been deleted.',
+    'image_not_found' => 'Obrázek nenalezen',
+    'image_not_found_subtitle' => 'Omlouváme se, ale obrázek, který hledáte, nebyl nalezen.',
+    'image_not_found_details' => 'Pokud očekáváte, že by obrázel měl existovat, tak byl zřejmě již odstraněn.',
     'return_home' => 'Návrat domů',
     'error_occurred' => 'Nastala chyba',
     'app_down' => ':appName je momentálně vypnutá',
-    'back_soon' => 'Brzy naběhne.',
+    'back_soon' => 'Brzy bude opět v provozu.',
 
     // API errors
-    'api_no_authorization_found' => 'V požadavku nebyla nalezen žádný autorizační token',
+    'api_no_authorization_found' => 'V požadavku nebyl nalezen žádný autorizační token',
     'api_bad_authorization_format' => 'V požadavku byl nalezen autorizační token, ale jeho formát se zdá být chybný',
     'api_user_token_not_found' => 'Pro zadaný autorizační token nebyl nalezen žádný odpovídající API token',
     'api_incorrect_token_secret' => 'Poskytnutý Token Secret neodpovídá použitému API tokenu',
index cc5669d4825d6e4892c20e9b149d2879c6bdcbdd..40b12b6070fe64cc7fa4f623ab83abadc3b13c4f 100644 (file)
@@ -6,10 +6,10 @@
  */
 return [
 
-    'password' => 'Heslo musí mít alespoň osm znaků a musí odpovídat potvrzení.',
+    'password' => 'Heslo musí mít alespoň osm znaků a musí odpovídat potvrzení hesla.',
     'user' => "Nemůžeme nalézt uživatele s touto e-mailovou adresou.",
-    'token' => 'Token pro obnovení hesla je neplatný pro tuto e-mailovou adresu.',
-    'sent' => 'Poslali jsme vám e-mail s odkazem pro obnovení hesla!',
+    'token' => 'Token pro obnovení hesla není platný pro tuto e-mailovou adresu.',
+    'sent' => 'Poslali jsme Vám e-mail s odkazem pro obnovení hesla!',
     'reset' => 'Vaše heslo bylo obnoveno!',
 
 ];
index 86b94956f2290d95c4cb2879650eb3a60fe83619..36d8bc0ecc861c654a07f462483a21856bf3fb74 100644 (file)
@@ -35,13 +35,13 @@ return [
     'app_primary_color' => 'Hlavní barva aplikace',
     'app_primary_color_desc' => 'Nastaví hlavní barvu aplikace včetně panelů, tlačítek a odkazů.',
     'app_homepage' => 'Úvodní stránka aplikace',
-    'app_homepage_desc' => 'Vyberte si zobrazení, které se použije jako úvodní stránka. U zvolených stránek bude ignorováno jejich oprávnění.',
+    'app_homepage_desc' => 'Zvolte si zobrazení, které se použije jako úvodní stránka. U zvolených stránek bude ignorováno jejich oprávnění.',
     'app_homepage_select' => 'Zvolte stránku',
-    '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_footer_links' => 'Odkazy v zápatí',
+    'app_footer_links_desc' => 'Přidejte odkazy, které se zobrazí v zápatí webu. Ty se zobrazí ve spodní části většiny stránek, včetně těch, které nevyžadují přihlášení. K použití překladů definovaných systémem můžete použít štítek „trans::<key>“. Například: Použití „trans::common.privacy_policy“ přeloží text na „Zásady ochrany osobních údajů“ a „trans::common.terms_of_service“ poskytne přeložený text „Podmínky služby“.',
+    'app_footer_links_label' => 'Text odkazu',
+    'app_footer_links_url' => 'URL odkazu',
+    'app_footer_links_add' => 'Přidat odkaz do zápatí',
     'app_disable_comments' => 'Vypnutí komentářů',
     'app_disable_comments_toggle' => 'Vypnout komentáře',
     'app_disable_comments_desc' => 'Vypne komentáře napříč všemi stránkami. <br> Existující komentáře se přestanou zobrazovat.',
@@ -73,10 +73,10 @@ 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_delete_images_only_in_revisions' => 'Also delete images that only exist in old page revisions',
+    'maint_delete_images_only_in_revisions' => 'Odstranit i obrázky, které se vyskytují pouze ve starých revizích stránky',
     '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.',
+    'maint_image_cleanup_warning' => 'Nalezeno :count potenciálně nepoužitých obrázků. Jste si jisti, že je chcete odstranit?',
+    'maint_image_cleanup_success' => 'Nalezeno :count potenciálně nepoužitých obrázků a všechny byly odstraněny!',
     'maint_image_cleanup_nothing_found' => 'Žádné potenciálně nepoužité obrázky nebyly nalezeny. Nic nebylo smazáno.',
     'maint_send_test_email' => 'Odeslat zkušební e-mail',
     'maint_send_test_email_desc' => 'Toto pošle zkušební e-mail na vaši e-mailovou adresu uvedenou ve vašem profilu.',
@@ -85,27 +85,29 @@ 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',
+    'maint_recycle_bin_desc' => 'Odstraněné knihovny, knihy, kapitoly a stránky se přesouvají do Koše, aby je bylo možné obnovit nebo trvale smazat. Starší položky v koši mohou být po čase automaticky odstraněny v závislosti na konfiguraci systému.',
+    'maint_recycle_bin_open' => 'Otevřít Koš',
 
     // 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.',
+    'recycle_bin' => 'Koš',
+    'recycle_bin_desc' => 'Zde můžete obnovit položky, které byly odstraněny, nebo zvolit jejich trvalé odstranění ze systému. Tento seznam je nefiltrovaný, na rozdíl od podobných seznamů aktivit v systému, kde jsou použity filtry podle oprávnění.',
+    'recycle_bin_deleted_item' => 'Odstraněná položka',
+    'recycle_bin_deleted_parent' => 'Nadřazená položka',
+    'recycle_bin_deleted_by' => 'Odstranil/a',
+    'recycle_bin_deleted_at' => 'Čas odstranění',
+    'recycle_bin_permanently_delete' => 'Trvale odstranit',
+    'recycle_bin_restore' => 'Obnovit',
+    'recycle_bin_contents_empty' => 'Koš je nyní prázdný',
+    'recycle_bin_empty' => 'Vysypat Koš',
+    'recycle_bin_empty_confirm' => 'Toto trvale odstraní všechny položky v Koši včetně obsahu vloženého v každé položce. Opravdu chcete vysypat Koš?',
+    'recycle_bin_destroy_confirm' => 'Tato akce trvale odstraní ze systému tuto položku spolu s veškerým vloženým obsahem a tento obsah nebudete moci obnovit. Opravdu chcete tuto položku trvale odstranit?',
+    'recycle_bin_destroy_list' => 'Položky k trvalému odstranění',
+    'recycle_bin_restore_list' => 'Položky k obnovení',
+    'recycle_bin_restore_confirm' => 'Tato akce obnoví odstraněnou položku včetně veškerého vloženého obsahu do původního umístění. Pokud bylo původní umístění od té doby odstraněno a nyní je v Koši, bude také nutné obnovit nadřazenou položku.',
+    'recycle_bin_restore_deleted_parent' => 'Nadřazená položka této položky byla také odstraněna. Ty zůstanou odstraněny, dokud nebude obnoven i nadřazený objekt.',
+    'recycle_bin_restore_parent' => 'Obnovit nadřazenu položku',
+    'recycle_bin_destroy_notification' => 'Celkem odstraněno :count položek z Koše.',
+    'recycle_bin_restore_notification' => 'Celkem obnoveno :count položek z Koše.',
 
     // Audit Log
     'audit' => 'Protokol auditu',
@@ -116,7 +118,8 @@ return [
     'audit_deleted_item_name' => 'Jméno: :name',
     'audit_table_user' => 'Uživatel',
     'audit_table_event' => 'Událost',
-    'audit_table_related' => 'Related Item or Detail',
+    'audit_table_related' => 'Související položka nebo detail',
+    'audit_table_ip' => 'IP Address',
     'audit_table_date' => 'Datum aktivity',
     'audit_date_from' => 'Časový rozsah od',
     'audit_date_to' => 'Časový rozsah do',
@@ -125,17 +128,18 @@ return [
     'roles' => 'Role',
     'role_user_roles' => 'Uživatelské role',
     'role_create' => 'Vytvořit novou roli',
-    'role_create_success' => 'Role byla úspěšně vytvořena',
-    'role_delete' => 'Smazat roli',
-    'role_delete_confirm' => 'Role \':roleName\' bude smazána.',
+    'role_create_success' => 'Role byla vytvořena',
+    'role_delete' => 'Odstranit roli',
+    'role_delete_confirm' => 'Role \':roleName\' bude odstraněna.',
     'role_delete_users_assigned' => 'Role je přiřazena :userCount uživatelům. Pokud jim chcete náhradou přidělit jinou roli, zvolte jednu z následujících.',
     'role_delete_no_migration' => "Nepřiřazovat uživatelům náhradní roli",
-    'role_delete_sure' => 'Opravdu chcete tuto roli smazat?',
-    'role_delete_success' => 'Role byla úspěšně smazána',
+    'role_delete_sure' => 'Opravdu chcete tuto roli odstranit?',
+    'role_delete_success' => 'Role byla odstraněna',
     'role_edit' => 'Upravit roli',
     'role_details' => 'Detaily role',
     'role_name' => 'Název role',
     'role_desc' => 'Stručný popis role',
+    'role_mfa_enforced' => 'Requires Multi-Factor Authentication',
     'role_external_auth_id' => 'Přihlašovací identifikátory třetích stran',
     'role_system' => 'Systémová oprávnění',
     'role_manage_users' => 'Správa uživatelů',
@@ -145,15 +149,16 @@ return [
     'role_manage_page_templates' => 'Správa šablon stránek',
     'role_access_api' => 'Přístup k systémovému API',
     'role_manage_settings' => 'Správa nastavení aplikace',
+    'role_export_content' => 'Export content',
     'role_asset' => 'Obsahová oprávnění',
     'roles_system_warning' => 'Berte na vědomí, že přístup k některému ze tří výše uvedených oprávnění může uživateli umožnit změnit svá vlastní oprávnění nebo oprávnění ostatních uživatelů v systému. Přiřazujte role s těmito oprávněními pouze důvěryhodným uživatelům.',
-    'role_asset_desc' => 'Tato práva řídí přístup k obsahu napříč systémem. Specifická práva na knihách, kapitolách a stránkách převáží tato nastavení.',
+    'role_asset_desc' => 'Tato oprávnění řídí přístup k obsahu napříč systémem. Specifická oprávnění na knihách, kapitolách a stránkách převáží tato nastavení.',
     'role_asset_admins' => 'Administrátoři automaticky dostávají přístup k veškerému obsahu, ale tyto volby mohou ukázat nebo skrýt volby v uživatelském rozhraní.',
     'role_all' => 'Vše',
     'role_own' => 'Vlastní',
     'role_controlled_by_asset' => 'Řídí se obsahem, do kterého jsou nahrávány',
     'role_save' => 'Uložit roli',
-    'role_update_success' => 'Role úspěšně aktualizována',
+    'role_update_success' => 'Role byla aktualizována',
     'role_users' => 'Uživatelé mající tuto roli',
     'role_users_none' => 'Žádný uživatel nemá tuto roli',
 
@@ -162,7 +167,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_latest_activity' => 'Nedávná aktivita',
     '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.',
@@ -178,51 +183,55 @@ return [
     'users_system_public' => 'Symbolizuje každého nepřihlášeného návštěvníka, který navštívil aplikaci. Nelze ho použít k přihlášení ale je přiřazen automaticky nepřihlášeným.',
     'users_delete' => 'Smazat uživatele',
     'users_delete_named' => 'Odstranit uživatele :userName',
-    'users_delete_warning' => 'Uživatel \':userName\' bude zcela smazán ze systému.',
+    'users_delete_warning' => 'Uživatel \':userName\' bude zcela odstraněn ze systému.',
     'users_delete_confirm' => 'Opravdu chcete tohoto uživatele smazat?',
-    '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_migrate_ownership' => 'Převést vlastnictví',
+    'users_migrate_ownership_desc' => 'Zde zvolte jiného uživatele, pokud chcete, aby se stal vlastníkem všech položek aktuálně vlastněných tímto uživatelem.',
+    'users_none_selected' => 'Nebyl zvolen žádný uživatel',
+    'users_delete_success' => 'Uživatel byl odstraněn',
     'users_edit' => 'Upravit uživatele',
     'users_edit_profile' => 'Upravit profil',
     'users_edit_success' => 'Uživatel byl úspěšně aktualizován',
     'users_avatar' => 'Obrázek uživatele',
-    'users_avatar_desc' => 'Vyberte obrázek, který bude reprezentovat tohoto uživatele. Měl by být přibližně 256px velký ve tvaru čtverce.',
+    'users_avatar_desc' => 'Zvolte obrázek, který bude reprezentovat tohoto uživatele. Měl by být přibližně 256px velký ve tvaru čtverce.',
     'users_preferred_language' => 'Preferovaný jazyk',
     'users_preferred_language_desc' => 'Tato volba ovlivní pouze jazyk používaný v uživatelském rozhraní aplikace. Volba nemá vliv na žádný uživateli vytvářený obsah.',
     'users_social_accounts' => 'Sociální účty',
     'users_social_accounts_info' => 'Zde můžete přidat vaše účty ze sociálních sítí pro pohodlnější přihlašování. Odpojení účtů neznamená, že tato aplikace ztratí práva číst detaily z vašeho účtu. Zakázat této aplikaci přístup k detailům vašeho účtu musíte přímo ve svém profilu na dané sociální síti.',
     'users_social_connect' => 'Připojit účet',
     'users_social_disconnect' => 'Odpojit účet',
-    'users_social_connected' => 'Účet :socialAccount byl úspěšně připojen k vašemu profilu.',
-    'users_social_disconnected' => 'Účet :socialAccount byl úspěšně odpojen od vašeho profilu.',
+    'users_social_connected' => 'Účet :socialAccount byl připojen k vašemu profilu.',
+    'users_social_disconnected' => 'Účet :socialAccount byl odpojen od vašeho profilu.',
     'users_api_tokens' => 'API Tokeny',
     'users_api_tokens_none' => 'Tento uživatel nemá vytvořené žádné API Tokeny',
     'users_api_tokens_create' => 'Vytvořit Token',
     'users_api_tokens_expires' => 'Vyprší',
     'users_api_tokens_docs' => 'API Dokumentace',
+    'users_mfa' => 'Multi-Factor Authentication',
+    'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
+    'users_mfa_x_methods' => ':count method configured|:count methods configured',
+    'users_mfa_configure' => 'Configure Methods',
 
     // API Tokens
-    'user_api_token_create' => 'Vytvořit API Klíč',
+    'user_api_token_create' => 'Vytvořit API Token',
     'user_api_token_name' => 'Název',
     'user_api_token_name_desc' => 'Zadejte srozumitelný název tokenu, který vám později může pomoci připomenout účel, za jakým jste token vytvářeli.',
     'user_api_token_expiry' => 'Platný do',
     'user_api_token_expiry_desc' => 'Zadejte datum, kdy platnost tokenu vyprší. Po tomto datu nebudou požadavky, které používají tento token, fungovat. Pokud ponecháte pole prázdné, bude tokenu nastavena platnost na dalších 100 let.',
     'user_api_token_create_secret_message' => 'Ihned po vytvoření tokenu Vám bude vygenerován a zobrazen "Token ID" a "Token Secret". Upozorňujeme, že "Token Secret" bude možné zobrazit pouze jednou, ujistěte se, že si jej poznamenáte a uložíte na bezpečné místo před tím, než budete pokračovat dále.',
-    'user_api_token_create_success' => 'API klíč úspěšně vytvořen',
-    'user_api_token_update_success' => 'API klíč úspěšně aktualizován',
-    'user_api_token' => 'API Klíč',
+    'user_api_token_create_success' => 'API Token byl vytvořen',
+    'user_api_token_update_success' => 'API Token byl aktualizován',
+    'user_api_token' => 'API Token',
     'user_api_token_id' => 'Token ID',
-    'user_api_token_id_desc' => 'Toto je neupravitelný systémový identifikátor generovaný pro tento klíč, který musí být uveden v API requestu.',
+    'user_api_token_id_desc' => 'Toto je neupravitelný systémový identifikátor generovaný pro tento Token, který musí být uveden v API requestu.',
     'user_api_token_secret' => 'Token Secret',
-    'user_api_token_secret_desc' => 'Toto je systémem generovaný "secret" pro tento klíč, který musí být v API requestech. Toto bude zobrazeno pouze jednou, takže si uložte tuto hodnotu na bezpečné místo.',
+    'user_api_token_secret_desc' => 'Toto je systémem generovaný "Secret" pro tento Token, který musí být v API requestech. Toto bude zobrazeno pouze jednou, takže si uložte tuto hodnotu na bezpečné místo.',
     'user_api_token_created' => 'Token vytvořen :timeAgo',
     'user_api_token_updated' => 'Token aktualizován :timeAgo',
     'user_api_token_delete' => 'Odstranit Token',
-    'user_api_token_delete_warning' => 'Tímto plně smažete tento API klíč s názvem \':tokenName\' ze systému.',
-    'user_api_token_delete_confirm' => 'Opravdu chcete odstranit tento API klíč?',
-    'user_api_token_delete_success' => 'API Klíč úspěšně odstraněn',
+    'user_api_token_delete_warning' => 'Tímto plně odstraníte tento API Token s názvem \':tokenName\' ze systému.',
+    'user_api_token_delete_confirm' => 'Opravdu chcete odstranit tento API Token?',
+    'user_api_token_delete_success' => 'API Token byl odstraněn',
 
     //! If editing translations files directly please ignore this in all
     //! languages apart from en. Content will be auto-copied from en.
@@ -247,6 +256,7 @@ return [
         'it' => 'Italian',
         'ja' => '日本語',
         'ko' => '한국어',
+        'lt' => 'Lietuvių Kalba',
         'lv' => 'Latviešu Valoda',
         'nl' => 'Nederlands',
         'nb' => 'Norsk (Bokmål)',
index 1a94b9e24f210a7c17eea9c3328ee1b2364ffdc9..ea7eebdf988d5a2d6c2cd7300f6bca16be1f099b 100644 (file)
@@ -15,6 +15,7 @@ return [
     'alpha_dash'           => ':attribute může obsahovat pouze písmena, číslice, pomlčky a podtržítka. České znaky (á, é, í, ó, ú, ů, ž, š, č, ř, ď, ť, ň) nejsou podporovány.',
     'alpha_num'            => ':attribute může obsahovat pouze písmena a číslice.',
     'array'                => ':attribute musí být pole.',
+    'backup_codes'         => 'The provided code is not valid or has already been used.',
     'before'               => ':attribute musí být datum před :date.',
     'between'              => [
         'numeric' => ':attribute musí být hodnota mezi :min a :max.',
@@ -60,7 +61,7 @@ return [
         'array'   => ':attribute by měl obsahovat méně než :value položek.',
     ],
     'lte'                  => [
-        'numeric' => ':attribute musí být menší nebo rovno než :value.',
+        'numeric' => ':attribute musí být menší nebo rovno :value.',
         'file'    => 'Velikost souboru :attribute musí být menší než :value kB.',
         'string'  => ':attribute nesmí být delší než :value znaků.',
         'array'   => ':attribute by měl obsahovat maximálně :value položek.',
@@ -89,7 +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.',
+    'safe_url'             => 'Zadaný odkaz může být nebezpečný.',
     'size'                 => [
         'numeric' => ':attribute musí být přesně :size.',
         'file'    => ':attribute musí mít přesně :size Kilobytů.',
@@ -98,6 +99,7 @@ return [
     ],
     'string'               => ':attribute musí být řetězec znaků.',
     'timezone'             => ':attribute musí být platná časová zóna.',
+    'totp'                 => 'The provided code is not valid or has expired.',
     'unique'               => ':attribute musí být unikátní.',
     'url'                  => 'Formát :attribute je neplatný.',
     'uploaded'             => 'Nahrávání :attribute se nezdařilo.',
index 614d1a8ac5d38b60aafea812815e7271e2259097..23c13b2d8444d0c6770df52f1b6503c060947ad0 100644 (file)
@@ -44,8 +44,12 @@ return [
     'bookshelf_delete_notification'    => 'Bogreolen blev opdateret',
 
     // Favourites
-    'favourite_add_notification' => '":name" has been added to your favourites',
-    'favourite_remove_notification' => '":name" has been removed from your favourites',
+    'favourite_add_notification' => '":name" er blevet tilføjet til dine favoritter',
+    'favourite_remove_notification' => '":name" er blevet fjernet fra dine favoritter',
+
+    // MFA
+    'mfa_setup_method_notification' => 'Multi-faktor metode konfigureret',
+    'mfa_remove_method_notification' => 'Multi-faktor metode fjernet',
 
     // Other
     'commented_on'                => 'kommenterede til',
index 8ea58517493187a77f01582a22590e2ec4aa2957..8c9d86ea69c6706419992a35312abe8700abb456 100644 (file)
@@ -73,5 +73,40 @@ return [
     'user_invite_page_welcome' => 'Velkommen til :appName!',
     'user_invite_page_text' => 'For at færdiggøre din konto og få adgang skal du indstille en adgangskode, der bruges til at logge ind på :appName ved fremtidige besøg.',
     'user_invite_page_confirm_button' => 'Bekræft adgangskode',
-    'user_invite_success' => 'Adgangskode indstillet, du har nu adgang til :appName!'
+    'user_invite_success' => 'Adgangskode indstillet, du har nu adgang til :appName!',
+
+    // Multi-factor Authentication
+    'mfa_setup' => 'Opsætning af Multi-faktor godkendelse',
+    'mfa_setup_desc' => 'Opsæt multi-faktor godkendelse som et ekstra lag af sikkerhed for din brugerkonto.',
+    'mfa_setup_configured' => 'Allerede konfigureret',
+    'mfa_setup_reconfigure' => 'Genkonfigurer',
+    'mfa_setup_remove_confirmation' => 'Er du sikker på, at du vil fjerne denne multi-faktor godkendelsesmetode?',
+    'mfa_setup_action' => 'Opsætning',
+    'mfa_backup_codes_usage_limit_warning' => 'Du har mindre end 5 backup koder tilbage, generere og gem et nyt sæt før du løber tør for koder, for at forhindre at blive lukket ude af din konto.',
+    'mfa_option_totp_title' => 'Mobil app',
+    'mfa_option_totp_desc' => 'For at bruge multi-faktor godkendelse, skal du bruge en mobil app, der understøtter TOTP såsom Google Authenticator, Authy eller Microsoft Authenticator.',
+    'mfa_option_backup_codes_title' => 'Backup koder',
+    'mfa_option_backup_codes_desc' => 'Gem sikkert et sæt af engangs backup koder, som du kan indtaste for at bekræfte din identitet.',
+    'mfa_gen_confirm_and_enable' => 'Bekræft og aktivér',
+    'mfa_gen_backup_codes_title' => 'Backup koder opsætning',
+    'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.',
+    'mfa_gen_backup_codes_download' => 'Download Codes',
+    'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once',
+    'mfa_gen_totp_title' => 'Mobile App Setup',
+    'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
+    'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.',
+    'mfa_gen_totp_verify_setup' => 'Verify Setup',
+    'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:',
+    'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here',
+    'mfa_verify_access' => 'Verify Access',
+    'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.',
+    'mfa_verify_no_methods' => 'No Methods Configured',
+    'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.',
+    'mfa_verify_use_totp' => 'Verify using a mobile app',
+    'mfa_verify_use_backup_codes' => 'Verify using a backup code',
+    'mfa_verify_backup_code' => 'Backup Code',
+    'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:',
+    'mfa_verify_backup_code_enter_here' => 'Enter backup code here',
+    'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:',
+    'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.',
 ];
\ No newline at end of file
index 0504f5a105ab00625722584e39dbd290eac2c6b4..0e426973467deb69384cc925acfe8f1f19d4965c 100644 (file)
@@ -39,11 +39,12 @@ return [
     'reset' => 'Nulstil',
     'remove' => 'Fjern',
     'add' => 'Tilføj',
+    'configure' => 'Konfigurer',
     'fullscreen' => 'Fuld skærm',
-    'favourite' => 'Favourite',
-    'unfavourite' => 'Unfavourite',
-    'next' => 'Next',
-    'previous' => 'Previous',
+    'favourite' => 'Foretrukken',
+    'unfavourite' => 'Fjern som foretrukken',
+    'next' => 'Næste',
+    'previous' => 'Forrige',
 
     // Sort Options
     'sort_options' => 'Sorteringsindstillinger',
@@ -60,6 +61,7 @@ return [
     'no_activity' => 'Ingen aktivitet at vise',
     'no_items' => 'Intet indhold tilgængeligt',
     'back_to_top' => 'Tilbage til toppen',
+    'skip_to_main_content' => 'Spring til indhold',
     'toggle_details' => 'Vis/skjul detaljer',
     'toggle_thumbnails' => 'Vis/skjul miniaturer',
     'details' => 'Detaljer',
index 36e290d86d8b38ac16dcf6dd8e2fdcd06eba5560..e488e201fa15f15d4b6982c16404a3a95b9c09cf 100644 (file)
@@ -27,8 +27,8 @@ return [
     'images' => 'Billeder',
     'my_recent_drafts' => 'Mine seneste kladder',
     'my_recently_viewed' => 'Mine senest viste',
-    'my_most_viewed_favourites' => 'My Most Viewed Favourites',
-    'my_favourites' => 'My Favourites',
+    'my_most_viewed_favourites' => 'Mine mest viste favoritter',
+    'my_favourites' => 'Mine favoritter',
     'no_pages_viewed' => 'Du har ikke besøgt nogle sider',
     'no_pages_recently_created' => 'Ingen sider er blevet oprettet for nyligt',
     'no_pages_recently_updated' => 'Ingen sider er blevet opdateret for nyligt',
@@ -36,6 +36,7 @@ return [
     'export_html' => 'Indeholdt webfil',
     'export_pdf' => 'PDF-fil',
     'export_text' => 'Almindelig tekstfil',
+    'export_md' => 'Markdown Fil',
 
     // Permissions and restrictions
     'permissions' => 'Rettigheder',
@@ -98,6 +99,7 @@ return [
     'shelves_permissions' => 'Reoltilladelser',
     'shelves_permissions_updated' => 'Reoltilladelser opdateret',
     'shelves_permissions_active' => 'Aktive reoltilladelser',
+    'shelves_permissions_cascade_warning' => 'Tilladelser på reoler nedarves ikke automatisk til indeholdte bøger. Dette skyldes, at en bog kan eksistere på flere hylder. Tilladelser kan dog kopieres ned til underliggende bøger ved hjælp af muligheden, der findes nedenfor.',
     'shelves_copy_permissions_to_books' => 'Kopier tilladelser til bøger',
     'shelves_copy_permissions' => 'Kopier tilladelser',
     'shelves_copy_permissions_explain' => 'Dette vil anvende de aktuelle tilladelsesindstillinger på denne boghylde på alle bøger indeholdt i. Før aktivering skal du sikre dig, at ændringer i tilladelserne til denne boghylde er blevet gemt.',
index 806ac8177c2f1a3071bff928db2c1f26497d7884..d54cac243b56e31126e82cfe688288a778efde92 100644 (file)
@@ -83,9 +83,9 @@ return [
     '404_page_not_found' => 'Siden blev ikke fundet',
     'sorry_page_not_found' => 'Beklager, siden du leder efter blev ikke fundet.',
     'sorry_page_not_found_permission_warning' => 'Hvis du forventede, at denne side skulle eksistere, har du muligvis ikke tilladelse til at se den.',
-    'image_not_found' => 'Image Not Found',
-    'image_not_found_subtitle' => 'Sorry, The image file you were looking for could not be found.',
-    'image_not_found_details' => 'If you expected this image to exist it might have been deleted.',
+    'image_not_found' => 'Billede ikke fundet',
+    'image_not_found_subtitle' => 'Beklager, billedet du ledte efter kunne ikke findes.',
+    'image_not_found_details' => 'Hvis du forventede, at dette billede skulle eksistere, kan det være blevet slettet.',
     'return_home' => 'Gå tilbage til hjem',
     'error_occurred' => 'Der opstod en fejl',
     'app_down' => ':appName er nede lige nu',
index 7bbc31edbc209d2712b9aa906536425a79254d25..cfb4ed908204eb609f391325a795a92316c2a9f7 100644 (file)
@@ -92,6 +92,7 @@ return [
     'recycle_bin' => 'Papirkurv',
     'recycle_bin_desc' => 'Her kan du gendanne elementer, der er blevet slettet eller vælge at permanent fjerne dem fra systemet. Denne liste er ufiltreret, i modsætning til lignende aktivitetslister i systemet, hvor tilladelsesfiltre anvendes.',
     'recycle_bin_deleted_item' => 'Slettet element',
+    'recycle_bin_deleted_parent' => 'Overordnet',
     'recycle_bin_deleted_by' => 'Slettet af',
     'recycle_bin_deleted_at' => 'Sletningstidspunkt',
     'recycle_bin_permanently_delete' => 'Slet permanent',
@@ -104,6 +105,7 @@ return [
     'recycle_bin_restore_list' => 'Elementer der skal gendannes',
     'recycle_bin_restore_confirm' => 'Denne handling vil gendanne det slettede element, herunder alle underelementer, til deres oprindelige placering. Hvis den oprindelige placering siden er blevet slettet, og nu er i papirkurven, vil det overordnede element også skulle gendannes.',
     'recycle_bin_restore_deleted_parent' => 'Det overordnede element til dette element er også blevet slettet. Disse vil forblive slettet indtil det overordnede også er gendannet.',
+    'recycle_bin_restore_parent' => 'Gendan Overordnet',
     'recycle_bin_destroy_notification' => 'Slettede :count elementer fra papirkurven.',
     'recycle_bin_restore_notification' => 'Gendannede :count elementer fra papirkurven.',
 
@@ -117,6 +119,7 @@ return [
     'audit_table_user' => 'Bruger',
     'audit_table_event' => 'Hændelse',
     'audit_table_related' => 'Relateret element eller detalje',
+    'audit_table_ip' => 'IP Address',
     'audit_table_date' => 'Aktivitetsdato',
     'audit_date_from' => 'Datointerval fra',
     'audit_date_to' => 'Datointerval til',
@@ -136,6 +139,7 @@ return [
     'role_details' => 'Rolledetaljer',
     'role_name' => 'Rollenavn',
     'role_desc' => 'Kort beskrivelse af rolle',
+    'role_mfa_enforced' => 'Kræver multifaktor godkendelse',
     'role_external_auth_id' => 'Eksterne godkendelses-IDer',
     'role_system' => 'Systemtilladelser',
     'role_manage_users' => 'Administrere brugere',
@@ -145,6 +149,7 @@ return [
     'role_manage_page_templates' => 'Administrer side-skabeloner',
     'role_access_api' => 'Tilgå system-API',
     'role_manage_settings' => 'Administrer app-indstillinger',
+    'role_export_content' => 'Eksporter indhold',
     'role_asset' => 'Tilladelser for medier og "assets"',
     'roles_system_warning' => 'Vær opmærksom på, at adgang til alle af de ovennævnte tre tilladelser, kan give en bruger mulighed for at ændre deres egne brugerrettigheder eller brugerrettigheder for andre i systemet. Tildel kun roller med disse tilladelser til betroede brugere.',
     'role_asset_desc' => 'Disse tilladelser kontrollerer standardadgang til medier og "assets" i systemet. Tilladelser til bøger, kapitler og sider tilsidesætter disse tilladelser.',
@@ -202,6 +207,10 @@ return [
     'users_api_tokens_create' => 'Opret Token',
     'users_api_tokens_expires' => 'Udløber',
     'users_api_tokens_docs' => 'API-dokumentation',
+    'users_mfa' => 'Multi-faktor godkendelse',
+    'users_mfa_desc' => 'Opsæt multi-faktor godkendelse som et ekstra lag af sikkerhed for din brugerkonto.',
+    'users_mfa_x_methods' => ':count metode konfigureret|:count metoder konfigureret',
+    'users_mfa_configure' => 'Konfigurer metoder',
 
     // API Tokens
     'user_api_token_create' => 'Opret API-token',
@@ -247,6 +256,7 @@ return [
         'it' => 'Italian',
         'ja' => '日本語',
         'ko' => '한국어',
+        'lt' => 'Lietuvių Kalba',
         'lv' => 'Latviešu Valoda',
         'nl' => 'Nederlands',
         'nb' => 'Norsk (Bokmål)',
index 6c11f2e0fe4f3d938cd6b87a2903a6c3ec31c173..c54b07a6eb529ced79a9d26bbde3ffc8efbbe921 100644 (file)
@@ -15,6 +15,7 @@ return [
     'alpha_dash'           => ':attribute må kun bestå af bogstaver, tal, binde- og under-streger.',
     'alpha_num'            => ':attribute må kun indeholde bogstaver og tal.',
     'array'                => ':attribute skal være et array.',
+    'backup_codes'         => 'Den angivne kode er ikke gyldig eller er allerede brugt.',
     'before'               => ':attribute skal være en dato før :date.',
     'between'              => [
         'numeric' => ':attribute skal være mellem :min og :max.',
@@ -98,6 +99,7 @@ return [
     ],
     'string'               => ':attribute skal være tekst.',
     'timezone'             => ':attribute skal være en gyldig zone.',
+    'totp'                 => 'Den angivne kode er ikke gyldig eller er udløbet.',
     'unique'               => ':attribute er allerede i brug.',
     'url'                  => ':attribute-formatet er ugyldigt.',
     'uploaded'             => 'Filen kunne ikke oploades. Serveren accepterer muligvis ikke filer af denne størrelse.',
index 52c99168f2c20dfea41c309086d7e3d2d3a32579..87dd3ee8ba534157be8a9e8d2c969dbb1d709112 100644 (file)
@@ -47,6 +47,10 @@ return [
     'favourite_add_notification' => '":name" wurde zu deinen Favoriten hinzugefügt',
     'favourite_remove_notification' => '":name" wurde aus Ihren Favoriten entfernt',
 
+    // MFA
+    'mfa_setup_method_notification' => 'Multi-Faktor-Methode erfolgreich konfiguriert',
+    'mfa_remove_method_notification' => 'Multi-Faktor-Methode erfolgreich entfernt',
+
     // Other
     'commented_on'                => 'hat einen Kommentar hinzugefügt',
     'permissions_update'          => 'hat die Berechtigungen aktualisiert',
index 1f5a49cbddc240b36b70ff02fec035e6355abdf3..efe82680da55808c0e42fac0c28378ac5c172d55 100644 (file)
@@ -6,7 +6,7 @@
  */
 return [
 
-    'failed' => 'Die eingegebenen Anmeldedaten sind ungültig.',
+    'failed' => 'Diese Anmeldedaten stimmen nicht mit unseren Aufzeichnungen überein.',
     'throttle' => 'Zu viele Anmeldeversuche. Bitte versuchen Sie es in :seconds Sekunden erneut.',
 
     // Login & Register
@@ -20,7 +20,7 @@ return [
     'username' => 'Benutzername',
     'email' => 'E-Mail',
     'password' => 'Passwort',
-    'password_confirm' => 'Passwort best&auml;tigen',
+    'password_confirm' => 'Passwort bestätigen',
     'password_hint' => 'Mindestlänge: 7 Zeichen',
     'forgot_password' => 'Passwort vergessen?',
     'remember_me' => 'Angemeldet bleiben',
@@ -73,5 +73,40 @@ return [
     'user_invite_page_welcome' => 'Willkommen bei :appName!',
     'user_invite_page_text' => 'Um die Anmeldung abzuschließen und Zugriff auf :appName zu bekommen muss noch ein Passwort festgelegt werden. Dieses wird in Zukunft zum Einloggen benötigt.',
     'user_invite_page_confirm_button' => 'Passwort wiederholen',
-    'user_invite_success' => 'Passwort gesetzt, Sie haben nun Zugriff auf :appName!'
+    'user_invite_success' => 'Passwort gesetzt, Sie haben nun Zugriff auf :appName!',
+
+    // Multi-factor Authentication
+    'mfa_setup' => 'Multi-Faktor-Authentifizierung einrichten',
+    'mfa_setup_desc' => 'Richten Sie Multi-Faktor-Authentifizierung als zusätzliche Sicherheitsstufe für Ihr Benutzerkonto ein.',
+    'mfa_setup_configured' => 'Bereits konfiguriert',
+    'mfa_setup_reconfigure' => 'Umkonfigurieren',
+    'mfa_setup_remove_confirmation' => 'Sind Sie sicher, dass Sie diese Multi-Faktor-Authentifizierungsmethode entfernen möchten?',
+    'mfa_setup_action' => 'Einrichtung',
+    'mfa_backup_codes_usage_limit_warning' => 'Sie haben weniger als 5 Backup-Codes übrig, Bitte erstellen und speichern Sie ein neues Set bevor Sie keine Codes mehr haben, um zu verhindern, dass Sie von Ihrem Konto gesperrt werden.',
+    'mfa_option_totp_title' => 'Mobile App',
+    'mfa_option_totp_desc' => 'Um Mehrfach-Faktor-Authentifizierung nutzen zu können, benötigen Sie eine mobile Anwendung, die TOTP unterstützt, wie Google Authenticator, Authy oder Microsoft Authenticator.',
+    'mfa_option_backup_codes_title' => 'Backup Code',
+    'mfa_option_backup_codes_desc' => 'Speichern Sie sicher eine Reihe von einmaligen Backup-Codes, die Sie eingeben können, um Ihre Identität zu überprüfen.',
+    'mfa_gen_confirm_and_enable' => 'Bestätigen und aktivieren',
+    'mfa_gen_backup_codes_title' => 'Backup-Codes einrichten',
+    'mfa_gen_backup_codes_desc' => 'Speichern Sie die folgende Liste der Codes an einem sicheren Ort. Wenn Sie auf das System zugreifen, können Sie einen der Codes als zweiten Authentifizierungsmechanismus verwenden.',
+    'mfa_gen_backup_codes_download' => 'Download Codes',
+    'mfa_gen_backup_codes_usage_warning' => 'Jeder Code kann nur einmal verwendet werden',
+    'mfa_gen_totp_title' => 'Mobile App einrichten',
+    'mfa_gen_totp_desc' => 'Um Mehrfach-Faktor-Authentifizierung nutzen zu können, benötigen Sie eine mobile Anwendung, die TOTP unterstützt, wie Google Authenticator, Authy oder Microsoft Authenticator.',
+    'mfa_gen_totp_scan' => 'Scannen Sie den QR-Code unten mit Ihrer bevorzugten Authentifizierungs-App, um loszulegen.',
+    'mfa_gen_totp_verify_setup' => 'Setup überprüfen',
+    'mfa_gen_totp_verify_setup_desc' => 'Überprüfen Sie, dass alles funktioniert, indem Sie einen Code in Ihrer Authentifizierungs-App in das Eingabefeld unten eingeben:',
+    'mfa_gen_totp_provide_code_here' => 'Geben Sie hier Ihre App generierten Code ein',
+    'mfa_verify_access' => 'Zugriff überprüfen',
+    'mfa_verify_access_desc' => 'Ihr Benutzerkonto erfordert, dass Sie Ihre Identität über eine zusätzliche Verifikationsebene bestätigen, bevor Sie den Zugriff gewähren. Überprüfen Sie mit einer Ihrer konfigurierten Methoden, um fortzufahren.',
+    'mfa_verify_no_methods' => 'Keine Methoden konfiguriert',
+    'mfa_verify_no_methods_desc' => 'Es konnten keine Mehrfach-Faktor-Authentifizierungsmethoden für Ihr Konto gefunden werden. Sie müssen mindestens eine Methode einrichten, bevor Sie Zugriff erhalten.',
+    'mfa_verify_use_totp' => 'Mit einer mobilen App verifizieren',
+    'mfa_verify_use_backup_codes' => 'Mit einem Backup-Code überprüfen',
+    'mfa_verify_backup_code' => 'Backup-Code',
+    'mfa_verify_backup_code_desc' => 'Geben Sie einen Ihrer verbleibenden Backup-Codes unten ein:',
+    'mfa_verify_backup_code_enter_here' => 'Backup-Code hier eingeben',
+    'mfa_verify_totp_desc' => 'Geben Sie den Code ein, der mit Ihrer mobilen App generiert wurde:',
+    'mfa_setup_login_notification' => 'Multi-Faktor-Methode konfiguriert. Bitte melden Sie sich jetzt erneut mit der konfigurierten Methode an.',
 ];
\ No newline at end of file
index f46537ced82d1f0a850c3f3a30fe65a2b548b86c..bd75e1737dc499330b4b5e7c6117ec5f4985734e 100644 (file)
@@ -33,14 +33,15 @@ return [
     'copy' => 'Kopieren',
     'reply' => 'Antworten',
     'delete' => 'Löschen',
-    'delete_confirm' => 'Löschen Bestätigen',
+    'delete_confirm' => 'Löschen bestätigen',
     'search' => 'Suchen',
     'search_clear' => 'Suche löschen',
     'reset' => 'Zurücksetzen',
     'remove' => 'Entfernen',
     'add' => 'Hinzufügen',
+    'configure' => 'Konfigurieren',
     'fullscreen' => 'Vollbild',
-    'favourite' => 'Favorit',
+    'favourite' => 'Favoriten',
     'unfavourite' => 'Kein Favorit',
     'next' => 'Nächste',
     'previous' => 'Vorheriges',
@@ -56,10 +57,11 @@ return [
     'sort_updated_at' => 'Aktualisierungsdatum',
 
     // Misc
-    'deleted_user' => 'Gelöschte Benutzer',
+    'deleted_user' => 'Gelöschter Benutzer',
     'no_activity' => 'Keine Aktivitäten zum Anzeigen',
-    'no_items' => 'Keine Einträge gefunden.',
+    'no_items' => 'Keine Einträge gefunden',
     'back_to_top' => 'nach oben',
+    'skip_to_main_content' => 'Direkt zum Hauptinhalt',
     'toggle_details' => 'Details zeigen/verstecken',
     'toggle_thumbnails' => 'Thumbnails zeigen/verstecken',
     'details' => 'Details',
index d11c8dbaf1c8c6f53dad54f13fda9eec647e2660..fdb26375878bb00be2b3ac04ad93172c48ce2c68 100644 (file)
@@ -36,6 +36,7 @@ return [
     'export_html' => 'HTML-Datei',
     'export_pdf' => 'PDF-Datei',
     'export_text' => 'Textdatei',
+    'export_md' => 'Markdown-Datei',
 
     // Permissions and restrictions
     'permissions' => 'Berechtigungen',
@@ -98,6 +99,7 @@ return [
     'shelves_permissions' => 'Regal-Berechtigungen',
     'shelves_permissions_updated' => 'Regal-Berechtigungen aktualisiert',
     'shelves_permissions_active' => 'Regal-Berechtigungen aktiv',
+    'shelves_permissions_cascade_warning' => 'Die Berechtigungen in Bücherregalen werden nicht automatisch auf enthaltene Bücher kaskadiert, weil ein Buch in mehreren Regalen existieren kann. Berechtigungen können jedoch mit der unten stehenden Option in untergeordnete Bücher kopiert werden.',
     'shelves_copy_permissions_to_books' => 'Kopiere die Berechtigungen zum Buch',
     'shelves_copy_permissions' => 'Berechtigungen kopieren',
     'shelves_copy_permissions_explain' => 'Hiermit werden die Berechtigungen des aktuellen Regals auf alle enthaltenen Bücher übertragen. Überprüfen Sie vor der Aktivierung, ob alle Berechtigungsänderungen am aktuellen Regal gespeichert wurden.',
index 0a21857df001085189190dcf1304df634ae20d98..65174eada10d69421c1be5e7becf3f6f02ebab62 100644 (file)
@@ -5,7 +5,7 @@
 return [
 
     // Permissions
-    'permission' => 'Sie haben keine Berechtigung, auf diese Seite zuzugreifen.',
+    'permission' => 'Sie haben keine Zugriffsberechtigung auf die angeforderte Seite.',
     'permissionJson' => 'Sie haben keine Berechtigung, die angeforderte Aktion auszuführen.',
 
     // Auth
@@ -14,7 +14,7 @@ return [
     'email_confirmation_invalid' => 'Der Bestätigungslink ist nicht gültig oder wurde bereits verwendet. Bitte registrieren Sie sich erneut.',
     'email_confirmation_expired' => 'Der Bestätigungslink ist abgelaufen. Es wurde eine neue Bestätigungs-E-Mail gesendet.',
     'email_confirmation_awaiting' => 'Die E-Mail-Adresse für das verwendete Konto muss bestätigt werden',
-    'ldap_fail_anonymous' => 'Anonymer LDAP-Zugriff ist fehlgeschlafgen',
+    'ldap_fail_anonymous' => 'Anonymer LDAP-Zugriff ist fehlgeschlagen',
     'ldap_fail_authed' => 'LDAP-Zugriff mit DN und Passwort ist fehlgeschlagen',
     'ldap_extension_not_installed' => 'LDAP-PHP-Erweiterung ist nicht installiert.',
     'ldap_cannot_connect' => 'Die Verbindung zum LDAP-Server ist fehlgeschlagen. Beim initialen Verbindungsaufbau trat ein Fehler auf.',
@@ -43,14 +43,14 @@ return [
     'uploaded'  => 'Der Server verbietet das Hochladen von Dateien mit dieser Dateigröße. Bitte versuchen Sie es mit einer kleineren Datei.',
     'image_upload_error' => 'Beim Hochladen des Bildes trat ein Fehler auf.',
     'image_upload_type_error' => 'Der Bildtyp der hochgeladenen Datei ist ungültig.',
-    'file_upload_timeout' => 'Der Upload der Datei ist abgelaufen.',
+    'file_upload_timeout' => 'Der Datei-Upload hat das Zeitlimit überschritten.',
 
     // Attachments
     'attachment_not_found' => 'Anhang konnte nicht gefunden werden.',
 
     // Pages
     'page_draft_autosave_fail' => 'Fehler beim Speichern des Entwurfs. Stellen Sie sicher, dass Sie mit dem Internet verbunden sind, bevor Sie den Entwurf dieser Seite speichern.',
-    'page_custom_home_deletion' => 'Eine als Startseite gesetzte Seite kann nicht gelöscht werden.',
+    'page_custom_home_deletion' => 'Eine als Startseite gesetzte Seite kann nicht gelöscht werden',
 
     // Entities
     'entity_not_found' => 'Eintrag nicht gefunden',
@@ -58,48 +58,48 @@ return [
     'book_not_found' => 'Buch nicht gefunden',
     'page_not_found' => 'Seite nicht gefunden',
     'chapter_not_found' => 'Kapitel nicht gefunden',
-    'selected_book_not_found' => 'Das gewählte Buch wurde nicht gefunden.',
+    'selected_book_not_found' => 'Das gewählte Buch wurde nicht gefunden',
     'selected_book_chapter_not_found' => 'Das gewählte Buch oder Kapitel wurde nicht gefunden.',
     'guests_cannot_save_drafts' => 'Gäste können keine Entwürfe speichern',
 
     // Users
-    'users_cannot_delete_only_admin' => 'Sie können den einzigen Administrator nicht löschen.',
+    'users_cannot_delete_only_admin' => 'Sie können den einzigen Administrator nicht löschen',
     'users_cannot_delete_guest' => 'Sie können den Gast-Benutzer nicht löschen',
 
     // Roles
-    'role_cannot_be_edited' => 'Diese Rolle kann nicht bearbeitet werden.',
+    'role_cannot_be_edited' => 'Diese Rolle kann nicht bearbeitet werden',
     'role_system_cannot_be_deleted' => 'Dies ist eine Systemrolle und kann nicht gelöscht werden',
     'role_registration_default_cannot_delete' => 'Diese Rolle kann nicht gelöscht werden, solange sie als Standardrolle für neue Registrierungen gesetzt ist',
-    'role_cannot_remove_only_admin' => 'Dieser Benutzer ist der einzige Benutzer, welchem die Administratorrolle zugeordnet ist. Ordnen Sie die Administratorrolle einem anderen Benutzer zu, bevor Sie versuchen, sie hier zu entfernen.',
+    'role_cannot_remove_only_admin' => 'Dieser Benutzer ist der einzige Benutzer, welchem die Administratorrolle zugeordnet ist. Ordnen Sie die Administratorrolle einem anderen Benutzer zu bevor Sie versuchen sie hier zu entfernen.',
 
     // Comments
     'comment_list' => 'Beim Abrufen der Kommentare ist ein Fehler aufgetreten.',
     'cannot_add_comment_to_draft' => 'Du kannst keine Kommentare zu einem Entwurf hinzufügen.',
     'comment_add' => 'Beim Hinzufügen des Kommentars ist ein Fehler aufgetreten.',
     'comment_delete' => 'Beim Löschen des Kommentars ist ein Fehler aufgetreten.',
-    'empty_comment' => 'Kann keinen leeren Kommentar hinzufügen',
+    'empty_comment' => 'Kann keinen leeren Kommentar hinzufügen.',
 
     // Error pages
     '404_page_not_found' => 'Seite nicht gefunden',
-    'sorry_page_not_found' => 'Entschuldigung. Die Seite, die Sie angefordert haben, wurde nicht gefunden.',
+    'sorry_page_not_found' => 'Entschuldigung. Die angeforderte Seite wurde nicht gefunden.',
     'sorry_page_not_found_permission_warning' => 'Wenn Sie erwartet haben, dass diese Seite existiert, haben Sie möglicherweise nicht die Berechtigung, sie anzuzeigen.',
     'image_not_found' => 'Bild nicht gefunden',
-    'image_not_found_subtitle' => 'Entschuldigung. Das Bild, die Sie angefordert haben, wurde nicht gefunden.',
+    'image_not_found_subtitle' => 'Entschuldigung. Das angeforderte Bild wurde nicht gefunden.',
     'image_not_found_details' => 'Wenn Sie erwartet haben, dass dieses Bild existiert, könnte es gelöscht worden sein.',
     'return_home' => 'Zurück zur Startseite',
     'error_occurred' => 'Es ist ein Fehler aufgetreten',
-    'app_down' => ':appName befindet sich aktuell im Wartungsmodus.',
+    'app_down' => ':appName befindet sich aktuell im Wartungsmodus',
     'back_soon' => 'Wir werden so schnell wie möglich wieder online sein.',
 
     // API errors
-    'api_no_authorization_found' => 'Kein Autorisierungs-Token für die Anfrage gefunden',
-    'api_bad_authorization_format' => 'Ein Autorisierungs-Token wurde auf die Anfrage gefunden, aber das Format schien falsch zu sein',
-    'api_user_token_not_found' => 'Es wurde kein passender API-Token für den angegebenen Autorisierungs-Token gefunden',
-    'api_incorrect_token_secret' => 'Das für den angegebenen API-Token angegebene Kennwort ist falsch',
-    'api_user_no_api_permission' => 'Der Besitzer des verwendeten API-Token hat keine Berechtigung für API-Aufrufe',
-    'api_user_token_expired' => 'Das verwendete Autorisierungs-Token ist abgelaufen',
+    'api_no_authorization_found' => 'Kein Autorisierungstoken für die Anfrage gefunden',
+    'api_bad_authorization_format' => 'Ein Autorisierungstoken wurde auf die Anfrage gefunden, aber das Format schien falsch zu sein',
+    'api_user_token_not_found' => 'Es wurde kein passender API-Token für den angegebenen Autorisierungstoken gefunden',
+    'api_incorrect_token_secret' => 'Das Kennwort für das angegebene API-Token ist falsch',
+    'api_user_no_api_permission' => 'Der Besitzer des verwendeten API-Tokens hat keine Berechtigung für API-Aufrufe',
+    'api_user_token_expired' => 'Das verwendete Autorisierungstoken ist abgelaufen',
 
     // Settings & Maintenance
-    'maintenance_test_email_failure' => 'Fehler beim Senden einer Test E-Mail:',
+    'maintenance_test_email_failure' => 'Fehler beim Versenden einer Test E-Mail:',
 
 ];
index a853a0b62ee1efc0ba768eb9474782fc9727616e..d24319c18ba1168c887ecfa1243f108b3ad0a27e 100644 (file)
@@ -18,7 +18,7 @@ return [
     'app_name_desc' => 'Dieser Name wird im Header und in E-Mails angezeigt.',
     'app_name_header' => 'Anwendungsname im Header anzeigen?',
     'app_public_access' => 'Öffentlicher Zugriff',
-    'app_public_access_desc' => 'Wenn Sie diese Option aktivieren, können Besucher, die nicht angemeldet sind, auf Inhalte in Ihrer BookStack-Instanz zugreifen.',
+    'app_public_access_desc' => 'Wenn Sie diese Option aktivieren können Besucher, die nicht angemeldet sind, auf Inhalte in Ihrer BookStack-Instanz zugreifen.',
     'app_public_access_desc_guest' => 'Der Zugang für öffentliche Besucher kann über den Benutzer "Guest" gesteuert werden.',
     'app_public_access_toggle' => 'Öffentlichen Zugriff erlauben',
     'app_public_viewing' => 'Öffentliche Ansicht erlauben?',
@@ -40,7 +40,7 @@ Wenn Sie nicht eingeben, wird die Anwendung auf die Standardfarbe zurückgesetzt
     'app_homepage_desc' => 'Wählen Sie eine Seite als Startseite aus, die statt der Standardansicht angezeigt werden soll. Seitenberechtigungen werden für die ausgewählten Seiten ignoriert.',
     'app_homepage_select' => 'Wählen Sie eine Seite aus',
     'app_footer_links' => 'Fußzeilen-Links',
-    'app_footer_links_desc' => 'Fügen Sie Links hinzu, die innerhalb der Seitenfußzeile angezeigt werden. Diese werden am unteren Ende der meisten Seiten angezeigt, einschließlich derjenigen, die keinen Login benötigen. Sie können die Bezeichnung "trans::<key>" verwenden, um systemdefinierte Übersetzungen zu verwenden. Beispiel: Mit "trans::common.privacy_policy" wird der übersetzte Text "Privacy Policy" bereitgestellt, und "trans::common.terms_of_service" liefert den übersetzten Text "Terms of Service".',
+    'app_footer_links_desc' => 'Fügen Sie Links hinzu, die innerhalb der Seitenfußzeile angezeigt werden. Diese werden am unteren Ende der meisten Seiten angezeigt, einschließlich derjenigen, die keinen Login benötigen. Sie können die Bezeichnung "trans::<key>" verwenden, um systemdefinierte Übersetzungen zu verwenden. Beispiel: Mit "trans::common.privacy_policy" wird der übersetzte Text "Privacy Policy" bereitgestellt und "trans::common.terms_of_service" liefert den übersetzten Text "Terms of Service".',
     'app_footer_links_label' => 'Link-Label',
     'app_footer_links_url' => 'Link-URL',
     'app_footer_links_add' => 'Fußzeilen-Link hinzufügen',
@@ -59,7 +59,7 @@ Wenn Sie nicht eingeben, wird die Anwendung auf die Standardfarbe zurückgesetzt
 
     // Registration Settings
     'reg_settings' => 'Registrierungseinstellungen',
-    'reg_enable' => 'Registrierung erlauben?',
+    'reg_enable' => 'Registrierung erlauben',
     'reg_enable_toggle' => 'Registrierung erlauben',
     'reg_enable_desc' => 'Wenn die Registrierung erlaubt ist, kann sich der Benutzer als Anwendungsbenutzer anmelden. Bei der Registrierung erhält er eine einzige, voreingestellte Benutzerrolle.',
     'reg_default_role' => 'Standard-Benutzerrolle nach Registrierung',
@@ -95,6 +95,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 
     '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_parent' => 'Übergeordnet',
     'recycle_bin_deleted_by' => 'Gelöscht von',
     'recycle_bin_deleted_at' => 'Löschzeitpunkt',
     'recycle_bin_permanently_delete' => 'Dauerhaft löschen',
@@ -107,6 +108,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 
     '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_restore_parent' => 'Übergeordneter Eintrag wiederherstellen',
     'recycle_bin_destroy_notification' => ':count Elemente wurden aus dem Papierkorb gelöscht.',
     'recycle_bin_restore_notification' => ':count Elemente wurden aus dem Papierkorb wiederhergestellt.',
 
@@ -120,6 +122,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 
     'audit_table_user' => 'Benutzer',
     'audit_table_event' => 'Ereignis',
     'audit_table_related' => 'Verknüpftes Element oder Detail',
+    'audit_table_ip' => 'IP Adresse',
     'audit_table_date' => 'Aktivitätsdatum',
     'audit_date_from' => 'Zeitraum von',
     'audit_date_to' => 'Zeitraum bis',
@@ -139,6 +142,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 
     'role_details' => 'Rollendetails',
     'role_name' => 'Rollenname',
     'role_desc' => 'Kurzbeschreibung der Rolle',
+    'role_mfa_enforced' => 'Benötigt Mehrfach-Faktor-Authentifizierung',
     'role_external_auth_id' => 'Externe Authentifizierungs-IDs',
     'role_system' => 'System-Berechtigungen',
     'role_manage_users' => 'Benutzer verwalten',
@@ -148,6 +152,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 
     'role_manage_page_templates' => 'Seitenvorlagen verwalten',
     'role_access_api' => 'Systemzugriffs-API',
     'role_manage_settings' => 'Globaleinstellungen verwalten',
+    'role_export_content' => 'Inhalt exportieren',
     'role_asset' => 'Berechtigungen',
     'roles_system_warning' => 'Beachten Sie, dass der Zugriff auf eine der oben genannten drei Berechtigungen einem Benutzer erlauben kann, seine eigenen Berechtigungen oder die Rechte anderer im System zu ändern. Weisen Sie nur Rollen, mit diesen Berechtigungen, vertrauenswürdigen Benutzern zu.',
     'role_asset_desc' => 'Diese Berechtigungen gelten für den Standard-Zugriff innerhalb des Systems. Berechtigungen für Bücher, Kapitel und Seiten überschreiben diese Berechtigungenen.',
@@ -205,6 +210,10 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 
     'users_api_tokens_create' => 'Token erstellen',
     'users_api_tokens_expires' => 'Endet',
     'users_api_tokens_docs' => 'API Dokumentation',
+    'users_mfa' => 'Multi-Faktor-Authentifizierung',
+    'users_mfa_desc' => 'Richten Sie Multi-Faktor-Authentifizierung als zusätzliche Sicherheitsstufe für Ihr Benutzerkonto ein.',
+    'users_mfa_x_methods' => ':count Methode konfiguriert|:count Methoden konfiguriert',
+    'users_mfa_configure' => 'Methoden konfigurieren',
 
     // API Tokens
     'user_api_token_create' => 'Neuen API-Token erstellen',
@@ -250,6 +259,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 
         'it' => 'Italian',
         'ja' => '日本語',
         'ko' => '한국어',
+        'lt' => 'Lietuvių Kalba',
         'lv' => 'Latviešu Valoda',
         'nl' => 'Nederlands',
         'nb' => 'Norsk (Bokmål)',
index 50f8a76a3e3817efd1ad4aedcc2ce41d0ecb734d..5d08c241a6871e79899916bd2614a5ffe5177806 100644 (file)
@@ -15,6 +15,7 @@ return [
     'alpha_dash'           => ':attribute kann nur Buchstaben, Zahlen und Bindestriche enthalten.',
     'alpha_num'            => ':attribute kann nur Buchstaben und Zahlen enthalten.',
     'array'                => ':attribute muss ein Array sein.',
+    'backup_codes'         => 'Der angegebene Code ist ungültig oder wurde bereits verwendet.',
     'before'               => ':attribute muss ein Datum vor :date sein.',
     'between'              => [
         'numeric' => ':attribute muss zwischen :min und :max liegen.',
@@ -98,6 +99,7 @@ return [
     ],
     'string'               => ':attribute muss eine Zeichenkette sein.',
     'timezone'             => ':attribute muss eine valide zeitzone sein.',
+    'totp'                 => 'Der angegebene Code ist ungültig oder abgelaufen.',
     'unique'               => ':attribute wird bereits verwendet.',
     'url'                  => ':attribute ist kein valides Format.',
     'uploaded'             => 'Die Datei konnte nicht hochgeladen werden. Der Server akzeptiert möglicherweise keine Dateien dieser Größe.',
index fbaa9a211c0f8d38615f1d8f3de6cd8e57868424..fec33bec224418fce45436e2b30ddee455726485 100644 (file)
@@ -47,6 +47,10 @@ return [
     'favourite_add_notification' => '":name" wurde zu deinen Favoriten hinzugefügt',
     'favourite_remove_notification' => '":name" wurde aus Ihren Favoriten entfernt',
 
+    // MFA
+    'mfa_setup_method_notification' => 'Multi-factor method successfully configured',
+    'mfa_remove_method_notification' => 'Multi-factor method successfully removed',
+
     // Other
     'commented_on'                => 'kommentiert',
     'permissions_update'          => 'aktualisierte Berechtigungen',
index 918598533969b4d69d2c38f9a03b42d367f79069..d09008e5a080aebb910fc7182cb487cb31ad2632 100644 (file)
@@ -73,5 +73,40 @@ return [
     'user_invite_page_welcome' => 'Willkommen bei :appName!',
     'user_invite_page_text' => 'Um die Anmeldung abzuschließen und Zugriff auf :appName zu bekommen muss noch ein Passwort festgelegt werden. Dieses wird in Zukunft zum Einloggen benötigt.',
     'user_invite_page_confirm_button' => 'Passwort bestätigen',
-    'user_invite_success' => 'Das Passwort wurde gesetzt, du hast nun Zugriff auf :appName!'
+    'user_invite_success' => 'Das Passwort wurde gesetzt, du hast nun Zugriff auf :appName!',
+
+    // Multi-factor Authentication
+    'mfa_setup' => 'Setup Multi-Factor Authentication',
+    'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
+    'mfa_setup_configured' => 'Already configured',
+    'mfa_setup_reconfigure' => 'Reconfigure',
+    'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?',
+    'mfa_setup_action' => 'Setup',
+    'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.',
+    'mfa_option_totp_title' => 'Mobile App',
+    'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
+    'mfa_option_backup_codes_title' => 'Backup Codes',
+    'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.',
+    'mfa_gen_confirm_and_enable' => 'Confirm and Enable',
+    'mfa_gen_backup_codes_title' => 'Backup Codes Setup',
+    'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.',
+    'mfa_gen_backup_codes_download' => 'Download Codes',
+    'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once',
+    'mfa_gen_totp_title' => 'Mobile App Setup',
+    'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
+    'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.',
+    'mfa_gen_totp_verify_setup' => 'Verify Setup',
+    'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:',
+    'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here',
+    'mfa_verify_access' => 'Verify Access',
+    'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.',
+    'mfa_verify_no_methods' => 'No Methods Configured',
+    'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.',
+    'mfa_verify_use_totp' => 'Verify using a mobile app',
+    'mfa_verify_use_backup_codes' => 'Verify using a backup code',
+    'mfa_verify_backup_code' => 'Backup Code',
+    'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:',
+    'mfa_verify_backup_code_enter_here' => 'Enter backup code here',
+    'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:',
+    'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.',
 ];
\ No newline at end of file
index 993d3e70c17dac3cbb53629db974888a753d6e83..898df928e5421c881957963e0c6236b85f57945d 100644 (file)
@@ -33,17 +33,18 @@ return [
     'copy' => 'Kopieren',
     'reply' => 'Antworten',
     'delete' => 'Löschen',
-    'delete_confirm' => 'Löschen Bestätigen',
+    'delete_confirm' => 'Löschen bestätigen',
     'search' => 'Suchen',
     'search_clear' => 'Suche löschen',
     'reset' => 'Zurücksetzen',
     'remove' => 'Entfernen',
     'add' => 'Hinzufügen',
+    'configure' => 'Konfigurieren',
     'fullscreen' => 'Vollbild',
-    'favourite' => 'Favorit',
+    'favourite' => 'Favoriten',
     'unfavourite' => 'Kein Favorit',
-    'next' => 'Next',
-    'previous' => 'Previous',
+    'next' => 'Nächste',
+    'previous' => 'Vorheriges',
 
     // Sort Options
     'sort_options' => 'Sortieroptionen',
@@ -56,10 +57,11 @@ return [
     'sort_updated_at' => 'Aktualisierungsdatum',
 
     // Misc
-    'deleted_user' => 'Gelöschte Benutzer',
+    'deleted_user' => 'Gelöschter Benutzer',
     'no_activity' => 'Keine Aktivitäten zum Anzeigen',
     'no_items' => 'Keine Einträge gefunden.',
     'back_to_top' => 'nach oben',
+    'skip_to_main_content' => 'Direkt zum Hauptinhalt',
     'toggle_details' => 'Details zeigen/verstecken',
     'toggle_thumbnails' => 'Thumbnails zeigen/verstecken',
     'details' => 'Details',
index 5377b1017ebbcc7db1fcf41f6d74e064b56cfdfe..3f8bccaed1c823cef9412cb56b54ecdd87921f4f 100644 (file)
@@ -36,6 +36,7 @@ return [
     'export_html' => 'HTML-Datei',
     'export_pdf' => 'PDF-Datei',
     'export_text' => 'Textdatei',
+    'export_md' => 'Markdown-Dateir',
 
     // Permissions and restrictions
     'permissions' => 'Berechtigungen',
@@ -98,6 +99,7 @@ return [
     'shelves_permissions' => 'Regal-Berechtigungen',
     'shelves_permissions_updated' => 'Regal-Berechtigungen aktualisiert',
     'shelves_permissions_active' => 'Regal-Berechtigungen aktiv',
+    'shelves_permissions_cascade_warning' => 'Die Berechtigungen in Bücherregalen werden nicht automatisch auf enthaltene Bücher kaskadiert, weil ein Buch in mehreren Regalen existieren kann. Berechtigungen können jedoch mit der unten stehenden Option in untergeordnete Bücher kopiert werden.',
     'shelves_copy_permissions_to_books' => 'Kopiere die Berechtigungen zum Buch',
     'shelves_copy_permissions' => 'Berechtigungen kopieren',
     'shelves_copy_permissions_explain' => 'Hiermit werden die Berechtigungen des aktuellen Regals auf alle enthaltenen Bücher übertragen. Überprüfe vor der Aktivierung, ob alle Berechtigungsänderungen am aktuellen Regal gespeichert wurden.',
index 5c915c537db69f069acb447d6b8e5a6177a3de5c..53d8f8359a26ef802eebc0a64f1e1ac3512a3699 100644 (file)
@@ -95,6 +95,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 
     'recycle_bin' => 'Papierkorb',
     'recycle_bin_desc' => 'Hier können Sie gelöschte Einträge 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öschter Eintrag',
+    'recycle_bin_deleted_parent' => 'Übergeordnet',
     'recycle_bin_deleted_by' => 'Gelöscht von',
     'recycle_bin_deleted_at' => 'Löschzeitpunkt',
     'recycle_bin_permanently_delete' => 'Dauerhaft löschen',
@@ -107,6 +108,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 
     'recycle_bin_restore_list' => 'Wiederherzustellende Einträge',
     'recycle_bin_restore_confirm' => 'Mit dieser Aktion wird der gelöschte Eintrag einschließlich aller untergeordneten Einträge an seinen ursprünglichen Ort wiederherstellen. Wenn der ursprüngliche Ort gelöscht wurde und sich nun im Papierkorb befindet, muss auch der übergeordnete Eintrag wiederhergestellt werden.',
     'recycle_bin_restore_deleted_parent' => 'Der übergeordnete Eintrag wurde ebenfalls gelöscht. Dieser Eintrag wird weiterhin als gelöscht zählen, bis auch der übergeordnete Eintrag wiederhergestellt wurde.',
+    'recycle_bin_restore_parent' => 'Übergeordneter Eintrag wiederherstellen',
     'recycle_bin_destroy_notification' => ':count Einträge wurden aus dem Papierkorb gelöscht.',
     'recycle_bin_restore_notification' => ':count Einträge wurden aus dem Papierkorb wiederhergestellt.',
 
@@ -120,6 +122,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 
     'audit_table_user' => 'Benutzer',
     'audit_table_event' => 'Ereignis',
     'audit_table_related' => 'Verknüpfter Eintrag oder Detail',
+    'audit_table_ip' => 'IP Address',
     'audit_table_date' => 'Aktivitätsdatum',
     'audit_date_from' => 'Zeitraum von',
     'audit_date_to' => 'Zeitraum bis',
@@ -139,6 +142,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 
     'role_details' => 'Rollendetails',
     'role_name' => 'Rollenname',
     'role_desc' => 'Kurzbeschreibung der Rolle',
+    'role_mfa_enforced' => 'Benötigt Mehrfach-Faktor-Authentifizierung',
     'role_external_auth_id' => 'Externe Authentifizierungs-IDs',
     'role_system' => 'System-Berechtigungen',
     'role_manage_users' => 'Benutzer verwalten',
@@ -148,6 +152,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 
     'role_manage_page_templates' => 'Seitenvorlagen verwalten',
     'role_access_api' => 'Systemzugriffs-API',
     'role_manage_settings' => 'Globaleinstellungen verwalten',
+    'role_export_content' => 'Inhalt exportieren',
     'role_asset' => 'Berechtigungen',
     'roles_system_warning' => 'Beachten Sie, dass der Zugriff auf eine der oben genannten drei Berechtigungen einem Benutzer erlauben kann, seine eigenen Berechtigungen oder die Rechte anderer im System zu ändern. Weisen Sie nur Rollen, mit diesen Berechtigungen, vertrauenswürdigen Benutzern zu.',
     'role_asset_desc' => 'Diese Berechtigungen gelten für den Standard-Zugriff innerhalb des Systems. Berechtigungen für Bücher, Kapitel und Seiten überschreiben diese Berechtigungenen.',
@@ -205,6 +210,10 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 
     'users_api_tokens_create' => 'Token erstellen',
     'users_api_tokens_expires' => 'Endet',
     'users_api_tokens_docs' => 'API Dokumentation',
+    'users_mfa' => 'Multi-Faktor-Authentifizierung',
+    'users_mfa_desc' => 'Richte die Multi-Faktor-Authentifizierung als zusätzliche Sicherheitsstufe für dein Benutzerkonto ein.',
+    'users_mfa_x_methods' => ':count Methode konfiguriert|:count Methoden konfiguriert',
+    'users_mfa_configure' => 'Methoden konfigurieren',
 
     // API Tokens
     'user_api_token_create' => 'Neuen API-Token erstellen',
@@ -250,6 +259,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 
         'it' => 'Italian',
         'ja' => '日本語',
         'ko' => '한국어',
+        'lt' => 'Lietuvių Kalba',
         'lv' => 'Latviešu Valoda',
         'nl' => 'Nederlands',
         'nb' => 'Norsk (Bokmål)',
index 42456da6eb3a13593cddbee4ea72643faad0dacf..7eb385ac9e53b2a62094a1eeaf281e2932557e5d 100644 (file)
@@ -15,6 +15,7 @@ return [
     'alpha_dash'           => ':attribute kann nur Buchstaben, Zahlen und Bindestriche enthalten.',
     'alpha_num'            => ':attribute kann nur Buchstaben und Zahlen enthalten.',
     'array'                => ':attribute muss ein Array sein.',
+    'backup_codes'         => 'The provided code is not valid or has already been used.',
     'before'               => ':attribute muss ein Datum vor :date sein.',
     'between'              => [
         'numeric' => ':attribute muss zwischen :min und :max liegen.',
@@ -98,6 +99,7 @@ return [
     ],
     'string'               => ':attribute muss eine Zeichenkette sein.',
     'timezone'             => ':attribute muss eine valide zeitzone sein.',
+    'totp'                 => 'The provided code is not valid or has expired.',
     'unique'               => ':attribute wird bereits verwendet.',
     'url'                  => ':attribute ist kein valides Format.',
     'uploaded'             => 'Die Datei konnte nicht hochgeladen werden. Der Server akzeptiert möglicherweise keine Dateien dieser Größe.',
index 5917de2cfb25745e4cda5a8046f310ec19da885b..50bda60bd294e2070a365b30047a0774aade83ae 100644 (file)
@@ -47,6 +47,10 @@ return [
     'favourite_add_notification' => '":name" has been added to your favourites',
     'favourite_remove_notification' => '":name" has been removed from your favourites',
 
+    // MFA
+    'mfa_setup_method_notification' => 'Multi-factor method successfully configured',
+    'mfa_remove_method_notification' => 'Multi-factor method successfully removed',
+
     // Other
     'commented_on'                => 'commented on',
     'permissions_update'          => 'updated permissions',
index d64fce93a62d90889b2297a9e4f6482ad9046475..e4d4c425b84cf9e0516755c21c0d6ce19c791937 100644 (file)
@@ -73,5 +73,40 @@ return [
     '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_success' => 'Password set, you now have access to :appName!',
+
+    // Multi-factor Authentication
+    'mfa_setup' => 'Setup Multi-Factor Authentication',
+    'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
+    'mfa_setup_configured' => 'Already configured',
+    'mfa_setup_reconfigure' => 'Reconfigure',
+    'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?',
+    'mfa_setup_action' => 'Setup',
+    'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.',
+    'mfa_option_totp_title' => 'Mobile App',
+    'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
+    'mfa_option_backup_codes_title' => 'Backup Codes',
+    'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.',
+    'mfa_gen_confirm_and_enable' => 'Confirm and Enable',
+    'mfa_gen_backup_codes_title' => 'Backup Codes Setup',
+    'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.',
+    'mfa_gen_backup_codes_download' => 'Download Codes',
+    'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once',
+    'mfa_gen_totp_title' => 'Mobile App Setup',
+    'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
+    'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.',
+    'mfa_gen_totp_verify_setup' => 'Verify Setup',
+    'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:',
+    'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here',
+    'mfa_verify_access' => 'Verify Access',
+    'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.',
+    'mfa_verify_no_methods' => 'No Methods Configured',
+    'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.',
+    'mfa_verify_use_totp' => 'Verify using a mobile app',
+    'mfa_verify_use_backup_codes' => 'Verify using a backup code',
+    'mfa_verify_backup_code' => 'Backup Code',
+    'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:',
+    'mfa_verify_backup_code_enter_here' => 'Enter backup code here',
+    'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:',
+    'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.',
 ];
\ No newline at end of file
index 1861869e3ff7ac7d8ed69becf0e3f9aa2aef9a39..f93fb034bf7e4093ef54ff1878af7bfda3bbb0cd 100644 (file)
@@ -39,6 +39,7 @@ return [
     'reset' => 'Reset',
     'remove' => 'Remove',
     'add' => 'Add',
+    'configure' => 'Configure',
     'fullscreen' => 'Fullscreen',
     'favourite' => 'Favourite',
     'unfavourite' => 'Unfavourite',
index 1d4632bce352c632bbb066cfd36fbfac953db7da..1be9c18e041818ad34df7c6bcc06563bf272f708 100644 (file)
@@ -99,6 +99,7 @@ return [
     'shelves_permissions' => 'Bookshelf Permissions',
     'shelves_permissions_updated' => 'Bookshelf Permissions Updated',
     'shelves_permissions_active' => 'Bookshelf Permissions Active',
+    'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',
     '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.',
index 789ef9d1b29e72d661aaba9663b9972ab909d90b..0ab168b66998bca0de4807f94d181b9b12ed1683 100755 (executable)
@@ -119,6 +119,7 @@ return [
     'audit_table_user' => 'User',
     'audit_table_event' => 'Event',
     'audit_table_related' => 'Related Item or Detail',
+    'audit_table_ip' => 'IP Address',
     'audit_table_date' => 'Activity Date',
     'audit_date_from' => 'Date Range From',
     'audit_date_to' => 'Date Range To',
@@ -138,6 +139,7 @@ return [
     'role_details' => 'Role Details',
     'role_name' => 'Role Name',
     'role_desc' => 'Short Description of Role',
+    'role_mfa_enforced' => 'Requires Multi-Factor Authentication',
     'role_external_auth_id' => 'External Authentication IDs',
     'role_system' => 'System Permissions',
     'role_manage_users' => 'Manage users',
@@ -147,6 +149,7 @@ return [
     'role_manage_page_templates' => 'Manage page templates',
     'role_access_api' => 'Access system API',
     'role_manage_settings' => 'Manage app settings',
+    'role_export_content' => 'Export content',
     '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.',
@@ -204,6 +207,10 @@ return [
     'users_api_tokens_create' => 'Create Token',
     'users_api_tokens_expires' => 'Expires',
     'users_api_tokens_docs' => 'API Documentation',
+    'users_mfa' => 'Multi-Factor Authentication',
+    'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
+    'users_mfa_x_methods' => ':count method configured|:count methods configured',
+    'users_mfa_configure' => 'Configure Methods',
 
     // API Tokens
     'user_api_token_create' => 'Create API Token',
@@ -249,6 +256,7 @@ return [
         'it' => 'Italian',
         'ja' => '日本語',
         'ko' => '한국어',
+        'lt' => 'Lietuvių Kalba',
         'lv' => 'Latviešu Valoda',
         'nl' => 'Nederlands',
         'nb' => 'Norsk (Bokmål)',
index 4031de2ae743b75bafd49195ff06b29a347cbf22..1963b0df2f9a9bb3d2586fc71fbc2ca04e02f97d 100644 (file)
@@ -15,6 +15,7 @@ return [
     'alpha_dash'           => 'The :attribute may only contain letters, numbers, dashes and underscores.',
     'alpha_num'            => 'The :attribute may only contain letters and numbers.',
     'array'                => 'The :attribute must be an array.',
+    'backup_codes'         => 'The provided code is not valid or has already been used.',
     'before'               => 'The :attribute must be a date before :date.',
     'between'              => [
         'numeric' => 'The :attribute must be between :min and :max.',
@@ -98,6 +99,7 @@ return [
     ],
     'string'               => 'The :attribute must be a string.',
     'timezone'             => 'The :attribute must be a valid zone.',
+    'totp'                 => 'The provided code is not valid or has expired.',
     'unique'               => 'The :attribute has already been taken.',
     'url'                  => 'The :attribute format is invalid.',
     'uploaded'             => 'The file could not be uploaded. The server may not accept files of this size.',
index b64ada3a4c8f925f468e852942b77db1eb61a04e..a3449269d2ba234374ab6009268b8cee0593c91c 100644 (file)
@@ -47,6 +47,10 @@ return [
     'favourite_add_notification' => '".name" ha sido añadido a sus favoritos',
     'favourite_remove_notification' => '".name" ha sido eliminado de sus favoritos',
 
+    // MFA
+    'mfa_setup_method_notification' => 'Método de Autenticación en Dos Pasos configurado correctamente',
+    'mfa_remove_method_notification' => 'Método de Autenticación en Dos Pasos eliminado correctamente',
+
     // Other
     'commented_on'                => 'comentada el',
     'permissions_update'          => 'permisos actualizados',
index 25fc5b650d2d62eafd641de2a786879b8adad640..70f069a12fe6d44326bcebf03837fffbedece7a7 100644 (file)
@@ -73,5 +73,40 @@ return [
     'user_invite_page_welcome' => '¡Bienvenido a :appName!',
     'user_invite_page_text' => 'Para completar la cuenta y tener acceso es necesario que configure una contraseña que se utilizará para entrar en :appName en futuros accesos.',
     'user_invite_page_confirm_button' => 'Confirmar Contraseña',
-    'user_invite_success' => '¡Contraseña guardada, ya tiene acceso a :appName!'
+    'user_invite_success' => '¡Contraseña guardada, ya tiene acceso a :appName!',
+
+    // Multi-factor Authentication
+    'mfa_setup' => 'Configurar Autenticación en Dos Pasos',
+    'mfa_setup_desc' => 'La autenticación en dos pasos añade una capa de seguridad adicional a tu cuenta de usuario.',
+    'mfa_setup_configured' => 'Ya está configurado',
+    'mfa_setup_reconfigure' => 'Reconfigurar',
+    'mfa_setup_remove_confirmation' => '¿Está seguro de que desea eliminar este método de autenticación en dos pasos?',
+    'mfa_setup_action' => 'Configuración',
+    'mfa_backup_codes_usage_limit_warning' => 'Quedan menos de 5 códigos de respaldo, Por favor, genera y almacena un nuevo conjunto antes de que te quedes sin códigos para evitar que te bloquees fuera de tu cuenta.',
+    'mfa_option_totp_title' => 'Aplicación para móviles',
+    'mfa_option_totp_desc' => 'Para utilizar la autenticación en dos pasos necesitarás una aplicación móvil que soporte TOTP como Google Authenticator, Authy o Microsoft Authenticator.',
+    'mfa_option_backup_codes_title' => 'Códigos de Respaldo',
+    'mfa_option_backup_codes_desc' => 'Almacena de forma segura un conjunto de códigos de respaldo de un solo uso que puedes introducir para verificar tu identidad.',
+    'mfa_gen_confirm_and_enable' => 'Confirmar y Activar',
+    'mfa_gen_backup_codes_title' => 'Configuración de Códigos de Respaldo',
+    'mfa_gen_backup_codes_desc' => 'Guarda la siguiente lista de códigos en un lugar seguro. Al acceder al sistema podrás usar uno de los códigos como un segundo mecanismo de autenticación.',
+    'mfa_gen_backup_codes_download' => 'Descargar Códigos',
+    'mfa_gen_backup_codes_usage_warning' => 'Cada código sólo puede utilizarse una vez',
+    'mfa_gen_totp_title' => 'Configuración de Aplicación móvil',
+    'mfa_gen_totp_desc' => 'Para utilizar la autenticación en dos pasos necesitarás una aplicación móvil que soporte TOTP como Google Authenticator, Authy o Microsoft Authenticator.',
+    'mfa_gen_totp_scan' => 'Escanea el código QR mostrado a continuación usando tu aplicación de autenticación preferida para empezar.',
+    'mfa_gen_totp_verify_setup' => 'Verificar Configuración',
+    'mfa_gen_totp_verify_setup_desc' => 'Verifica que todo está funcionando introduciendo un código, generado en tu aplicación de autenticación, en el campo de texto a continuación:',
+    'mfa_gen_totp_provide_code_here' => 'Introduce aquí tu código generado por la aplicación',
+    'mfa_verify_access' => 'Verificar Acceso',
+    'mfa_verify_access_desc' => 'Tu cuenta de usuario requiere que confirmes tu identidad a través de un nivel adicional de verificación antes de que te conceda el acceso. Verifica tu identidad usando uno de los métodos configurados para continuar.',
+    'mfa_verify_no_methods' => 'No hay Métodos Configurados',
+    'mfa_verify_no_methods_desc' => 'No se han encontrado métodos de autenticación en dos pasos para tu cuenta. Tendrás que configurar al menos un método antes de obtener acceso.',
+    'mfa_verify_use_totp' => 'Verificar usando una aplicación móvil',
+    'mfa_verify_use_backup_codes' => 'Verificar usando un código de respaldo',
+    'mfa_verify_backup_code' => 'Códigos de Respaldo',
+    'mfa_verify_backup_code_desc' => 'Introduzca uno de sus códigos de respaldo restantes a continuación:',
+    'mfa_verify_backup_code_enter_here' => 'Introduce el código de respaldo aquí',
+    'mfa_verify_totp_desc' => 'Introduzca el código, generado con tu aplicación móvil, a continuación:',
+    'mfa_setup_login_notification' => 'Método de dos factores configurado. Por favor, inicia sesión de nuevo utilizando el método configurado.',
 ];
\ No newline at end of file
index d19277402a858898eaa578aa6509c0a1321725b1..b8514a87628dc30b35cd21744abd4ba510ddbf77 100644 (file)
@@ -39,6 +39,7 @@ return [
     'reset' => 'Resetear',
     'remove' => 'Remover',
     'add' => 'Añadir',
+    'configure' => 'Configurar',
     'fullscreen' => 'Pantalla completa',
     'favourite' => 'Añadir a favoritos',
     'unfavourite' => 'Eliminar de favoritos',
@@ -60,6 +61,7 @@ return [
     'no_activity' => 'Ninguna actividad para mostrar',
     'no_items' => 'No hay elementos disponibles',
     'back_to_top' => 'Volver arriba',
+    'skip_to_main_content' => 'Ir al contenido principal',
     'toggle_details' => 'Alternar detalles',
     'toggle_thumbnails' => 'Alternar miniaturas',
     'details' => 'Detalles',
index 2478d86893c26095eada6154901b6b28f70252a8..3325557b7860b30625fb8446355ff07d0b197909 100644 (file)
@@ -6,9 +6,9 @@
 return [
 
     // Shared
-    'recently_created' => 'Recientemente creado',
-    'recently_created_pages' => 'Páginas recientemente creadas',
-    'recently_updated_pages' => 'Páginas recientemente actualizadas',
+    'recently_created' => 'Creado Recientemente',
+    'recently_created_pages' => 'Páginas creadas recientemente',
+    'recently_updated_pages' => 'Páginas actualizadas recientemente',
     'recently_created_chapters' => 'Capítulos recientemente creados',
     'recently_created_books' => 'Libros recientemente creados',
     'recently_created_shelves' => 'Estantes recientemente creados',
@@ -36,6 +36,7 @@ return [
     'export_html' => 'Archivo web',
     'export_pdf' => 'Archivo PDF',
     'export_text' => 'Archivo de texto',
+    'export_md' => 'Archivo Markdown',
 
     // Permissions and restrictions
     'permissions' => 'Permisos',
@@ -98,6 +99,7 @@ return [
     'shelves_permissions' => 'Permisos del estante',
     'shelves_permissions_updated' => 'Permisos del estante actualizados',
     'shelves_permissions_active' => 'Permisos del estante activos',
+    'shelves_permissions_cascade_warning' => 'Los permisos en los estantes no se aplican automáticamente a los libros contenidos. Esto se debe a que un libro puede existir en múltiples estantes. Sin embargo, los permisos pueden ser aplicados a los libros del estante utilizando la opción a continuación.',
     'shelves_copy_permissions_to_books' => 'Copiar permisos a los libros',
     'shelves_copy_permissions' => 'Copiar permisos',
     'shelves_copy_permissions_explain' => 'Esto aplicará los ajustes de permisos de este estante para todos sus libros. Antes de activarlo, asegúrese de que todos los cambios de permisos para este estante han sido guardados.',
index 0010e7cae457d19328900d346bb9524b6649b35c..bf9c89a63c79a99a320223ce1504e2abac676989 100644 (file)
@@ -92,6 +92,7 @@ return [
     '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_parent' => 'Superior',
     'recycle_bin_deleted_by' => 'Eliminado por',
     'recycle_bin_deleted_at' => 'Fecha de eliminación',
     'recycle_bin_permanently_delete' => 'Eliminar permanentemente',
@@ -104,6 +105,7 @@ return [
     '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_restore_parent' => 'Restaurar Superior',
     '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.',
 
@@ -117,6 +119,7 @@ return [
     'audit_table_user' => 'Usuario',
     'audit_table_event' => 'Evento',
     'audit_table_related' => 'Elemento o detalle relacionados',
+    'audit_table_ip' => 'Dirección IP',
     'audit_table_date' => 'Fecha de la actividad',
     'audit_date_from' => 'Rango de fecha desde',
     'audit_date_to' => 'Rango de fecha hasta',
@@ -136,6 +139,7 @@ return [
     'role_details' => 'Detalles de rol',
     'role_name' => 'Nombre de rol',
     'role_desc' => 'Descripción corta de rol',
+    'role_mfa_enforced' => 'Requiere Autenticación en Dos Pasos',
     'role_external_auth_id' => 'ID externo de autenticación',
     'role_system' => 'Permisos de sistema',
     'role_manage_users' => 'Gestionar usuarios',
@@ -145,6 +149,7 @@ return [
     'role_manage_page_templates' => 'Administrar plantillas',
     'role_access_api' => 'API de sistema de acceso',
     'role_manage_settings' => 'Gestionar ajustes de la aplicación',
+    'role_export_content' => 'Exportar contenido',
     'role_asset' => 'Permisos de contenido',
     '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 contenidos del sistema. Los permisos de Libros, Capítulos y Páginas sobreescribiran estos permisos.',
@@ -202,6 +207,10 @@ return [
     'users_api_tokens_create' => 'Crear token',
     'users_api_tokens_expires' => 'Expira',
     'users_api_tokens_docs' => 'Documentación API',
+    'users_mfa' => 'Autenticación en Dos Pasos',
+    'users_mfa_desc' => 'La autenticación en dos pasos añade una capa de seguridad adicional a tu cuenta.',
+    'users_mfa_x_methods' => ':count método configurado|:count métodos configurados',
+    'users_mfa_configure' => 'Configurar Métodos',
 
     // API Tokens
     'user_api_token_create' => 'Crear token API',
@@ -247,6 +256,7 @@ return [
         'it' => 'Italian',
         'ja' => '日本語',
         'ko' => '한국어',
+        'lt' => 'Lietuvių Kalba',
         'lv' => 'Latviešu Valoda',
         'nl' => 'Nederlands',
         'nb' => 'Norsk (Bokmål)',
index 450e923753b001d77f58a84a31a33c56c991dcf0..177eb812c47d5cff9eb84e147a3143100f05aad5 100644 (file)
@@ -15,6 +15,7 @@ return [
     'alpha_dash'           => 'El :attribute solo puede contener letras, números y guiones.',
     'alpha_num'            => 'El :attribute solo puede contener letras y números.',
     'array'                => 'El :attribute debe de ser un array.',
+    'backup_codes'         => 'El código suministrado no es válido o ya ha sido utilizado.',
     'before'               => 'El :attribute debe ser una fecha anterior a  :date.',
     'between'              => [
         'numeric' => 'El :attribute debe estar entre :min y :max.',
@@ -98,6 +99,7 @@ return [
     ],
     'string'               => 'El atributo :attribute debe ser una cadena de texto.',
     'timezone'             => 'El atributo :attribute debe ser una zona válida.',
+    'totp'                 => 'El código suministrado no es válido o ya ha expirado.',
     'unique'               => 'El atributo :attribute ya ha sido tomado.',
     'url'                  => 'El atributo :attribute tiene un formato inválido.',
     'uploaded'             => 'El archivo no ha podido subirse. Es posible que el servidor no acepte archivos de este tamaño.',
index 2ee087b71fd7241f10667b3e98d1cbcb41dfbffa..861115fc54604f3b59a317594a51ae4923864ca2 100644 (file)
@@ -47,6 +47,10 @@ return [
     'favourite_add_notification' => '".name" se añadió a sus favoritos',
     'favourite_remove_notification' => '".name" se eliminó de sus favoritos',
 
+    // MFA
+    'mfa_setup_method_notification' => 'Método de Autenticación en Dos Pasos configurado correctamente',
+    'mfa_remove_method_notification' => 'Método de Autenticación en Dos Pasos eliminado correctamente',
+
     // Other
     'commented_on'                => 'comentado',
     'permissions_update'          => 'permisos actualizados',
index 2f957f46d6f18d108cc5453f4b8faec60483efd4..c57b267469582e8c03ef0bf4d960297ca5338198 100644 (file)
@@ -73,5 +73,40 @@ return [
     'user_invite_page_welcome' => 'Bienvenido a :appName!',
     'user_invite_page_text' => 'Para finalizar la cuenta y tener acceso debe establcer una contraseña que utilizará para ingresar a :appName en visitas futuras.',
     'user_invite_page_confirm_button' => 'Confirmar Contraseña',
-    'user_invite_success' => 'Contraseña establecida, ahora tiene acceso a :appName!'
+    'user_invite_success' => 'Contraseña establecida, ahora tiene acceso a :appName!',
+
+    // Multi-factor Authentication
+    'mfa_setup' => 'Configurar Autenticación en Dos Pasos',
+    'mfa_setup_desc' => 'La autenticación en dos pasos añade una capa de seguridad adicional a tu cuenta de usuario.',
+    'mfa_setup_configured' => 'Ya está configurado',
+    'mfa_setup_reconfigure' => 'Reconfigurar',
+    'mfa_setup_remove_confirmation' => '¿Está seguro de que desea eliminar este método de autenticación de dos pasos?',
+    'mfa_setup_action' => 'Configuración',
+    'mfa_backup_codes_usage_limit_warning' => 'Quedan menos de 5 códigos de respaldo, Por favor, genera y almacena un nuevo conjunto antes de que te quedes sin códigos para evitar que te bloquees fuera de tu cuenta.',
+    'mfa_option_totp_title' => 'Aplicación para móviles',
+    'mfa_option_totp_desc' => 'Para utilizar la autenticación de dos pasos necesitarás una aplicación móvil que soporte TOTP como Google Authenticator, Authy o Microsoft Authenticator.',
+    'mfa_option_backup_codes_title' => 'Códigos de Respaldo',
+    'mfa_option_backup_codes_desc' => 'Almacena de forma segura un conjunto de códigos de respaldo de un solo uso que puedes introducir para verificar tu identidad.',
+    'mfa_gen_confirm_and_enable' => 'Confirmar y Activar',
+    'mfa_gen_backup_codes_title' => 'Configuración de Códigos de Respaldo',
+    'mfa_gen_backup_codes_desc' => 'Guarda la siguiente lista de códigos en un lugar seguro. Al acceder al sistema podrás usar uno de los códigos como un segundo mecanismo de autenticación.',
+    'mfa_gen_backup_codes_download' => 'Descargar Códigos',
+    'mfa_gen_backup_codes_usage_warning' => 'Cada código sólo puede utilizarse una vez',
+    'mfa_gen_totp_title' => 'Configuración de Aplicación móvil',
+    'mfa_gen_totp_desc' => 'Para utilizar la autenticación de dos pasos necesitarás una aplicación móvil que soporte TOTP como Google Authenticator, Authy o Microsoft Authenticator.',
+    'mfa_gen_totp_scan' => 'Escanea el código QR mostrado a continuación usando tu aplicación de autenticación preferida para empezar.',
+    'mfa_gen_totp_verify_setup' => 'Verificar Configuración',
+    'mfa_gen_totp_verify_setup_desc' => 'Verifica que todo está funcionando introduciendo un código, generado en tu aplicación de autenticación, en el campo de texto a continuación:',
+    'mfa_gen_totp_provide_code_here' => 'Introduce aquí tu código generado por la aplicación',
+    'mfa_verify_access' => 'Verificar Acceso',
+    'mfa_verify_access_desc' => 'Tu cuenta de usuario requiere que confirmes tu identidad a través de un nivel adicional de verificación antes de que te conceda el acceso. Verifica tu identidad usando uno de los métodos configurados para continuar.',
+    'mfa_verify_no_methods' => 'No hay Métodos Configurados',
+    'mfa_verify_no_methods_desc' => 'No se han encontrado métodos de autenticación de dos pasos para tu cuenta. Tendrás que configurar al menos un método antes de obtener acceso.',
+    'mfa_verify_use_totp' => 'Verificar usando una aplicación móvil',
+    'mfa_verify_use_backup_codes' => 'Verificar usando un código de respaldo',
+    'mfa_verify_backup_code' => 'Códigos de Respaldo',
+    'mfa_verify_backup_code_desc' => 'Introduzca uno de sus códigos de respaldo restantes a continuación:',
+    'mfa_verify_backup_code_enter_here' => 'Introduce el código de respaldo aquí',
+    'mfa_verify_totp_desc' => 'Introduzca el código, generado con tu aplicación móvil, a continuación:',
+    'mfa_setup_login_notification' => 'Método de dos factores configurado. Por favor, inicia sesión de nuevo utilizando el método configurado.',
 ];
\ No newline at end of file
index 190d99c1ca7a5db4dd20555c4c991718d52568a3..05c76cb111c5a2ed11e51bbe7f0af55e0f911500 100644 (file)
@@ -39,6 +39,7 @@ return [
     'reset' => 'Restablecer',
     'remove' => 'Remover',
     'add' => 'Agregar',
+    'configure' => 'Configurar',
     'fullscreen' => 'Pantalla completa',
     'favourite' => 'Favoritos',
     'unfavourite' => 'Eliminar de favoritos',
@@ -60,6 +61,7 @@ return [
     'no_activity' => 'Ninguna actividad para mostrar',
     'no_items' => 'No hay elementos disponibles',
     'back_to_top' => 'Volver arriba',
+    'skip_to_main_content' => 'Ir al contenido principal',
     'toggle_details' => 'Alternar detalles',
     'toggle_thumbnails' => 'Alternar miniaturas',
     'details' => 'Detalles',
index 02c2dcbb91907dacba82138c167ef618b692a623..5e71ba266a7b06f0eb22c09f2760ccf64a4b1993 100644 (file)
@@ -36,6 +36,7 @@ return [
     'export_html' => 'Archivo web contenido',
     'export_pdf' => 'Archivo PDF',
     'export_text' => 'Archivo de texto plano',
+    'export_md' => 'Archivo Markdown',
 
     // Permissions and restrictions
     'permissions' => 'Permisos',
@@ -98,6 +99,7 @@ return [
     'shelves_permissions' => 'Permisos del Estante',
     'shelves_permissions_updated' => 'Permisos del Estante actualizados',
     'shelves_permissions_active' => 'Permisos Activos del Estante',
+    'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',
     'shelves_copy_permissions_to_books' => 'Copiar Permisos a los Libros',
     'shelves_copy_permissions' => 'Copiar Permisos',
     'shelves_copy_permissions_explain' => 'Esta acción aplicará los permisos de este estante a todos los libros contenidos en él. Antes de activarlos, asegúrese que los cambios a los permisos de este estante estén guardados.',
index 962ef89a4d213a8b964a16d9ec1f0349bc5559f4..99ec4c219cbab03775834427ba8d6fd99d8fcd64 100644 (file)
@@ -92,6 +92,7 @@ return [
     '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_parent' => 'Superior',
     'recycle_bin_deleted_by' => 'Eliminado por',
     'recycle_bin_deleted_at' => 'Fecha de eliminación',
     'recycle_bin_permanently_delete' => 'Eliminar permanentemente',
@@ -104,6 +105,7 @@ return [
     '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_restore_parent' => 'Restaurar Superior',
     'recycle_bin_destroy_notification' => 'Eliminados :count elementos de la papelera de reciclaje.',
     'recycle_bin_restore_notification' => 'Restaurados :count elementos desde la papelera de reciclaje.',
 
@@ -117,6 +119,7 @@ return [
     'audit_table_user' => 'Usuario',
     'audit_table_event' => 'Evento',
     'audit_table_related' => 'Elemento o detalle relacionados',
+    'audit_table_ip' => 'IP Address',
     'audit_table_date' => 'Fecha de la Actividad',
     'audit_date_from' => 'Inicio del Rango de Fecha',
     'audit_date_to' => 'Final del Rango de Fecha',
@@ -136,6 +139,7 @@ return [
     'role_details' => 'Detalles de rol',
     'role_name' => 'Nombre de rol',
     'role_desc' => 'Descripción corta de rol',
+    'role_mfa_enforced' => 'Requiere Autenticación en Dos Pasos',
     'role_external_auth_id' => 'IDs de Autenticación Externa',
     'role_system' => 'Permisos de sistema',
     'role_manage_users' => 'Gestionar usuarios',
@@ -146,6 +150,7 @@ return [
     'role_manage_page_templates' => 'Gestionar las plantillas de páginas',
     'role_access_api' => 'API de sistema de acceso',
     'role_manage_settings' => 'Gestionar ajustes de activos',
+    'role_export_content' => 'Exportar contenido',
     '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 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.',
@@ -203,6 +208,10 @@ return [
     'users_api_tokens_create' => 'Crear token',
     'users_api_tokens_expires' => 'Expira',
     'users_api_tokens_docs' => 'Documentación API',
+    'users_mfa' => 'Autenticación en Dos Pasos',
+    'users_mfa_desc' => 'La autenticación en dos pasos añade una capa de seguridad adicional a tu cuenta.',
+    'users_mfa_x_methods' => ':count método configurado|:count métodos configurados',
+    'users_mfa_configure' => 'Configurar Métodos',
 
     // API Tokens
     'user_api_token_create' => 'Crear token API',
@@ -248,6 +257,7 @@ return [
         'it' => 'Italian',
         'ja' => '日本語',
         'ko' => '한국어',
+        'lt' => 'Lietuvių Kalba',
         'lv' => 'Latviešu Valoda',
         'nl' => 'Nederlands',
         'nb' => 'Norsk (Bokmål)',
index c3f5d83dd69c684752fb3cb7e8071d50ac43b9c6..2cc8ed9bf442d69a50a06743521f709b5790bbeb 100644 (file)
@@ -15,6 +15,7 @@ return [
     'alpha_dash'           => 'El :attribute solo puede contener letras, números y guiones.',
     'alpha_num'            => 'El :attribute solo puede contener letras y número.',
     'array'                => 'El :attribute debe de ser un array.',
+    'backup_codes'         => 'El código suministrado no es válido o ya ha sido utilizado.',
     'before'               => 'El :attribute debe ser una fecha anterior a  :date.',
     'between'              => [
         'numeric' => 'El :attribute debe estar entre :min y :max.',
@@ -98,6 +99,7 @@ return [
     ],
     'string'               => 'El atributo :attribute debe ser una cadena.',
     'timezone'             => 'El atributo :attribute debe ser una zona válida.',
+    'totp'                 => 'El código suministrado no es válido o ya ha expirado.',
     'unique'               => 'El atributo :attribute ya ha sido tomado.',
     'url'                  => 'El atributo :attribute tiene un formato inválido.',
     'uploaded'             => 'El archivo no se pudo subir. Puede ser que el servidor no acepte archivos de este tamaño.',
index 5917de2cfb25745e4cda5a8046f310ec19da885b..43b6b4789c9bdb8039d9f12b2c949115c5d55578 100644 (file)
@@ -6,48 +6,52 @@
 return [
 
     // Pages
-    'page_create'                 => 'created page',
-    'page_create_notification'    => 'Page Successfully Created',
-    'page_update'                 => 'updated page',
-    'page_update_notification'    => 'Page Successfully Updated',
-    'page_delete'                 => 'deleted page',
-    'page_delete_notification'    => 'Page Successfully Deleted',
-    'page_restore'                => 'restored page',
-    'page_restore_notification'   => 'Page Successfully Restored',
-    'page_move'                   => 'moved page',
+    'page_create'                 => 'صفحه ایجاد شده',
+    'page_create_notification'    => 'صفحه با موفقیت ایجاد شد',
+    'page_update'                 => 'صفحه بروز شده',
+    'page_update_notification'    => 'صفحه با موفقیت به روزرسانی شد',
+    'page_delete'                 => 'حذف صفحه',
+    'page_delete_notification'    => 'صفحه با موفقیت حذف شد',
+    'page_restore'                => 'بازیابی صفحه',
+    'page_restore_notification'   => 'صفحه با موفقیت بازیابی شد',
+    'page_move'                   => 'انتقال صفحه',
 
     // Chapters
-    'chapter_create'              => 'created chapter',
-    'chapter_create_notification' => 'Chapter Successfully Created',
-    'chapter_update'              => 'updated chapter',
-    'chapter_update_notification' => 'Chapter Successfully Updated',
-    'chapter_delete'              => 'deleted chapter',
-    'chapter_delete_notification' => 'Chapter Successfully Deleted',
-    'chapter_move'                => 'moved chapter',
+    'chapter_create'              => 'ایجاد فصل',
+    'chapter_create_notification' => 'فصل با موفقیت ایجاد شد',
+    'chapter_update'              => 'به روزرسانی فصل',
+    'chapter_update_notification' => 'فصل با موفقیت به روزرسانی شد',
+    'chapter_delete'              => 'حذف فصل',
+    'chapter_delete_notification' => 'فصل با موفقیت حذف شد',
+    'chapter_move'                => 'انتقال فصل',
 
     // Books
-    'book_create'                 => 'created book',
-    'book_create_notification'    => 'Book Successfully Created',
-    'book_update'                 => 'updated book',
-    'book_update_notification'    => 'Book Successfully Updated',
-    'book_delete'                 => 'deleted book',
-    'book_delete_notification'    => 'Book Successfully Deleted',
-    'book_sort'                   => 'sorted book',
-    'book_sort_notification'      => 'Book Successfully Re-sorted',
+    'book_create'                 => 'ایجاد کتاب',
+    'book_create_notification'    => 'کتاب با موفقیت ایجاد شد',
+    'book_update'                 => 'به روزرسانی کتاب',
+    'book_update_notification'    => 'کتاب با موفقیت به روزرسانی شد',
+    'book_delete'                 => 'حذف کتاب',
+    'book_delete_notification'    => 'کتاب با موفقیت حذف شد',
+    'book_sort'                   => 'مرتب سازی کتاب',
+    'book_sort_notification'      => 'کتاب با موفقیت مرتب سازی شد',
 
     // Bookshelves
-    'bookshelf_create'            => 'created Bookshelf',
-    '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'            => 'ایجاد قفسه کتاب',
+    'bookshelf_create_notification'    => 'قفسه کتاب با موفقیت ایجاد شد',
+    'bookshelf_update'                 => 'به روزرسانی قفسه کتاب',
+    'bookshelf_update_notification'    => 'قفسه کتاب با موفقیت به روزرسانی شد',
+    'bookshelf_delete'                 => 'حذف قفسه کتاب',
+    'bookshelf_delete_notification'    => 'قفسه کتاب با موفقیت حذف شد',
 
     // Favourites
-    'favourite_add_notification' => '":name" has been added to your favourites',
-    'favourite_remove_notification' => '":name" has been removed from your favourites',
+    'favourite_add_notification' => '":name" به علاقه مندی های شما اضافه شد',
+    'favourite_remove_notification' => '":name" از علاقه مندی های شما حذف شد',
+
+    // MFA
+    'mfa_setup_method_notification' => 'Multi-factor method successfully configured',
+    'mfa_remove_method_notification' => 'Multi-factor method successfully removed',
 
     // Other
-    'commented_on'                => 'commented on',
-    'permissions_update'          => 'updated permissions',
+    'commented_on'                => 'ثبت دیدگاه',
+    'permissions_update'          => 'به روزرسانی مجوزها',
 ];
index d64fce93a62d90889b2297a9e4f6482ad9046475..4f950b77f0482e8e28cfa77333d6821f6b1e18ec 100644 (file)
  */
 return [
 
-    'failed' => 'These credentials do not match our records.',
-    'throttle' => 'Too many login attempts. Please try again in :seconds seconds.',
+    'failed' => 'مشخصات وارد شده با اطلاعات ما سازگار نیست.',
+    'throttle' => 'دفعات تلاش شما برای ورود بیش از حد مجاز است. لطفا پس از :seconds ثانیه مجددا تلاش فرمایید.',
 
     // Login & Register
-    'sign_up' => 'Sign up',
-    'log_in' => 'Log in',
-    'log_in_with' => 'Login with :socialDriver',
-    'sign_up_with' => 'Sign up with :socialDriver',
-    'logout' => 'Logout',
+    'sign_up' => 'ثبت نام',
+    'log_in' => 'ورود',
+    'log_in_with' => 'ورود با :socialDriver',
+    'sign_up_with' => 'ثبت نام با :socialDriver',
+    'logout' => 'خروج',
 
-    'name' => 'Name',
-    'username' => 'Username',
-    'email' => 'Email',
-    'password' => 'Password',
-    'password_confirm' => 'Confirm Password',
-    'password_hint' => 'Must be over 7 characters',
-    'forgot_password' => 'Forgot Password?',
-    'remember_me' => 'Remember Me',
-    'ldap_email_hint' => 'Please enter an email to use for this account.',
-    'create_account' => 'Create Account',
-    'already_have_account' => 'Already have an account?',
-    'dont_have_account' => 'Don\'t have an account?',
-    'social_login' => 'Social Login',
-    'social_registration' => 'Social Registration',
-    'social_registration_text' => 'Register and sign in using another service.',
+    'name' => 'نام',
+    'username' => 'نام کاربری',
+    'email' => 'پست الکترونیک',
+    'password' => 'کلمه عبور',
+    'password_confirm' => 'تایید کلمه عبور',
+    'password_hint' => 'باید بیش از 7 کاراکتر باشد',
+    'forgot_password' => 'کلمه عبور خود را فراموش کرده اید؟',
+    'remember_me' => 'مرا به خاطر بسپار',
+    'ldap_email_hint' => 'لطفا برای استفاده از این حساب کاربری پست الکترونیک وارد نمایید.',
+    'create_account' => 'ایجاد حساب کاربری',
+    'already_have_account' => 'قبلا ثبت نام نموده اید؟',
+    'dont_have_account' => 'حساب کاربری ندارید؟',
+    'social_login' => 'ورود از طریق شبکه اجتماعی',
+    'social_registration' => 'ثبت نام از طریق شبکه اجتماعی',
+    'social_registration_text' => 'با استفاده از سرویس دیگری ثبت نام نموده و وارد سیستم شوید.',
 
-    'register_thanks' => 'Thanks for registering!',
-    'register_confirm' => 'Please check your email and click the confirmation button to access :appName.',
-    'registrations_disabled' => 'Registrations are currently disabled',
-    'registration_email_domain_invalid' => 'That email domain does not have access to this application',
-    'register_success' => 'Thanks for signing up! You are now registered and signed in.',
+    'register_thanks' => 'از ثبت نام شما متشکریم!',
+    'register_confirm' => 'لطفا پست الکترونیک خود را بررسی نموده و برای دسترسی به:appName دکمه تایید را کلیک نمایید.',
+    'registrations_disabled' => 'ثبت نام در حال حاضر غیر فعال است',
+    'registration_email_domain_invalid' => 'دامنه پست الکترونیک به این برنامه دسترسی ندارد',
+    'register_success' => 'از ثبت نام شما سپاسگزاریم! شما اکنون ثبت نام کرده و وارد سیستم شده اید.',
 
 
     // Password Reset
-    'reset_password' => 'Reset Password',
-    'reset_password_send_instructions' => 'Enter your email below and you will be sent an email with a password reset link.',
-    'reset_password_send_button' => 'Send Reset Link',
-    'reset_password_sent' => 'A password reset link will be sent to :email if that email address is found in the system.',
-    'reset_password_success' => 'Your password has been successfully reset.',
-    'email_reset_subject' => 'Reset your :appName password',
-    'email_reset_text' => 'You are receiving this email because we received a password reset request for your account.',
-    'email_reset_not_requested' => 'If you did not request a password reset, no further action is required.',
+    'reset_password' => 'بازنشانی کلمه عبور',
+    'reset_password_send_instructions' => 'پست الکترونیک خود را در کادر زیر وارد نموده تا یک پیام حاوی لینک بازنشانی کلمه عبور دریافت نمایید.',
+    'reset_password_send_button' => 'ارسال لینک بازنشانی',
+    'reset_password_sent' => 'در صورت موجود بودن پست الکترونیک، یک لینک بازنشانی کلمه عبور برای شما ارسال خواهد شد.',
+    'reset_password_success' => 'کلمه عبور شما با موفقیت بازنشانی شد.',
+    'email_reset_subject' => 'بازنشانی کلمه عبور :appName',
+    'email_reset_text' => 'شما این پیام را به علت درخواست بازنشانی کلمه عبور دریافت می نمایید.',
+    'email_reset_not_requested' => 'در صورتی که درخواست بازنشانی کلمه عبور از سمت شما نمی باشد، نیاز به انجام هیچ فعالیتی ندارید.',
 
 
     // Email Confirmation
-    'email_confirm_subject' => 'Confirm your email on :appName',
-    'email_confirm_greeting' => 'Thanks for joining :appName!',
-    'email_confirm_text' => 'Please confirm your email address by clicking the button below:',
-    'email_confirm_action' => 'Confirm Email',
-    'email_confirm_send_error' => 'Email confirmation required but the system could not send the email. Contact the admin to ensure email is set up correctly.',
-    'email_confirm_success' => 'Your email has been confirmed!',
-    'email_confirm_resent' => 'Confirmation email resent, Please check your inbox.',
+    'email_confirm_subject' => 'پست الکترونیک خود را در:appName تایید نمایید',
+    'email_confirm_greeting' => 'برای پیوستن به :appName متشکریم!',
+    'email_confirm_text' => 'لطفا با کلیک بر روی دکمه زیر پست الکترونیک خود را تایید نمایید:',
+    'email_confirm_action' => 'تایید پست الکترونیک',
+    'email_confirm_send_error' => 'تایید پست الکترونیک الزامی می باشد، اما سیستم قادر به ارسال پیام نمی باشد.',
+    'email_confirm_success' => 'پست الکترونیک شما تایید گردیده است!',
+    'email_confirm_resent' => 'پیام تایید پست الکترونیک مجدد ارسال گردید، لطفا صندوق ورودی خود را بررسی نمایید.',
 
-    'email_not_confirmed' => 'Email Address Not Confirmed',
-    'email_not_confirmed_text' => 'Your email address has not yet been confirmed.',
-    'email_not_confirmed_click_link' => 'Please click the link in the email that was sent shortly after you registered.',
-    'email_not_confirmed_resend' => 'If you cannot find the email you can re-send the confirmation email by submitting the form below.',
-    'email_not_confirmed_resend_button' => 'Resend Confirmation Email',
+    'email_not_confirmed' => 'پست الکترونیک تایید نشده است',
+    'email_not_confirmed_text' => 'پست الکترونیک شما هنوز تایید نشده است.',
+    'email_not_confirmed_click_link' => 'لطفا بر روی لینک موجود در پیامی که بلافاصله پس از ثبت نام ارسال شده است کلیک نمایید.',
+    'email_not_confirmed_resend' => 'در صورتی که نمی توانید پیام را پیدا کنید، می توانید با ارسال فرم زیر، پیام تایید را مجدد دریافت نمایید.',
+    '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' => 'از شما برای پیوستن به :appName دعوت شده است!',
+    'user_invite_email_greeting' => 'حساب کاربری برای شما در :appName ایجاد شده است.',
+    '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 دسترسی دارید!',
+
+    // Multi-factor Authentication
+    'mfa_setup' => 'تنظیم احراز هویت چند مرحله‌ای',
+    'mfa_setup_desc' => 'تنظیم احراز هویت چند مرحله ای یک لایه امنیتی دیگر به حساب شما اضافه میکند.',
+    'mfa_setup_configured' => 'هم اکنون تنظیم شده است.',
+    'mfa_setup_reconfigure' => 'تنظیم مجدد',
+    'mfa_setup_remove_confirmation' => 'از حذف احراز هویت چند مرحله ای اطمینان دارید؟',
+    'mfa_setup_action' => 'تنظیم',
+    'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.',
+    'mfa_option_totp_title' => 'Mobile App',
+    'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
+    'mfa_option_backup_codes_title' => 'Backup Codes',
+    'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.',
+    'mfa_gen_confirm_and_enable' => 'Confirm and Enable',
+    'mfa_gen_backup_codes_title' => 'Backup Codes Setup',
+    'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.',
+    'mfa_gen_backup_codes_download' => 'Download Codes',
+    'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once',
+    'mfa_gen_totp_title' => 'Mobile App Setup',
+    'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
+    'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.',
+    'mfa_gen_totp_verify_setup' => 'Verify Setup',
+    'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:',
+    'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here',
+    'mfa_verify_access' => 'Verify Access',
+    'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.',
+    'mfa_verify_no_methods' => 'No Methods Configured',
+    'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.',
+    'mfa_verify_use_totp' => 'Verify using a mobile app',
+    'mfa_verify_use_backup_codes' => 'Verify using a backup code',
+    'mfa_verify_backup_code' => 'Backup Code',
+    'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:',
+    'mfa_verify_backup_code_enter_here' => 'Enter backup code here',
+    'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:',
+    'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.',
 ];
\ No newline at end of file
index d4508c3c7d4c9a7def3b457b1d9d2d96673d7d23..6d3768de4679382896f7675a7744aa45676b91f5 100644 (file)
@@ -5,89 +5,91 @@
 return [
 
     // Buttons
-    'cancel' => 'Cancel',
-    'confirm' => 'Confirm',
-    'back' => 'Back',
-    'save' => 'Save',
-    'continue' => 'Continue',
-    'select' => 'Select',
-    'toggle_all' => 'Toggle All',
-    'more' => 'More',
+    'cancel' => 'لغو',
+    'confirm' => 'تایید',
+    'back' => 'بازگشت',
+    'save' => 'ذخیره',
+    'continue' => 'ادامه',
+    'select' => 'انتخاب',
+    'toggle_all' => 'معکوس کردن همه',
+    'more' => 'بیشتر',
 
     // Form Labels
-    'name' => 'Name',
-    'description' => 'Description',
-    'role' => 'Role',
-    'cover_image' => 'Cover image',
-    'cover_image_description' => 'This image should be approx 440x250px.',
+    'name' => 'نام',
+    'description' => 'توضیحات',
+    'role' => 'نقش',
+    'cover_image' => 'تصویر روی جلد',
+    'cover_image_description' => 'سایز تصویر باید 440x250 باشد.',
     
     // Actions
-    'actions' => 'Actions',
-    'view' => 'View',
-    'view_all' => 'View All',
-    'create' => 'Create',
-    'update' => 'Update',
-    'edit' => 'Edit',
-    'sort' => 'Sort',
-    'move' => 'Move',
-    'copy' => 'Copy',
-    'reply' => 'Reply',
-    'delete' => 'Delete',
-    'delete_confirm' => 'Confirm Deletion',
-    'search' => 'Search',
-    'search_clear' => 'Clear Search',
-    'reset' => 'Reset',
-    'remove' => 'Remove',
-    'add' => 'Add',
-    'fullscreen' => 'Fullscreen',
-    'favourite' => 'Favourite',
+    'actions' => 'عملیات',
+    'view' => 'نمایش',
+    'view_all' => 'نمایش همه',
+    'create' => 'ایجاد',
+    'update' => 'به‌روز رسانی',
+    'edit' => 'ويرايش',
+    'sort' => 'مرتب سازی',
+    'move' => 'جابجایی',
+    'copy' => 'کپی',
+    'reply' => 'پاسخ',
+    'delete' => 'حذف',
+    'delete_confirm' => 'تأیید حذف',
+    'search' => 'جستجو',
+    'search_clear' => 'پاک کردن جستجو',
+    'reset' => 'بازنشانی',
+    'remove' => 'حذف',
+    'add' => 'ﺍﻓﺰﻭﺩﻥ',
+    'configure' => 'Configure',
+    'fullscreen' => 'تمام صفحه',
+    'favourite' => 'علاقه‌مندی',
     'unfavourite' => 'Unfavourite',
-    'next' => 'Next',
-    'previous' => 'Previous',
+    'next' => 'بعدی',
+    'previous' => 'قبلى',
 
     // Sort Options
-    'sort_options' => 'Sort Options',
-    'sort_direction_toggle' => 'Sort Direction Toggle',
-    'sort_ascending' => 'Sort Ascending',
-    'sort_descending' => 'Sort Descending',
-    'sort_name' => 'Name',
-    'sort_default' => 'Default',
-    'sort_created_at' => 'Created Date',
-    'sort_updated_at' => 'Updated Date',
+    'sort_options' => 'گزینه‌های مرتب سازی',
+    'sort_direction_toggle' => 'معکوس کردن جهت مرتب سازی',
+    'sort_ascending' => 'مرتب‌سازی صعودی',
+    'sort_descending' => 'مرتب‌سازی نزولی',
+    'sort_name' => 'نام',
+    'sort_default' => 'پیش‎فرض',
+    'sort_created_at' => 'تاریخ ایجاد',
+    'sort_updated_at' => 'تاریخ بروزرسانی',
 
     // Misc
-    'deleted_user' => 'Deleted User',
-    'no_activity' => 'No activity to show',
-    'no_items' => 'No items available',
-    'back_to_top' => 'Back to top',
-    'toggle_details' => 'Toggle Details',
-    'toggle_thumbnails' => 'Toggle Thumbnails',
-    'details' => 'Details',
-    'grid_view' => 'Grid View',
-    'list_view' => 'List View',
-    'default' => 'Default',
-    'breadcrumb' => 'Breadcrumb',
+    'deleted_user' => 'کاربر حذف شده',
+    'no_activity' => 'بایگانی برای نمایش وجود ندارد',
+    'no_items' => 'هیچ آیتمی موجود نیست',
+    'back_to_top' => 'بازگشت به بالا',
+    'skip_to_main_content' => 'رفتن به محتوای اصلی',
+    'toggle_details' => 'معکوس کردن اطلاعات',
+    'toggle_thumbnails' => 'معکوس ریز عکس ها',
+    'details' => 'جزییات',
+    'grid_view' => 'نمایش شبکه‌ای',
+    'list_view' => 'نمای لیست',
+    'default' => 'پیش‎فرض',
+    'breadcrumb' => 'مسیر جاری',
 
     // Header
-    'header_menu_expand' => 'Expand Header Menu',
-    'profile_menu' => 'Profile Menu',
-    'view_profile' => 'View Profile',
-    'edit_profile' => 'Edit Profile',
-    'dark_mode' => 'Dark Mode',
-    'light_mode' => 'Light Mode',
+    'header_menu_expand' => 'گسترش منو',
+    'profile_menu' => 'منو پروفایل',
+    'view_profile' => 'مشاهده پروفایل',
+    'edit_profile' => 'ویرایش پروفایل',
+    'dark_mode' => 'حالت تاریک',
+    'light_mode' => 'حالت روشن',
 
     // Layout tabs
-    'tab_info' => 'Info',
-    'tab_info_label' => 'Tab: Show Secondary Information',
-    'tab_content' => 'Content',
-    'tab_content_label' => 'Tab: Show Primary Content',
+    'tab_info' => 'اطلاعات',
+    'tab_info_label' => 'زبانه: نمایش اطلاعات ثانویه',
+    'tab_content' => 'محتوا',
+    'tab_content_label' => 'زبانه: نمایش محتوای اصلی',
 
     // 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',
+    'email_action_help' => 'اگر با دکمه بالا مشکلی دارید ، ادرس وبسایت *URLزیر را در مرورگر وب خود کپی و پیست کنید:',
+    'email_rights' => 'تمام حقوق محفوظ است',
 
     // Footer Link Options
     // Not directly used but available for convenience to users.
-    'privacy_policy' => 'Privacy Policy',
-    'terms_of_service' => 'Terms of Service',
+    'privacy_policy' => 'سیاست حفظ حریم خصوصی',
+    'terms_of_service' => 'شرایط خدمات',
 ];
index 48a0a32faa38c4821a9d71dda9a5fb4f97d35232..126bd093db9854d58c2d6b2718b86b605d4e996a 100644 (file)
@@ -5,30 +5,30 @@
 return [
 
     // Image Manager
-    'image_select' => 'Image Select',
-    'image_all' => 'All',
-    'image_all_title' => 'View all images',
-    'image_book_title' => 'View images uploaded to this book',
-    'image_page_title' => 'View images uploaded to this page',
-    'image_search_hint' => 'Search by image name',
-    'image_uploaded' => 'Uploaded :uploadedDate',
-    'image_load_more' => 'Load More',
-    'image_image_name' => 'Image Name',
-    'image_delete_used' => 'This image is used in the pages below.',
-    'image_delete_confirm_text' => 'Are you sure you want to delete this image?',
-    'image_select_image' => 'Select Image',
-    'image_dropzone' => 'Drop images or click here to upload',
-    'images_deleted' => 'Images Deleted',
-    'image_preview' => 'Image Preview',
-    'image_upload_success' => 'Image uploaded successfully',
-    'image_update_success' => 'Image details successfully updated',
-    'image_delete_success' => 'Image successfully deleted',
-    'image_upload_remove' => 'Remove',
+    'image_select' => 'انتخاب تصویر',
+    'image_all' => 'همه',
+    'image_all_title' => 'نمایش تمام تصاویر',
+    'image_book_title' => 'تصاویر بارگذاری شده در این کتاب را مشاهده کنید',
+    'image_page_title' => 'تصاویر بارگذاری شده در این صفحه را مشاهده کنید',
+    'image_search_hint' => 'جستجو بر اساس نام تصویر',
+    'image_uploaded' => 'بارگذاری شده :uploadedDate',
+    'image_load_more' => 'بارگذاری بیشتر',
+    'image_image_name' => 'نام تصویر',
+    'image_delete_used' => 'این تصویر در صفحات زیر استفاده شده است.',
+    'image_delete_confirm_text' => 'آیا مطمئن هستید که میخواهید این عکس را پاک کنید؟',
+    'image_select_image' => 'انتخاب تصویر',
+    'image_dropzone' => 'تصاویر را رها کنید یا برای بارگذاری اینجا را کلیک کنید',
+    'images_deleted' => 'تصاویر حذف شده',
+    'image_preview' => 'پیش نمایش تصویر',
+    'image_upload_success' => 'تصویر با موفقیت بارگذاری شد',
+    'image_update_success' => 'جزئیات تصویر با موفقیت به روز شد',
+    'image_delete_success' => 'تصویر با موفقیت حذف شد',
+    'image_upload_remove' => 'حذف',
 
     // Code Editor
-    'code_editor' => 'Edit Code',
-    'code_language' => 'Code Language',
-    'code_content' => 'Code Content',
-    'code_session_history' => 'Session History',
-    'code_save' => 'Save Code',
+    'code_editor' => 'ویرایش کد',
+    'code_language' => 'زبان کد',
+    'code_content' => 'محتوی کد',
+    'code_session_history' => 'تاریخچه جلسات',
+    'code_save' => 'ذخیره کد',
 ];
index 462402f33f407b458700e80e2c15f22257ceba52..3d45e2165757defed65c03655f2facf6fc7dbd82 100644 (file)
@@ -6,59 +6,60 @@
 return [
 
     // Shared
-    'recently_created' => 'Recently Created',
-    'recently_created_pages' => 'Recently Created Pages',
-    'recently_updated_pages' => 'Recently Updated Pages',
-    'recently_created_chapters' => 'Recently Created Chapters',
-    'recently_created_books' => 'Recently Created Books',
-    'recently_created_shelves' => 'Recently Created Shelves',
-    'recently_update' => 'Recently Updated',
-    'recently_viewed' => 'Recently Viewed',
-    'recent_activity' => 'Recent Activity',
-    'create_now' => 'Create one now',
-    'revisions' => 'Revisions',
-    'meta_revision' => 'Revision #:revisionCount',
-    'meta_created' => 'Created :timeLength',
-    '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',
-    'my_recently_viewed' => 'My Recently Viewed',
-    'my_most_viewed_favourites' => 'My Most Viewed Favourites',
-    'my_favourites' => 'My Favourites',
-    'no_pages_viewed' => 'You have not viewed any pages',
-    'no_pages_recently_created' => 'No pages have been recently created',
-    'no_pages_recently_updated' => 'No pages have been recently updated',
-    'export' => 'Export',
-    'export_html' => 'Contained Web File',
-    'export_pdf' => 'PDF File',
-    'export_text' => 'Plain Text File',
+    'recently_created' => 'اخیرا ایجاد شده',
+    'recently_created_pages' => 'صفحات اخیرا ایجاد شده',
+    'recently_updated_pages' => 'صفحاتی که اخیرا روزآمد شده‌اند',
+    'recently_created_chapters' => 'فصل های اخیرا ایجاد شده',
+    'recently_created_books' => 'کتاب های اخیرا ایجاد شده',
+    'recently_created_shelves' => 'قفسه کتاب های اخیرا ایجاد شده',
+    'recently_update' => 'اخیرا به روز شده',
+    'recently_viewed' => 'اخیرا مشاهده شده',
+    'recent_activity' => 'فعالیت های اخیر',
+    'create_now' => 'اکنون یکی ایجاد کنید',
+    'revisions' => 'بازبینی‌ها',
+    'meta_revision' => 'بازبینی #:revisionCount',
+    'meta_created' => 'ایجاد شده :timeLength',
+    'meta_created_name' => 'ایجاد شده :timeLength توسط :user',
+    'meta_updated' => 'به روزرسانی شده :timeLength',
+    'meta_updated_name' => 'به روزرسانی شده :timeLength توسط :user',
+    'meta_owned_name' => 'توسط :user ایجاد شده‌است',
+    'entity_select' => 'انتخاب موجودیت',
+    'images' => 'عکس ها',
+    'my_recent_drafts' => 'پیش نویس های اخیر من',
+    'my_recently_viewed' => 'بازدیدهای اخیر من',
+    'my_most_viewed_favourites' => 'محبوب ترین موارد مورد علاقه من',
+    'my_favourites' => 'مورد علاقه من',
+    'no_pages_viewed' => 'شما هیچ صفحه ای را مشاهده نکرده اید',
+    'no_pages_recently_created' => 'اخیرا هیچ صفحه ای ایجاد نشده است',
+    'no_pages_recently_updated' => 'اخیرا هیچ صفحه ای به روزرسانی نشده است',
+    'export' => 'خروجی',
+    'export_html' => 'فایل وب موجود است',
+    'export_pdf' => 'فایل PDF',
+    'export_text' => 'پرونده متنی ساده',
+    'export_md' => 'راهنما مارک‌دون',
 
     // Permissions and restrictions
-    'permissions' => 'Permissions',
-    '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',
+    'permissions' => 'مجوزها',
+    'permissions_intro' => 'پس از فعال شدن، این مجوزها نسبت به مجوزهای تعیین شده نقش اولویت دارند.',
+    'permissions_enable' => 'مجوزهای سفارشی را فعال کنید',
+    'permissions_save' => 'ذخيره مجوزها',
+    'permissions_owner' => 'مالک',
 
     // Search
-    'search_results' => 'Search Results',
+    'search_results' => 'نتایج جستجو',
     'search_total_results_found' => ':count result found|:count total results found',
-    'search_clear' => 'Clear Search',
-    'search_no_pages' => 'No pages matched this search',
-    'search_for_term' => 'Search for :term',
-    'search_more' => 'More Results',
-    'search_advanced' => 'Advanced Search',
-    'search_terms' => 'Search Terms',
-    'search_content_type' => 'Content Type',
-    'search_exact_matches' => 'Exact Matches',
-    'search_tags' => 'Tag Searches',
-    'search_options' => 'Options',
-    'search_viewed_by_me' => 'Viewed by me',
-    'search_not_viewed_by_me' => 'Not viewed by me',
+    'search_clear' => 'پاک کردن جستجو',
+    'search_no_pages' => 'هیچ صفحه ای با این جستجو مطابقت ندارد',
+    'search_for_term' => 'جستجو برای :term',
+    'search_more' => 'نتایج بیشتر',
+    'search_advanced' => 'جستجوی پیشرفته',
+    'search_terms' => 'عبارات جستجو',
+    'search_content_type' => 'نوع محتوا',
+    'search_exact_matches' => 'مطابقت کامل',
+    'search_tags' => 'جستجوها را برچسب بزنید',
+    'search_options' => 'گزینه ها',
+    'search_viewed_by_me' => 'بازدید شده به وسیله من',
+    'search_not_viewed_by_me' => 'توسط من مشاهده نشده است',
     'search_permissions_set' => 'Permissions set',
     'search_created_by_me' => 'Created by me',
     'search_updated_by_me' => 'Updated by me',
@@ -98,6 +99,7 @@ return [
     'shelves_permissions' => 'Bookshelf Permissions',
     'shelves_permissions_updated' => 'Bookshelf Permissions Updated',
     'shelves_permissions_active' => 'Bookshelf Permissions Active',
+    'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',
     '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.',
index eb8ba54ea81506519a4958769d54086cc3b3d7be..9b0281bc0a05707955bc22b5fbe4ad6072f84511 100644 (file)
@@ -5,34 +5,34 @@
 return [
 
     // Permissions
-    'permission' => 'You do not have permission to access the requested page.',
-    'permissionJson' => 'You do not have permission to perform the requested action.',
+    'permission' => 'شما مجوز مشاهده صفحه درخواست شده را ندارید.',
+    'permissionJson' => 'شما مجاز به انجام این عمل نیستید.',
 
     // Auth
-    'error_user_exists_different_creds' => 'A user with the email :email already exists but with different credentials.',
-    'email_already_confirmed' => 'Email has already been confirmed, Try logging in.',
-    'email_confirmation_invalid' => 'This confirmation token is not valid or has already been used, Please try registering again.',
-    'email_confirmation_expired' => 'The confirmation token has expired, A new confirmation email has been sent.',
-    'email_confirmation_awaiting' => 'The email address for the account in use needs to be confirmed',
-    'ldap_fail_anonymous' => 'LDAP access failed using anonymous bind',
-    'ldap_fail_authed' => 'LDAP access failed using given dn & password details',
-    'ldap_extension_not_installed' => 'LDAP PHP extension not installed',
-    'ldap_cannot_connect' => 'Cannot connect to ldap server, Initial connection failed',
-    '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',
-    'social_no_action_defined' => 'No action defined',
-    'social_login_bad_response' => "Error received during :socialAccount login: \n:error",
-    'social_account_in_use' => 'This :socialAccount account is already in use, Try logging in via the :socialAccount option.',
-    'social_account_email_in_use' => 'The email :email is already in use. If you already have an account you can connect your :socialAccount account from your profile settings.',
-    'social_account_existing' => 'This :socialAccount is already attached to your profile.',
-    'social_account_already_used_existing' => 'This :socialAccount account is already used by another user.',
-    'social_account_not_used' => 'This :socialAccount account is not linked to any users. Please attach it in your profile settings. ',
-    'social_account_register_instructions' => 'If you do not yet have an account, You can register an account using the :socialAccount option.',
-    'social_driver_not_found' => 'Social driver not found',
-    'social_driver_not_configured' => 'Your :socialAccount social settings are not configured correctly.',
+    'error_user_exists_different_creds' => 'کاربری با ایمیل :email از قبل وجود دارد اما دارای اطلاعات متفاوتی می باشد.',
+    'email_already_confirmed' => 'ایمیل قبلا تایید شده است، وارد سیستم شوید.',
+    'email_confirmation_invalid' => 'این کلمه عبور معتبر نمی باشد و یا قبلا استفاده شده است، لطفا دوباره ثبت نام نمایید.',
+    'email_confirmation_expired' => 'کلمه عبور منقضی شده است، یک ایمیل تایید جدید ارسال شد.',
+    'email_confirmation_awaiting' => 'آدرس ایمیل حساب مورد استفاده باید تایید شود',
+    'ldap_fail_anonymous' => 'دسترسی LDAP با استفاده از صحافی ناشناس انجام نشد',
+    'ldap_fail_authed' => 'دسترسی به LDAP با استفاده از جزئیات داده شده و رمز عبور انجام نشد',
+    'ldap_extension_not_installed' => 'افزونه PHP LDAP نصب نشده است',
+    'ldap_cannot_connect' => 'اتصال به سرور LDAP امکان پذیر نیست، اتصال اولیه برقرار نشد',
+    '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' => "خطای دریافت شده در هنگام ورود به سیستم:\n:error",
+    'social_account_in_use' => 'این حساب :socialAccount از قبل در حال استفاده است، سعی کنید از طریق گزینه :socialAccount وارد سیستم شوید.',
+    'social_account_email_in_use' => 'ایمیل :email از قبل در حال استفاده است. اگر از قبل حساب کاربری دارید می توانید از تنظیمات نمایه خود :socialAccount خود را وصل کنید.',
+    'social_account_existing' => 'این :socialAccount از قبل به نمایه شما پیوست شده است.',
+    'social_account_already_used_existing' => 'این حساب :socialAccount قبلا توسط کاربر دیگری استفاده شده است.',
+    'social_account_not_used' => 'این حساب :socialAccount به هیچ کاربری پیوند ندارد. لطفا آن را در تنظیمات نمایه خود ضمیمه کنید. ',
+    'social_account_register_instructions' => 'اگر هنوز حساب کاربری ندارید ، می توانید با استفاده از گزینه :socialAccount حساب خود را ثبت کنید.',
+    'social_driver_not_found' => 'درایور شبکه اجتماعی یافت نشد',
+    'social_driver_not_configured' => 'تنظیمات شبکه اجتماعی :socialAccount به درستی پیکربندی نشده است.',
     'invite_token_expired' => 'This invitation link has expired. You can instead try to reset your account password.',
 
     // System
index 85bd12fc319557dcc852fdacc07e882583d66be3..22fb0b89e697e6d9e2ac4c49513eecced2a2cb47 100644 (file)
@@ -6,7 +6,7 @@
  */
 return [
 
-    'previous' => '&laquo; Previous',
-    'next'     => 'Next &raquo;',
+    'previous' => '&laquo; قبلی',
+    'next'     => 'بعدی &raquo;',
 
 ];
index b408f3c2fdaf1e80e9cdafa36ae9507db9fbda48..06b8f8b50d69bd3b812c13ee0392e6f16d27e819 100644 (file)
@@ -6,10 +6,10 @@
  */
 return [
 
-    'password' => 'Passwords must be at least eight characters and match the confirmation.',
-    'user' => "We can't find a user with that e-mail address.",
-    'token' => 'The password reset token is invalid for this email address.',
-    'sent' => 'We have e-mailed your password reset link!',
-    'reset' => 'Your password has been reset!',
+    'password' => 'گذرواژه باید حداقل هشت حرف و با تایید مطابقت داشته باشد.',
+    'user' => "ما کاربری با این نشانی ایمیل نداریم.",
+    'token' => 'مشخصه‌ی بازگردانی رمز عبور معتبر نیست.',
+    'sent' => 'لینک بازگردانی رمز عبور به ایمیل شما ارسال شد!',
+    'reset' => 'رمز عبور شما بازگردانی شد!',
 
 ];
index 8a7946b1281269a0f62d17d6dacea2afd466f56b..0ab168b66998bca0de4807f94d181b9b12ed1683 100644 (file)
@@ -92,6 +92,7 @@ return [
     '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_parent' => 'Parent',
     'recycle_bin_deleted_by' => 'Deleted By',
     'recycle_bin_deleted_at' => 'Deletion Time',
     'recycle_bin_permanently_delete' => 'Permanently Delete',
@@ -104,6 +105,7 @@ return [
     '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_restore_parent' => 'Restore Parent',
     'recycle_bin_destroy_notification' => 'Deleted :count total items from the recycle bin.',
     'recycle_bin_restore_notification' => 'Restored :count total items from the recycle bin.',
 
@@ -117,6 +119,7 @@ return [
     'audit_table_user' => 'User',
     'audit_table_event' => 'Event',
     'audit_table_related' => 'Related Item or Detail',
+    'audit_table_ip' => 'IP Address',
     'audit_table_date' => 'Activity Date',
     'audit_date_from' => 'Date Range From',
     'audit_date_to' => 'Date Range To',
@@ -136,6 +139,7 @@ return [
     'role_details' => 'Role Details',
     'role_name' => 'Role Name',
     'role_desc' => 'Short Description of Role',
+    'role_mfa_enforced' => 'Requires Multi-Factor Authentication',
     'role_external_auth_id' => 'External Authentication IDs',
     'role_system' => 'System Permissions',
     'role_manage_users' => 'Manage users',
@@ -145,6 +149,7 @@ return [
     'role_manage_page_templates' => 'Manage page templates',
     'role_access_api' => 'Access system API',
     'role_manage_settings' => 'Manage app settings',
+    'role_export_content' => 'Export content',
     '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.',
@@ -202,6 +207,10 @@ return [
     'users_api_tokens_create' => 'Create Token',
     'users_api_tokens_expires' => 'Expires',
     'users_api_tokens_docs' => 'API Documentation',
+    'users_mfa' => 'Multi-Factor Authentication',
+    'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
+    'users_mfa_x_methods' => ':count method configured|:count methods configured',
+    'users_mfa_configure' => 'Configure Methods',
 
     // API Tokens
     'user_api_token_create' => 'Create API Token',
@@ -247,6 +256,7 @@ return [
         'it' => 'Italian',
         'ja' => '日本語',
         'ko' => '한국어',
+        'lt' => 'Lietuvių Kalba',
         'lv' => 'Latviešu Valoda',
         'nl' => 'Nederlands',
         'nb' => 'Norsk (Bokmål)',
index 4031de2ae743b75bafd49195ff06b29a347cbf22..cb9cd8eff023f76740b98df21912b0761987446c 100644 (file)
 return [
 
     // Standard laravel validation lines
-    'accepted'             => 'The :attribute must be accepted.',
-    'active_url'           => 'The :attribute is not a valid URL.',
-    'after'                => 'The :attribute must be a date after :date.',
-    'alpha'                => 'The :attribute may only contain letters.',
-    'alpha_dash'           => 'The :attribute may only contain letters, numbers, dashes and underscores.',
-    'alpha_num'            => 'The :attribute may only contain letters and numbers.',
-    'array'                => 'The :attribute must be an array.',
-    'before'               => 'The :attribute must be a date before :date.',
+    'accepted'             => ':attribute باید پذیرفته شده باشد.',
+    'active_url'           => 'آدرس :attribute معتبر نیست.',
+    'after'                => ':attribute باید تاریخی بعد از :date باشد.',
+    'alpha'                => ':attribute باید فقط حروف الفبا باشد.',
+    'alpha_dash'           => ':attribute باید فقط حروف الفبا، اعداد، خط تیره و زیرخط باشد.',
+    'alpha_num'            => ':attribute باید فقط حروف الفبا و اعداد باشد.',
+    'array'                => ':attribute باید آرایه باشد.',
+    'backup_codes'         => 'The provided code is not valid or has already been used.',
+    'before'               => ':attribute باید تاریخی قبل از :date باشد.',
     'between'              => [
-        'numeric' => 'The :attribute must be between :min and :max.',
-        'file'    => 'The :attribute must be between :min and :max kilobytes.',
-        'string'  => 'The :attribute must be between :min and :max characters.',
-        'array'   => 'The :attribute must have between :min and :max items.',
+        'numeric' => ':attribute باید بین :min و :max باشد.',
+        'file'    => ':attribute باید بین :min و :max کیلوبایت باشد.',
+        'string'  => ':attribute باید بین :min و :max کاراکتر باشد.',
+        'array'   => ':attribute باید بین :min و :max آیتم باشد.',
     ],
-    'boolean'              => 'The :attribute field must be true or false.',
-    'confirmed'            => 'The :attribute confirmation does not match.',
-    'date'                 => 'The :attribute is not a valid date.',
-    'date_format'          => 'The :attribute does not match the format :format.',
-    'different'            => 'The :attribute and :other must be different.',
-    'digits'               => 'The :attribute must be :digits digits.',
-    'digits_between'       => 'The :attribute must be between :min and :max digits.',
-    'email'                => 'The :attribute must be a valid email address.',
-    'ends_with' => 'The :attribute must end with one of the following: :values',
-    'filled'               => 'The :attribute field is required.',
+    'boolean'              => 'فیلد :attribute فقط می‌تواند true و یا false باشد.',
+    'confirmed'            => ':attribute با فیلد تکرار مطابقت ندارد.',
+    'date'                 => ':attribute یک تاریخ معتبر نیست.',
+    'date_format'          => ':attribute با الگوی :format مطابقت ندارد.',
+    'different'            => ':attribute و :other باید از یکدیگر متفاوت باشند.',
+    'digits'               => ':attribute باید :digits رقم باشد.',
+    'digits_between'       => ':attribute باید بین :min و :max رقم باشد.',
+    'email'                => ':attribute باید یک ایمیل معتبر باشد.',
+    'ends_with' => 'فیلد :attribute باید با یکی از مقادیر زیر خاتمه یابد: :values',
+    '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' => ':attribute باید بزرگتر از :value باشد.',
+        'file'    => ':attribute باید بزرگتر از :value کیلوبایت باشد.',
+        'string'  => ':attribute باید بیشتر از :value کاراکتر داشته باشد.',
+        'array'   => ':attribute باید بیشتر از :value آیتم داشته باشد.',
     ],
     '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' => ':attribute باید بزرگتر یا مساوی :value باشد.',
+        'file'    => ':attribute باید بزرگتر یا مساوی :value کیلوبایت باشد.',
+        'string'  => ':attribute باید بیشتر یا مساوی :value کاراکتر داشته باشد.',
+        'array'   => ':attribute باید بیشتر یا مساوی :value آیتم داشته باشد.',
     ],
-    'exists'               => 'The selected :attribute is invalid.',
-    'image'                => 'The :attribute must be an image.',
-    'image_extension'      => 'The :attribute must have a valid & supported image extension.',
-    'in'                   => 'The selected :attribute is invalid.',
-    'integer'              => 'The :attribute must be an integer.',
-    'ip'                   => 'The :attribute must be a valid IP address.',
-    'ipv4'                 => 'The :attribute must be a valid IPv4 address.',
-    'ipv6'                 => 'The :attribute must be a valid IPv6 address.',
-    'json'                 => 'The :attribute must be a valid JSON string.',
+    'exists'               => ':attribute انتخاب شده، معتبر نیست.',
+    'image'                => ':attribute باید یک تصویر معتبر باشد.',
+    'image_extension'      => ':attribute باید یک تصویر با فرمت معتبر باشد.',
+    'in'                   => ':attribute انتخاب شده، معتبر نیست.',
+    'integer'              => ':attribute باید عدد صحیح باشد.',
+    'ip'                   => ':attribute باید آدرس IP معتبر باشد.',
+    'ipv4'                 => ':attribute باید یک آدرس معتبر از نوع IPv4 باشد.',
+    'ipv6'                 => ':attribute باید یک آدرس معتبر از نوع IPv6 باشد.',
+    'json'                 => 'فیلد :attribute باید یک رشته از نوع 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' => ':attribute باید کوچکتر از :value باشد.',
+        'file'    => ':attribute باید کوچکتر از :value کیلوبایت باشد.',
+        'string'  => ':attribute باید کمتر از :value کاراکتر داشته باشد.',
+        'array'   => ':attribute باید کمتر از :value آیتم داشته باشد.',
     ],
     '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' => ':attribute باید کوچکتر یا مساوی :value باشد.',
+        'file'    => ':attribute باید کوچکتر یا مساوی :value کیلوبایت باشد.',
+        'string'  => ':attribute باید کمتر یا مساوی :value کاراکتر داشته باشد.',
+        'array'   => ':attribute باید کمتر یا مساوی :value آیتم داشته باشد.',
     ],
     'max'                  => [
-        'numeric' => 'The :attribute may not be greater than :max.',
-        'file'    => 'The :attribute may not be greater than :max kilobytes.',
-        'string'  => 'The :attribute may not be greater than :max characters.',
-        'array'   => 'The :attribute may not have more than :max items.',
+        'numeric' => ':attribute نباید بزرگتر از :max باشد.',
+        'file'    => ':attribute نباید بزرگتر از :max کیلوبایت باشد.',
+        'string'  => ':attribute نباید بیشتر از :max کاراکتر داشته باشد.',
+        'array'   => ':attribute نباید بیشتر از :max آیتم داشته باشد.',
     ],
-    'mimes'                => 'The :attribute must be a file of type: :values.',
+    'mimes'                => 'فرمت‌های معتبر فایل عبارتند از: :values.',
     'min'                  => [
-        'numeric' => 'The :attribute must be at least :min.',
-        'file'    => 'The :attribute must be at least :min kilobytes.',
-        'string'  => 'The :attribute must be at least :min characters.',
-        'array'   => 'The :attribute must have at least :min items.',
+        'numeric' => ':attribute نباید کوچکتر از :min باشد.',
+        'file'    => ':attribute نباید کوچکتر از :min کیلوبایت باشد.',
+        'string'  => ':attribute نباید کمتر از :min کاراکتر داشته باشد.',
+        'array'   => ':attribute نباید کمتر از :min آیتم داشته باشد.',
     ],
-    'not_in'               => 'The selected :attribute is invalid.',
-    'not_regex'            => 'The :attribute format is invalid.',
-    'numeric'              => 'The :attribute must be a number.',
-    'regex'                => 'The :attribute format is invalid.',
-    'required'             => 'The :attribute field is required.',
-    'required_if'          => 'The :attribute field is required when :other is :value.',
-    'required_with'        => 'The :attribute field is required when :values is present.',
-    'required_with_all'    => 'The :attribute field is required when :values is present.',
-    '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.',
+    'not_in'               => ':attribute انتخاب شده، معتبر نیست.',
+    'not_regex'            => 'فرمت :attribute معتبر نیست.',
+    'numeric'              => ':attribute باید عدد یا رشته‌ای از اعداد باشد.',
+    'regex'                => 'فرمت :attribute معتبر نیست.',
+    'required'             => 'فیلد :attribute الزامی است.',
+    'required_if'          => 'هنگامی که :other برابر با :value است، فیلد :attribute الزامی است.',
+    'required_with'        => 'در صورت وجود فیلد :values، فیلد :attribute نیز الزامی است.',
+    'required_with_all'    => 'در صورت وجود فیلدهای :values، فیلد :attribute نیز الزامی است.',
+    'required_without'     => 'در صورت عدم وجود فیلد :values، فیلد :attribute الزامی است.',
+    'required_without_all' => 'در صورت عدم وجود هر یک از فیلدهای :values، فیلد :attribute الزامی است.',
+    'same'                 => ':attribute و :other باید همانند هم باشند.',
+    'safe_url'             => ':attribute معتبر نمی‌باشد.',
     'size'                 => [
-        'numeric' => 'The :attribute must be :size.',
-        'file'    => 'The :attribute must be :size kilobytes.',
-        'string'  => 'The :attribute must be :size characters.',
-        'array'   => 'The :attribute must contain :size items.',
+        'numeric' => ':attribute باید برابر با :size باشد.',
+        'file'    => ':attribute باید برابر با :size کیلوبایت باشد.',
+        'string'  => ':attribute باید برابر با :size کاراکتر باشد.',
+        'array'   => ':attribute باید شامل :size آیتم باشد.',
     ],
-    'string'               => 'The :attribute must be a string.',
-    'timezone'             => 'The :attribute must be a valid zone.',
-    'unique'               => 'The :attribute has already been taken.',
-    'url'                  => 'The :attribute format is invalid.',
-    'uploaded'             => 'The file could not be uploaded. The server may not accept files of this size.',
+    'string'               => 'فیلد :attribute باید متن باشد.',
+    'timezone'             => 'فیلد :attribute باید یک منطقه زمانی معتبر باشد.',
+    'totp'                 => 'The provided code is not valid or has expired.',
+    'unique'               => ':attribute قبلا انتخاب شده است.',
+    'url'                  => ':attribute معتبر نمی‌باشد.',
+    'uploaded'             => 'بارگذاری فایل :attribute موفقیت آمیز نبود.',
 
     // Custom validation lines
     'custom' => [
         'password-confirm' => [
-            'required_with' => 'Password confirmation required',
+            'required_with' => 'تایید کلمه عبور اجباری می باشد',
         ],
     ],
 
index 05f7410097627b7fc3df8140a96fac105a642201..9850bc93cd23068bd5d14f23987dd5594323a325 100644 (file)
@@ -26,11 +26,11 @@ return [
     'chapter_move'                => 'a déplacé le chapitre',
 
     // Books
-    'book_create'                 => 'a créé le livre',
+    'book_create'                 => 'a créé un livre',
     'book_create_notification'    => 'Livre créé avec succès',
     'book_update'                 => 'a modifié le livre',
     'book_update_notification'    => 'Livre modifié avec succès',
-    'book_delete'                 => 'a supprimé le livre',
+    'book_delete'                 => 'a supprimé un livre',
     'book_delete_notification'    => 'Livre supprimé avec succès',
     'book_sort'                   => 'a réordonné le livre',
     'book_sort_notification'      => 'Livre réordonné avec succès',
@@ -47,7 +47,11 @@ return [
     'favourite_add_notification' => '":name" a été ajouté dans vos favoris',
     'favourite_remove_notification' => '":name" a été supprimé de vos favoris',
 
+    // MFA
+    'mfa_setup_method_notification' => 'Méthode multi-facteurs configurée avec succès',
+    'mfa_remove_method_notification' => 'Méthode multi-facteurs supprimée avec succès',
+
     // Other
     'commented_on'                => 'a commenté',
-    'permissions_update'          => 'mettre à jour les autorisations',
+    'permissions_update'          => 'a mis à jour les autorisations sur',
 ];
index 07252420a030a8b87fc3e1b3eb9de8e41c389f78..c608f02fb043359e3d8bd7f05edbf490739b8a2a 100644 (file)
@@ -57,13 +57,13 @@ return [
     'email_confirm_action' => 'Confirmez votre adresse e-mail',
     'email_confirm_send_error' => 'La confirmation par e-mail est requise mais le système n\'a pas pu envoyer l\'e-mail. Contactez l\'administrateur système.',
     'email_confirm_success' => 'Votre adresse e-mail a été confirmée !',
-    'email_confirm_resent' => 'L\'e-mail de confirmation a été ré-envoyé. Vérifiez votre boîte de récéption.',
+    'email_confirm_resent' => 'L\'e-mail de confirmation a été ré-envoyé. Vérifiez votre boîte de réception.',
 
     'email_not_confirmed' => 'Adresse e-mail non confirmée',
     'email_not_confirmed_text' => 'Votre adresse e-mail n\'a pas été confirmée.',
     'email_not_confirmed_click_link' => 'Merci de cliquer sur le lien dans l\'e-mail qui vous a été envoyé après l\'enregistrement.',
     'email_not_confirmed_resend' => 'Si vous ne retrouvez plus l\'e-mail, vous pouvez renvoyer un e-mail de confirmation en utilisant le formulaire ci-dessous.',
-    'email_not_confirmed_resend_button' => 'Renvoyez l\'e-mail de confirmation',
+    'email_not_confirmed_resend_button' => 'Renvoyer l\'e-mail de confirmation',
 
     // User Invite
     'user_invite_email_subject' => 'Vous avez été invité(e) à rejoindre :appName !',
@@ -73,5 +73,40 @@ return [
     'user_invite_page_welcome' => 'Bienvenue dans :appName !',
     'user_invite_page_text' => 'Pour finaliser votre compte et recevoir l\'accès, vous devez renseigner le mot de passe qui sera utilisé pour la connexion à :appName les prochaines fois.',
     'user_invite_page_confirm_button' => 'Confirmez le mot de passe',
-    'user_invite_success' => 'Mot de passe renseigné, vous avez maintenant accès à :appName !'
+    'user_invite_success' => 'Mot de passe renseigné, vous avez maintenant accès à :appName !',
+
+    // Multi-factor Authentication
+    'mfa_setup' => 'Authentification multi-facteurs',
+    'mfa_setup_desc' => 'Configurer l\'authentification multi-facteurs ajoute une couche supplémentaire de sécurité à votre compte utilisateur.',
+    'mfa_setup_configured' => 'Déjà configuré',
+    'mfa_setup_reconfigure' => 'Reconfigurer',
+    'mfa_setup_remove_confirmation' => 'Êtes-vous sûr de vouloir supprimer cette méthode d\'authentification multi-facteurs ?',
+    'mfa_setup_action' => 'Configuration',
+    'mfa_backup_codes_usage_limit_warning' => 'Il vous reste moins de 5 codes de secours, veuillez générer et stocker un nouveau jeu de codes afin d\'éviter tout verrouillage de votre compte.',
+    'mfa_option_totp_title' => 'Application mobile',
+    'mfa_option_totp_desc' => 'Pour utiliser l\'authentification multi-facteurs, vous aurez besoin d\'une application mobile qui supporte TOTP comme Google Authenticator, Authy ou Microsoft Authenticator.',
+    'mfa_option_backup_codes_title' => 'Codes de secours',
+    'mfa_option_backup_codes_desc' => 'Stockez en toute sécurité un jeu de codes de secours que vous pourrez utiliser pour vérifier votre identité.',
+    'mfa_gen_confirm_and_enable' => 'Confirmer et activer',
+    'mfa_gen_backup_codes_title' => 'Configuration des codes de secours',
+    'mfa_gen_backup_codes_desc' => 'Stockez la liste des codes ci-dessous dans un endroit sûr. Lorsque vous accédez au système, vous pourrez utiliser l\'un des codes comme un deuxième mécanisme d\'authentification.',
+    'mfa_gen_backup_codes_download' => 'Télécharger les codes',
+    'mfa_gen_backup_codes_usage_warning' => 'Chaque code ne peut être utilisé qu\'une seule fois',
+    'mfa_gen_totp_title' => 'Configuration de l\'application mobile',
+    'mfa_gen_totp_desc' => 'Pour utiliser l\'authentification multi-facteurs, vous aurez besoin d\'une application mobile qui supporte TOTP comme Google Authenticator, Authy ou Microsoft Authenticator.',
+    'mfa_gen_totp_scan' => 'Scannez le QR code ci-dessous avec votre application d\'authentification préférée pour débuter.',
+    'mfa_gen_totp_verify_setup' => 'Vérifier la configuration',
+    'mfa_gen_totp_verify_setup_desc' => 'Vérifiez que tout fonctionne en utilisant un code généré par votre application d\'authentification, dans la zone ci-dessous :',
+    'mfa_gen_totp_provide_code_here' => 'Fournissez le code généré par votre application ici',
+    'mfa_verify_access' => 'Vérifier l\'accès',
+    'mfa_verify_access_desc' => 'Votre compte d\'utilisateur vous demande de confirmer votre identité par un niveau supplémentaire de vérification avant que vous n\'ayez accès. Vérifiez-la en utilisant l\'une de vos méthodes configurées pour continuer.',
+    'mfa_verify_no_methods' => 'Aucune méthode configurée',
+    'mfa_verify_no_methods_desc' => 'Aucune méthode d\'authentification multi-facteurs n\'a pu être trouvée pour votre compte. Vous devez configurer au moins une méthode avant d\'obtenir l\'accès.',
+    'mfa_verify_use_totp' => 'Vérifier à l\'aide d\'une application mobile',
+    'mfa_verify_use_backup_codes' => 'Vérifier en utilisant un code de secours',
+    'mfa_verify_backup_code' => 'Code de secours',
+    'mfa_verify_backup_code_desc' => 'Entrez l\'un de vos codes de secours restants ci-dessous :',
+    'mfa_verify_backup_code_enter_here' => 'Saisissez un code de secours ici',
+    'mfa_verify_totp_desc' => 'Entrez ci-dessous le code généré à l\'aide de votre application mobile :',
+    'mfa_setup_login_notification' => 'Méthode multi-facteurs configurée. Veuillez maintenant vous reconnecter en utilisant la méthode configurée.',
 ];
\ No newline at end of file
index 6203c7326cafeb75deadd684cd5078d079820c2d..e6e8bf1a272100e5c92e481de4292e1edc7f5bff 100644 (file)
@@ -27,18 +27,19 @@ return [
     'view_all' => 'Tout afficher',
     'create' => 'Créer',
     'update' => 'Modifier',
-    'edit' => 'Editer',
+    'edit' => 'Éditer',
     'sort' => 'Trier',
     'move' => 'Déplacer',
     'copy' => 'Copier',
     'reply' => 'Répondre',
     'delete' => 'Supprimer',
     'delete_confirm' => 'Confirmer la suppression',
-    'search' => 'Chercher',
+    'search' => 'Rechercher',
     'search_clear' => 'Réinitialiser la recherche',
     'reset' => 'Réinitialiser',
     'remove' => 'Enlever',
     'add' => 'Ajouter',
+    'configure' => 'Configurer',
     'fullscreen' => 'Plein écran',
     'favourite' => 'Favoris',
     'unfavourite' => 'Supprimer des favoris',
@@ -60,6 +61,7 @@ return [
     'no_activity' => 'Aucune activité',
     'no_items' => 'Aucun élément',
     'back_to_top' => 'Retour en haut',
+    'skip_to_main_content' => 'Passer au contenu principal',
     'toggle_details' => 'Afficher les détails',
     'toggle_thumbnails' => 'Afficher les vignettes',
     'details' => 'Détails',
index 6cce4f80418f1e5967daceee7b8e72b30b0f28a6..fed157a4793ff589ffc975026859b157c2062a96 100644 (file)
@@ -26,7 +26,7 @@ return [
     'image_upload_remove' => 'Supprimer',
 
     // Code Editor
-    'code_editor' => 'Editer le code',
+    'code_editor' => 'Éditer le code',
     'code_language' => 'Langage du code',
     'code_content' => 'Contenu du code',
     'code_session_history' => 'Historique de session',
index 507d630e1b272d4d884a092eab4cb9ce849d8e84..1ae697c405321fafa42cac7247b76d417fc8fffd 100644 (file)
@@ -22,12 +22,12 @@ return [
     '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',
+    'meta_owned_name' => 'Appartient à :user',
     'entity_select' => 'Sélectionner l\'entité',
     'images' => 'Images',
     'my_recent_drafts' => 'Mes brouillons récents',
     'my_recently_viewed' => 'Vus récemment',
-    'my_most_viewed_favourites' => 'Mes Favoris les plus vus',
+    'my_most_viewed_favourites' => 'Mes favoris les plus vus',
     'my_favourites' => 'Mes favoris',
     'no_pages_viewed' => 'Vous n\'avez rien visité récemment',
     'no_pages_recently_created' => 'Aucune page créée récemment',
@@ -36,10 +36,11 @@ return [
     'export_html' => 'Fichiers web',
     'export_pdf' => 'Fichier PDF',
     'export_text' => 'Document texte',
+    'export_md' => 'Fichiers Markdown',
 
     // Permissions and restrictions
     'permissions' => 'Autorisations',
-    'permissions_intro' => 'Une fois activées ces permissions prendront la priorité sur tous les sets de permissions préexistants.',
+    'permissions_intro' => 'Une fois activées, ces permissions auront la priorité sur tous les jeux de permissions préexistants.',
     'permissions_enable' => 'Activer les permissions personnalisées',
     'permissions_save' => 'Enregistrer les permissions',
     'permissions_owner' => 'Propriétaire',
@@ -79,8 +80,8 @@ return [
     'shelves_empty' => 'Aucune étagère n\'a été créée',
     'shelves_create' => 'Créer une nouvelle étagère',
     'shelves_popular' => 'Étagères populaires',
-    'shelves_new' => 'Nouvelles Ã\89tagères',
-    'shelves_new_action' => 'Nouvelle Ã\89tagère',
+    'shelves_new' => 'Nouvelles Ã©tagères',
+    'shelves_new_action' => 'Nouvelle Ã©tagère',
     'shelves_popular_empty' => 'Les étagères les plus populaires apparaîtront ici.',
     'shelves_new_empty' => 'Les étagères les plus récentes apparaitront ici.',
     'shelves_save' => 'Enregistrer l\'étagère',
@@ -98,6 +99,7 @@ return [
     'shelves_permissions' => 'Permissions de l\'étagère',
     'shelves_permissions_updated' => 'Permissions de l\'étagère mises à jour',
     'shelves_permissions_active' => 'Permissions de l\'étagère activées',
+    'shelves_permissions_cascade_warning' => 'Les permissions sur les étagères ne sont pas automatiquement recopiées aux livres qu\'elles contiennent, car un livre peut exister dans plusieurs étagères. Les permissions peuvent cependant être recopiées vers les livres contenus en utilisant l\'option ci-dessous.',
     'shelves_copy_permissions_to_books' => 'Copier les permissions vers les livres',
     'shelves_copy_permissions' => 'Copier les permissions',
     'shelves_copy_permissions_explain' => 'Ceci va appliquer les permissions actuelles de cette étagère à tous les livres qu\'elle contient. Avant de  continuer, assurez-vous que toutes les permissions de cette étagère ont été sauvegardées.',
@@ -130,7 +132,7 @@ return [
     'books_empty_sort_current_book' => 'Trier les pages du livre',
     'books_empty_add_chapter' => 'Ajouter un chapitre',
     'books_permissions_active' => 'Permissions personnalisées activées',
-    'books_search_this' => 'Chercher dans le livre',
+    'books_search_this' => 'Rechercher dans ce livre',
     'books_navigation' => 'Navigation dans le livre',
     'books_sort' => 'Trier les contenus du livre',
     'books_sort_named' => 'Trier le livre :bookName',
@@ -172,7 +174,7 @@ return [
     'pages_popular' => 'Pages populaires',
     'pages_new' => 'Nouvelle page',
     'pages_attachments' => 'Fichiers joints',
-    'pages_navigation' => 'Navigation des pages',
+    'pages_navigation' => 'Navigation dans la page',
     'pages_delete' => 'Supprimer la page',
     'pages_delete_named' => 'Supprimer la page :pageName',
     'pages_delete_draft_named' => 'supprimer le brouillon de la page :pageName',
@@ -187,16 +189,16 @@ return [
     'pages_edit_draft' => 'Modifier le brouillon',
     'pages_editing_draft' => 'Modification du brouillon',
     'pages_editing_page' => 'Modification de la page',
-    'pages_edit_draft_save_at' => 'Brouillon sauvé le ',
+    'pages_edit_draft_save_at' => 'Brouillon enregistré le ',
     'pages_edit_delete_draft' => 'Supprimer le brouillon',
-    'pages_edit_discard_draft' => 'Ecarter le brouillon',
+    'pages_edit_discard_draft' => 'Jeter le brouillon',
     'pages_edit_set_changelog' => 'Remplir le journal des changements',
     'pages_edit_enter_changelog_desc' => 'Entrez une brève description des changements effectués',
-    'pages_edit_enter_changelog' => 'Entrer dans le journal des changements',
-    'pages_save' => 'Enregistrez la page',
+    'pages_edit_enter_changelog' => 'Ouvrir le journal des changements',
+    'pages_save' => 'Enregistrer la page',
     'pages_title' => 'Titre de la page',
     'pages_name' => 'Nom de la page',
-    'pages_md_editor' => 'Editeur',
+    'pages_md_editor' => 'Éditeur',
     'pages_md_preview' => 'Prévisualisation',
     'pages_md_insert_image' => 'Insérer une image',
     'pages_md_insert_link' => 'Insérer un lien',
@@ -221,7 +223,7 @@ return [
     'pages_revisions_numbered_changes' => 'Modification #:id',
     'pages_revisions_changelog' => 'Journal des changements',
     'pages_revisions_changes' => 'Changements',
-    'pages_revisions_current' => 'Version courante',
+    'pages_revisions_current' => 'Version actuelle',
     'pages_revisions_preview' => 'Prévisualisation',
     'pages_revisions_restore' => 'Restaurer',
     'pages_revisions_none' => 'Cette page n\'a aucune révision',
@@ -230,8 +232,8 @@ return [
     'pages_permissions_active' => 'Permissions de page actives',
     'pages_initial_revision' => 'Publication initiale',
     'pages_initial_name' => 'Nouvelle page',
-    'pages_editing_draft_notification' => 'Vous éditez actuellement un brouillon qui a été sauvé :timeDiff.',
-    'pages_draft_edited_notification' => 'La page a été mise à jour depuis votre dernière visite. Vous devriez écarter ce brouillon.',
+    'pages_editing_draft_notification' => 'Vous éditez actuellement un brouillon qui a été enregistré :timeDiff.',
+    'pages_draft_edited_notification' => 'La page a été mise à jour depuis votre dernière visite. Vous devriez jeter ce brouillon.',
     'pages_draft_edit_active' => [
         'start_a' => ':count utilisateurs ont commencé à éditer cette page',
         'start_b' => ':userName a commencé à éditer cette page',
@@ -240,7 +242,7 @@ return [
         'message' => ':start :time. Attention à ne pas écraser les mises à jour de quelqu\'un d\'autre !',
     ],
     'pages_draft_discarded' => 'Brouillon écarté, la page est dans sa version actuelle.',
-    'pages_specific' => 'Page Spécifique',
+    'pages_specific' => 'Page spécifique',
     'pages_is_template' => 'Modèle de page',
 
     // Editor Sidebar
@@ -251,10 +253,10 @@ return [
     'tag' => 'Mot-clé',
     'tags' =>  'Mots-clés',
     'tag_name' =>  'Nom du tag',
-    'tag_value' => 'Valeur du mot-clé (Optionnel)',
+    'tag_value' => 'Valeur du mot-clé (optionnel)',
     'tags_explain' => "Ajouter des mots-clés pour catégoriser votre contenu.",
     'tags_add' => 'Ajouter un autre mot-clé',
-    'tags_remove' => 'Supprimer le tag',
+    'tags_remove' => 'Supprimer le mot-clé',
     'attachments' => 'Fichiers joints',
     'attachments_explain' => 'Ajouter des fichiers ou des liens pour les afficher sur votre page. Ils seront affichés dans la barre latérale',
     'attachments_explain_instant_save' => 'Ces changements sont enregistrés immédiatement.',
@@ -265,13 +267,13 @@ return [
     'attachments_delete' => 'Êtes-vous sûr de vouloir supprimer la pièce jointe ?',
     'attachments_dropzone' => 'Glissez des fichiers ou cliquez ici pour attacher des fichiers',
     'attachments_no_files' => 'Aucun fichier ajouté',
-    'attachments_explain_link' => 'Vous pouvez attacher un lien si vous ne souhaitez pas uploader un fichier.',
+    'attachments_explain_link' => 'Vous pouvez ajouter un lien si vous ne souhaitez pas uploader un fichier.',
     'attachments_link_name' => 'Nom du lien',
     'attachment_link' => 'Lien de l\'attachement',
     'attachments_link_url' => 'Lien sur un fichier',
     'attachments_link_url_hint' => 'URL du site ou du fichier',
-    'attach' => 'Attacher',
-    'attachments_insert_link' => 'Ajouter un lien de pièce jointe à la page',
+    'attach' => 'Ajouter',
+    'attachments_insert_link' => 'Ajouter un lien à la page',
     'attachments_edit_file' => 'Modifier le fichier',
     'attachments_edit_file_name' => 'Nom du fichier',
     'attachments_edit_drop_upload' => 'Glissez un fichier ou cliquer pour mettre à jour le fichier',
@@ -286,7 +288,7 @@ return [
     'templates_explain_set_as_template' => 'Vous pouvez définir cette page comme modèle pour que son contenu soit utilisé lors de la création d\'autres pages. Les autres utilisateurs pourront utiliser ce modèle s\'ils ont les permissions pour cette page.',
     'templates_replace_content' => 'Remplacer le contenu de la page',
     'templates_append_content' => 'Ajouter après le contenu de la page',
-    'templates_prepend_content' => 'Ajouter devant le contenu de la page',
+    'templates_prepend_content' => 'Ajouter avant le contenu de la page',
 
     // Profile View
     'profile_user_for_x' => 'Utilisateur depuis :time',
@@ -311,7 +313,7 @@ return [
     'comment_deleted_success' => 'Commentaire supprimé',
     'comment_created_success' => 'Commentaire ajouté',
     'comment_updated_success' => 'Commentaire mis à jour',
-    'comment_delete_confirm' => 'Etes-vous sûr de vouloir supprimer ce commentaire ?',
+    'comment_delete_confirm' => 'Êtes-vous sûr de vouloir supprimer ce commentaire ?',
     'comment_in_reply_to' => 'En réponse à :commentId',
 
     // Revision
index 511683285649e13aa9177451af9b85f8655400f2..d7f00d8e1891b149ad4615ecf73b2f077a9be42f 100644 (file)
@@ -16,24 +16,24 @@ return [
     'email_confirmation_awaiting' => 'L\'adresse e-mail du compte utilisé doit être confirmée',
     'ldap_fail_anonymous' => 'L\'accès LDAP anonyme n\'a pas abouti',
     'ldap_fail_authed' => 'L\'accès LDAP n\'a pas abouti avec cet utilisateur et ce mot de passe',
-    'ldap_extension_not_installed' => 'L\'extension LDAP PHP n\'est pas installée',
+    'ldap_extension_not_installed' => 'L\'extension PHP LDAP n\'est pas installée',
     'ldap_cannot_connect' => 'Impossible de se connecter au serveur LDAP, la connexion initiale a échoué',
     'saml_already_logged_in' => 'Déjà connecté',
     'saml_user_not_registered' => 'L\'utilisateur :name n\'est pas enregistré et l\'enregistrement automatique est désactivé',
     'saml_no_email_address' => 'Impossible de trouver une adresse e-mail, pour cet utilisateur, dans les données fournies par le système d\'authentification externe',
     'saml_invalid_response_id' => 'La requête du système d\'authentification externe n\'est pas reconnue par un processus démarré par cette application. Naviguer après une connexion peut causer ce problème.',
-    'saml_fail_authed' => 'Connexion avec :system échoue, le système n\'a pas fourni l\'autorisation réussie',
+    'saml_fail_authed' => 'Connexion avec :system échouée, le système n\'a pas fourni l\'autorisation réussie',
     'social_no_action_defined' => 'Pas d\'action définie',
     'social_login_bad_response' => "Erreur pendant la tentative de connexion à :socialAccount : \n:error",
     'social_account_in_use' => 'Ce compte :socialAccount est déjà utilisé. Essayez de vous connecter via :socialAccount.',
-    'social_account_email_in_use' => 'L\'email :email est déjà utilisé. Si vous avez déjà un compte :socialAccount, vous pouvez le joindre à votre profil existant.',
+    'social_account_email_in_use' => 'L\'email :email est déjà utilisé. Si vous avez déjà un compte :socialAccount, vous pouvez le rattacher à votre profil existant.',
     'social_account_existing' => 'Ce compte :socialAccount est déjà rattaché à votre profil.',
     'social_account_already_used_existing' => 'Ce compte :socialAccount est déjà utilisé par un autre utilisateur.',
     'social_account_not_used' => 'Ce compte :socialAccount n\'est lié à aucun utilisateur. ',
-    'social_account_register_instructions' => 'Si vous n\'avez pas encore de compte, vous pouvez le lier avec l\'option :socialAccount.',
-    'social_driver_not_found' => 'Pilote de compte social absent',
+    'social_account_register_instructions' => 'Si vous n\'avez pas encore de compte, vous pouvez en créer un avec l\'option :socialAccount.',
+    'social_driver_not_found' => 'Pilote de compte de réseaux sociaux absent',
     'social_driver_not_configured' => 'Vos préférences pour le compte :socialAccount sont incorrectes.',
-    'invite_token_expired' => 'Le lien de cette invitation a expiré. Vous pouvez essayer de réinitiliser votre mot de passe.',
+    'invite_token_expired' => 'Le lien de cette invitation a expiré. Vous pouvez essayer de réinitialiser votre mot de passe.',
 
     // System
     'path_not_writable' => 'Impossible d\'écrire dans :filePath. Assurez-vous d\'avoir les droits d\'écriture sur le serveur',
@@ -42,14 +42,14 @@ return [
     'server_upload_limit' => 'La taille du fichier est trop grande.',
     'uploaded'  => 'Le serveur n\'autorise pas l\'envoi d\'un fichier de cette taille. Veuillez essayer avec une taille de fichier réduite.',
     'image_upload_error' => 'Une erreur est survenue pendant l\'envoi de l\'image',
-    'image_upload_type_error' => 'LE format de l\'image envoyée n\'est pas valide',
+    'image_upload_type_error' => 'Le format de l\'image envoyée n\'est pas valide',
     'file_upload_timeout' => 'Le téléchargement du fichier a expiré.',
 
     // Attachments
     'attachment_not_found' => 'Fichier joint non trouvé',
 
     // Pages
-    'page_draft_autosave_fail' => 'Le brouillon n\'a pas pu être sauvé. Vérifiez votre connexion internet',
+    'page_draft_autosave_fail' => 'Le brouillon n\'a pas pu être enregistré. Vérifiez votre connexion internet',
     'page_custom_home_deletion' => 'Impossible de supprimer une page définie comme page d\'accueil',
 
     // Entities
@@ -60,10 +60,10 @@ return [
     'chapter_not_found' => 'Chapitre non trouvé',
     'selected_book_not_found' => 'Ce livre n\'a pas été trouvé',
     'selected_book_chapter_not_found' => 'Ce livre ou chapitre n\'a pas été trouvé',
-    'guests_cannot_save_drafts' => 'Les invités ne peuvent pas sauver de brouillons',
+    'guests_cannot_save_drafts' => 'Les invités ne peuvent pas enregistrer de brouillons',
 
     // Users
-    'users_cannot_delete_only_admin' => 'Vous ne pouvez pas supprimer le dernier admin',
+    'users_cannot_delete_only_admin' => 'Vous ne pouvez pas supprimer le dernier administrateur',
     'users_cannot_delete_guest' => 'Vous ne pouvez pas supprimer l\'utilisateur invité',
 
     // Roles
@@ -74,7 +74,7 @@ return [
 
     // Comments
     'comment_list' => 'Une erreur s\'est produite lors de la récupération des commentaires.',
-    'cannot_add_comment_to_draft' => 'Vous ne pouvez pas ajouter de commentaires à un projet.',
+    'cannot_add_comment_to_draft' => 'Vous ne pouvez pas ajouter de commentaires à un brouillon.',
     'comment_add' => 'Une erreur s\'est produite lors de l\'ajout du commentaire.',
     'comment_delete' => 'Une erreur s\'est produite lors de la suppression du commentaire.',
     'empty_comment' => 'Impossible d\'ajouter un commentaire vide.',
@@ -82,10 +82,10 @@ return [
     // Error pages
     '404_page_not_found' => 'Page non trouvée',
     'sorry_page_not_found' => 'Désolé, cette page n\'a pas pu être trouvée.',
-    'sorry_page_not_found_permission_warning' => 'Si vous vous attendiez à ce que cette page existe, il se peut que vous n\'ayez pas l\'autorisation de la consulter.',
+    'sorry_page_not_found_permission_warning' => 'Si cette page est censée exister, il se peut que vous n\'ayez pas l\'autorisation de la consulter.',
     'image_not_found' => 'Image non trouvée',
     'image_not_found_subtitle' => 'Désolé, l\'image que vous cherchez ne peut être trouvée.',
-    'image_not_found_details' => 'Si vous vous attendiez à ce que cette image existe, elle pourrait avoir été supprimée.',
+    'image_not_found_details' => 'Si cette image était censée exister, il se pourrait qu\'elle ait été supprimée.',
     'return_home' => 'Retour à l\'accueil',
     'error_occurred' => 'Une erreur est survenue',
     'app_down' => ':appName n\'est pas en service pour le moment',
@@ -96,7 +96,7 @@ return [
     'api_bad_authorization_format' => 'Un jeton d\'autorisation a été trouvé pour la requête, mais le format semble incorrect',
     'api_user_token_not_found' => 'Aucun jeton API correspondant n\'a été trouvé pour le jeton d\'autorisation fourni',
     'api_incorrect_token_secret' => 'Le secret fourni pour le jeton d\'API utilisé est incorrect',
-    'api_user_no_api_permission' => 'Le propriétaire du jeton API utilisé n\'a pas la permission de passer des appels API',
+    'api_user_no_api_permission' => 'Le propriétaire du jeton API utilisé n\'a pas la permission de passer des requêtes API',
     'api_user_token_expired' => 'Le jeton d\'autorisation utilisé a expiré',
 
     // Settings & Maintenance
index b0ff20e282989ec735bf278ba97baaf6d1477b2f..e209c21782343ffb746a66c60aad00b0d0c592bf 100644 (file)
@@ -6,10 +6,10 @@
  */
 return [
 
-    'password' => 'Les mots de passe doivent faire au moins 6 caractères et correspondre à la confirmation.',
+    'password' => 'Les mots de passe doivent faire au moins 8 caractères et correspondre à la confirmation.',
     'user' => "Nous n'avons pas trouvé d'utilisateur avec cette adresse.",
     'token' => 'Le mot de passe reset du token n\'est pas valide pour cette adresse e-mail.',
-    'sent' => 'Nous vous avons envoyé un lien de réinitialisation de mot de passe !',
+    'sent' => 'Nous vous avons envoyé un lien de réinitialisation de mot de passe par e-mail !',
     'reset' => 'Votre mot de passe a été réinitialisé !',
 
 ];
index ab1febc65d92534e46716bfbab10633b3546cb5f..aa58aeee080d4016964876fa3e85faeb7c9ca760 100644 (file)
@@ -21,16 +21,16 @@ return [
     'app_public_access_desc' => 'L\'activation de cette option permettra aux visiteurs, qui ne sont pas connectés, d\'accéder au contenu de votre instance BookStack.',
     'app_public_access_desc_guest' => 'L\'accès pour les visiteurs publics peut être contrôlé par l\'utilisateur "Guest".',
     'app_public_access_toggle' => 'Autoriser l\'accès public',
-    'app_public_viewing' => 'Accepter le visionnage public des pages ?',
-    'app_secure_images' => 'Activer l\'ajout d\'image sécurisé ?',
+    'app_public_viewing' => 'Accepter l\'affichage public des pages ?',
+    'app_secure_images' => 'Ajout d\'image sécurisé',
     'app_secure_images_toggle' => 'Activer l\'ajout d\'image sécurisé',
     'app_secure_images_desc' => 'Pour des questions de performances, toutes les images sont publiques. Cette option ajoute une chaîne aléatoire difficile à deviner dans les URLs des images.',
-    'app_editor' => 'Editeur des pages',
+    'app_editor' => 'Éditeur des pages',
     'app_editor_desc' => 'Sélectionnez l\'éditeur qui sera utilisé pour modifier les pages.',
     'app_custom_html' => 'HTML personnalisé dans l\'en-tête',
     'app_custom_html_desc' => 'Le contenu inséré ici sera ajouté en bas de la balise <head> de toutes les pages. Vous pouvez l\'utiliser pour ajouter du CSS personnalisé ou un tracker analytique.',
-    'app_custom_html_disabled_notice' => 'Le contenu de la tête HTML personnalisée est désactivé sur cette page de paramètres pour garantir que les modifications les plus récentes peuvent être annulées.',
-    'app_logo' => 'Logo de l\'Application',
+    'app_custom_html_disabled_notice' => 'Le contenu de l\'en-tête HTML personnalisé est désactivé sur cette page de paramètres pour garantir que les modifications les plus récentes puissent être annulées.',
+    'app_logo' => 'Logo de l\'application',
     'app_logo_desc' => 'Cette image doit faire 43px de hauteur. <br>Les images plus larges seront réduites.',
     'app_primary_color' => 'Couleur principale de l\'application',
     'app_primary_color_desc' => 'Cela devrait être une valeur hexadécimale. <br>Laisser vide pour rétablir la couleur par défaut.',
@@ -38,7 +38,7 @@ return [
     'app_homepage_desc' => 'Choisissez une page à afficher sur la page d\'accueil au lieu de la vue par défaut. Les permissions sont ignorées pour les pages sélectionnées.',
     'app_homepage_select' => 'Choisissez une page',
     'app_footer_links' => 'Liens de pied de page',
-    'app_footer_links_desc' => 'Ajoutez des liens dans le pied de page du site. Ils seront affichés en bas de la plupart des pages, incluant celles qui ne nécesittent pas de connexion. Vous pouvez utiliser l\'étiquette "trans::<key>" pour utiliser les traductions définies par le système. Par exemple, utiliser "trans::common.privacy_policy" fournira la traduction de "Politique de Confidentalité" et "trans::common.terms_of_service" fournira la traduction de "Conditions d\'utilisation".',
+    'app_footer_links_desc' => 'Ajouter des liens à afficher dans le pied de page du site. Ils seront affichés en bas de la plupart des pages, y compris celles qui ne nécessitent pas de connexion. Vous pouvez utiliser une étiquette de "trans::<key>" pour utiliser les traductions définies par le système. Par exemple : utiliser "trans::common.privacy_policy" fournira le texte traduit "Privacy Policy" et "trans::common.terms_of_service" fournira le texte traduit "Terms of Service".',
     'app_footer_links_label' => 'Libellé du lien',
     'app_footer_links_url' => 'URL du lien',
     'app_footer_links_add' => 'Ajouter un lien en pied de page',
@@ -49,11 +49,11 @@ return [
     // Color settings
     'content_colors' => 'Couleur du contenu',
     'content_colors_desc' => 'Définit les couleurs pour tous les éléments de la hiérarchie d\'organisation des pages. Choisir les couleurs avec une luminosité similaire aux couleurs par défaut est recommandé pour la lisibilité.',
-    'bookshelf_color' => 'Couleur de l\'étagère',
-    'book_color' => 'Couleur du livre',
-    'chapter_color' => 'Couleur du chapitre',
-    'page_color' => 'Couleur de la page',
-    'page_draft_color' => 'Couleur du brouillon',
+    'bookshelf_color' => 'Couleur des étagères',
+    'book_color' => 'Couleur des livres',
+    'chapter_color' => 'Couleur des chapitres',
+    'page_color' => 'Couleur des pages',
+    'page_draft_color' => 'Couleur des brouillons',
 
     // Registration Settings
     'reg_settings' => 'Préférence pour l\'inscription',
@@ -72,19 +72,19 @@ return [
     // Maintenance settings
     '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_desc' => "Scanne le contenu des pages et des révisions pour vérifier les images, les dessins en cours d'utilisation et les doublons. Assurez-vous d'avoir une sauvegarde de la base de données et des images avant de lancer ceci.",
     '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_warning' => ':count images potentiellement inutilisées trouvées. Êtes-vous sûr de vouloir supprimer ces images ?',
     'maint_image_cleanup_success' => ':count images potentiellement inutilisées trouvées et supprimées !',
     'maint_image_cleanup_nothing_found' => 'Aucune image inutilisée trouvée, rien à supprimer !',
-    'maint_send_test_email' => 'Envoyer un email de test',
+    'maint_send_test_email' => 'Envoyer un e-mail de test',
     'maint_send_test_email_desc' => 'Ceci envoie un e-mail de test à votre adresse e-mail spécifiée dans votre profil.',
-    'maint_send_test_email_run' => 'Envoyer un email de test',
-    'maint_send_test_email_success' => 'Email envoyé à :address',
-    '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_send_test_email_run' => 'Envoyer un e-mail de test',
+    'maint_send_test_email_success' => 'E-mail envoyé à :address',
+    'maint_send_test_email_mail_subject' => 'E-mail de test',
+    'maint_send_test_email_mail_greeting' => 'L\'envoi d\'e-mail semble fonctionner !',
+    'maint_send_test_email_mail_text' => 'Félicitations ! Comme vous avez bien reçu cette notification, vos paramètres d\'e-mail 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',
 
@@ -92,18 +92,20 @@ return [
     '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_parent' => 'Parent',
     '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' => '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_restore_parent' => 'Restaurer le parent',
     '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.',
 
@@ -115,9 +117,10 @@ return [
     'audit_deleted_item' => 'Élément supprimé',
     'audit_deleted_item_name' => 'Nom: :name',
     'audit_table_user' => 'Utilisateur',
-    'audit_table_event' => 'Evènement',
-    'audit_table_related' => 'Élément ou détail lié',
-    'audit_table_date' => 'Date d\'activation',
+    'audit_table_event' => 'Événement',
+    'audit_table_related' => 'Élément concerné ou action réalisée',
+    'audit_table_ip' => 'Adresse IP',
+    'audit_table_date' => 'Horodatage',
     'audit_date_from' => 'À partir du',
     'audit_date_to' => 'Jusqu\'au',
 
@@ -136,6 +139,7 @@ return [
     'role_details' => 'Détails du rôle',
     'role_name' => 'Nom du rôle',
     'role_desc' => 'Courte description du rôle',
+    'role_mfa_enforced' => 'Nécessite une authentification multi-facteurs',
     'role_external_auth_id' => 'Identifiants d\'authentification externes',
     'role_system' => 'Permissions système',
     'role_manage_users' => 'Gérer les utilisateurs',
@@ -145,8 +149,9 @@ return [
     'role_manage_page_templates' => 'Gérer les modèles de page',
     'role_access_api' => 'Accès à l\'API du système',
     'role_manage_settings' => 'Gérer les préférences de l\'application',
+    'role_export_content' => 'Exporter le contenu',
     'role_asset' => 'Permissions des ressources',
-    'roles_system_warning' => 'Sachez que l\'accès à l\'une des trois permissions ci-dessus peut permettre à un utilisateur de modifier ses propres privilèges ou les privilèges des autres utilisateurs du système. Attribuer uniquement des rôles avec ces permissions à des utilisateurs de confiance.',
+    'roles_system_warning' => 'Sachez que l\'accès à l\'une des trois permissions ci-dessus peut permettre à un utilisateur de modifier ses propres privilèges ou les privilèges des autres utilisateurs du système. N\'attribuez uniquement des rôles avec ces permissions qu\'à des utilisateurs de confiance.',
     'role_asset_desc' => 'Ces permissions contrôlent l\'accès par défaut des ressources dans le système. Les permissions dans les livres, les chapitres et les pages ignoreront ces permissions',
     'role_asset_admins' => 'Les administrateurs ont automatiquement accès à tous les contenus mais les options suivantes peuvent afficher ou masquer certaines options de l\'interface.',
     'role_all' => 'Tous',
@@ -161,7 +166,7 @@ return [
     'users' => 'Utilisateurs',
     'user_profile' => 'Profil d\'utilisateur',
     'users_add_new' => 'Ajouter un nouvel utilisateur',
-    'users_search' => 'Chercher les utilisateurs',
+    'users_search' => 'Rechercher 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.',
@@ -169,20 +174,20 @@ return [
     'users_role' => 'Rôles de l\'utilisateur',
     'users_role_desc' => 'Sélectionnez les rôles auxquels cet utilisateur sera affecté. Si un utilisateur est affecté à plusieurs rôles, les permissions de ces rôles s\'empileront et ils recevront toutes les capacités des rôles affectés.',
     'users_password' => 'Mot de passe de l\'utilisateur',
-    'users_password_desc' => 'Définissez un mot de passe utilisé pour vous connecter à l\'application. Il doit comporter au moins 5 caractères.',
-    'users_send_invite_text' => 'Vous pouvez choisir d\'envoyer à cet utilisateur un email d\'invitation qui lui permet de définir son propre mot de passe, sinon vous pouvez définir son mot de passe vous-même.',
+    'users_password_desc' => 'Définissez un mot de passe utilisé pour vous connecter à l\'application. Il doit comporter au moins 6 caractères.',
+    'users_send_invite_text' => 'Vous pouvez choisir d\'envoyer à cet utilisateur un e-mail d\'invitation qui lui permet de définir son propre mot de passe, sinon vous pouvez définir son mot de passe vous-même.',
     'users_send_invite_option' => 'Envoyer l\'e-mail d\'invitation',
     'users_external_auth_id' => 'Identifiant d\'authentification externe',
     'users_external_auth_id_desc' => 'C\'est l\'ID utilisé pour correspondre à cet utilisateur lors de la communication avec votre système d\'authentification externe.',
-    'users_password_warning' => 'Remplissez ce formulaire uniquement si vous souhaitez changer de mot de passe:',
+    'users_password_warning' => 'Remplissez ce formulaire uniquement si vous souhaitez changer de mot de passe :',
     'users_system_public' => 'Cet utilisateur représente les invités visitant votre instance. Il est assigné automatiquement aux invités.',
     'users_delete' => 'Supprimer un utilisateur',
     '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_migrate_ownership' => 'Migré propriété',
+    'users_migrate_ownership' => 'Transférer la 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_none_selected' => 'Aucun utilisateur n\'a été sélectionné',
     'users_delete_success' => 'Utilisateur supprimé avec succès',
     'users_edit' => 'Modifier l\'utilisateur',
     'users_edit_profile' => 'Modifier le profil',
@@ -191,17 +196,21 @@ return [
     'users_avatar_desc' => 'Cette image doit être un carré d\'environ 256 px.',
     'users_preferred_language' => 'Langue préférée',
     'users_preferred_language_desc' => 'Cette option changera la langue utilisée pour l\'interface utilisateur de l\'application. Ceci n\'affectera aucun contenu créé par l\'utilisateur.',
-    'users_social_accounts' => 'Comptes sociaux',
+    'users_social_accounts' => 'Réseaux sociaux',
     'users_social_accounts_info' => 'Vous pouvez connecter des réseaux sociaux à votre compte pour vous connecter plus rapidement. Déconnecter un compte n\'enlèvera pas les accès autorisés précédemment sur votre compte de réseau social.',
     'users_social_connect' => 'Connecter le compte',
     'users_social_disconnect' => 'Déconnecter le compte',
     'users_social_connected' => 'Votre compte :socialAccount a été ajouté avec succès.',
     'users_social_disconnected' => 'Votre compte :socialAccount a été déconnecté avec succès',
-    'users_api_tokens' => 'Jetons de l\'API',
+    'users_api_tokens' => 'Jetons API',
     'users_api_tokens_none' => 'Aucun jeton API n\'a été créé pour cet utilisateur',
     'users_api_tokens_create' => 'Créer un jeton',
     'users_api_tokens_expires' => 'Expiré',
     'users_api_tokens_docs' => 'Documentation de l\'API',
+    'users_mfa' => 'Authentification multi-facteurs',
+    'users_mfa_desc' => 'Configurer l\'authentification multi-facteurs ajoute une couche supplémentaire de sécurité à votre compte utilisateur.',
+    'users_mfa_x_methods' => ':count méthode configurée|:count méthodes configurées',
+    'users_mfa_configure' => 'Méthode de configuration',
 
     // API Tokens
     'user_api_token_create' => 'Créer un nouveau jeton API',
@@ -210,19 +219,19 @@ return [
     'user_api_token_expiry' => 'Date d\'expiration',
     'user_api_token_expiry_desc' => 'Définissez une date à laquelle ce jeton expire. Après cette date, les demandes effectuées à l\'aide de ce jeton ne fonctionneront plus. Le fait de laisser ce champ vide entraînera une expiration dans 100 ans.',
     'user_api_token_create_secret_message' => 'Immédiatement après la création de ce jeton, un "ID de jeton" "et" Secret de jeton "sera généré et affiché. Le secret ne sera affiché qu\'une seule fois, alors assurez-vous de copier la valeur dans un endroit sûr et sécurisé avant de continuer.',
-    'user_api_token_create_success' => 'L\'API token a été créé avec succès',
-    'user_api_token_update_success' => 'L\'API token a été mis à jour avec succès',
-    'user_api_token' => 'Token API',
+    'user_api_token_create_success' => 'Le jeton API a été créé avec succès',
+    'user_api_token_update_success' => 'Le jeton API a été mis à jour avec succès',
+    'user_api_token' => 'Jeton API',
     'user_api_token_id' => 'Token ID',
     'user_api_token_id_desc' => 'Il s\'agit d\'un identifiant généré par le système non modifiable pour ce jeton qui devra être fourni dans les demandes d\'API.',
     'user_api_token_secret' => 'Token Secret',
     'user_api_token_secret_desc' => 'Il s\'agit d\'un secret généré par le système pour ce jeton, qui devra être fourni dans les demandes d\'API. Cela ne sera affiché qu\'une seule fois, alors copiez cette valeur dans un endroit sûr et sécurisé.',
     'user_api_token_created' => 'Jeton créé :timeAgo',
     'user_api_token_updated' => 'Jeton mis à jour :timeAgo',
-    'user_api_token_delete' => 'Supprimer le Token',
+    'user_api_token_delete' => 'Supprimer le jeton',
     'user_api_token_delete_warning' => 'Cela supprimera complètement le jeton d\'API avec le nom \':tokenName\'.',
-    'user_api_token_delete_confirm' => 'Souhaitez-vous vraiment effacer l\'API Token ?',
-    'user_api_token_delete_success' => 'L\'API token a été supprimé avec succès',
+    'user_api_token_delete_confirm' => 'Souhaitez-vous vraiment effacer ce jeton API ?',
+    'user_api_token_delete_success' => 'Le jeton API a été supprimé avec succès',
 
     //! If editing translations files directly please ignore this in all
     //! languages apart from en. Content will be auto-copied from en.
@@ -247,6 +256,7 @@ return [
         'it' => 'Italian',
         'ja' => '日本語',
         'ko' => '한국어',
+        'lt' => 'Lietuvių Kalba',
         'lv' => 'Latviešu Valoda',
         'nl' => 'Nederlands',
         'nb' => 'Norvegien',
index 684404f42d45c5ad921c0d95fa879c714c41dc40..71a5e8e083d9b6a420833d9472816c2f75c105a7 100644 (file)
@@ -15,10 +15,11 @@ return [
     'alpha_dash'           => ':attribute doit contenir uniquement des lettres, chiffres et traits d\'union.',
     'alpha_num'            => ':attribute doit contenir uniquement des chiffres et des lettres.',
     'array'                => ':attribute doit être un tableau.',
+    'backup_codes'         => 'Le code fourni n\'est pas valide ou a déjà été utilisé.',
     'before'               => ':attribute doit être inférieur à :date.',
     'between'              => [
         'numeric' => ':attribute doit être compris entre :min et :max.',
-        'file'    => ':attribute doit être compris entre :min et :max kilobytes.',
+        'file'    => ':attribute doit être compris entre :min et :max Ko.',
         'string'  => ':attribute doit être compris entre :min et :max caractères.',
         'array'   => ':attribute doit être compris entre :min et :max éléments.',
     ],
@@ -34,13 +35,13 @@ return [
     'filled'               => ':attribute est un champ requis.',
     'gt'                   => [
         'numeric' => ':attribute doit être plus grand que :value.',
-        'file'    => ':attribute doit être plus grand que :value kilobytes.',
+        'file'    => ':attribute doit être plus grand que :value Ko.',
         'string'  => ':attribute doit être plus grand que :value caractères.',
         'array'   => ':attribute doit avoir plus que :value éléments.',
     ],
     'gte'                  => [
         'numeric' => ':attribute doit être plus grand ou égal à :value.',
-        'file'    => ':attribute doit être plus grand ou égal à :value kilobytes.',
+        'file'    => ':attribute doit être plus grand ou égal à :value Ko.',
         'string'  => ':attribute doit être plus grand ou égal à :value caractères.',
         'array'   => ':attribute doit avoir :value éléments ou plus.',
     ],
@@ -52,22 +53,22 @@ return [
     'ip'                   => ':attribute doit être une adresse IP valide.',
     'ipv4'                 => ':attribute doit être une adresse IPv4 valide.',
     'ipv6'                 => ':attribute doit être une adresse IPv6 valide.',
-    'json'                 => ':attribute doit être une chaine JSON valide.',
+    'json'                 => ':attribute doit être une chaîne JSON valide.',
     'lt'                   => [
         'numeric' => ':attribute doit être plus petit que :value.',
-        'file'    => ':attribute doit être plus petit que :value kilobytes.',
+        'file'    => ':attribute doit être plus petit que :value Ko.',
         'string'  => ':attribute doit être plus petit que :value caractères.',
         'array'   => ':attribute doit avoir moins de :value éléments.',
     ],
     'lte'                  => [
         'numeric' => ':attribute doit être plus petit ou égal à :value.',
-        'file'    => ':attribute doit être plus petit ou égal à :value kilobytes.',
+        'file'    => ':attribute doit être plus petit ou égal à :value Ko.',
         'string'  => ':attribute doit être plus petit ou égal à :value caractères.',
         'array'   => ':attribute ne doit pas avoir plus de :value éléments.',
     ],
     'max'                  => [
         'numeric' => ':attribute ne doit pas excéder :max.',
-        'file'    => ':attribute ne doit pas excéder :max kilobytes.',
+        'file'    => ':attribute ne doit pas excéder :max Ko.',
         'string'  => ':attribute ne doit pas excéder :max caractères.',
         'array'   => ':attribute ne doit pas contenir plus de :max éléments.',
     ],
@@ -98,6 +99,7 @@ return [
     ],
     'string'               => ':attribute doit être une chaîne de caractères.',
     'timezone'             => ':attribute doit être une zone valide.',
+    'totp'                 => 'Le code fourni n\'est pas valide ou est expiré.',
     'unique'               => ':attribute est déjà utilisé.',
     'url'                  => ':attribute a un format invalide.',
     'uploaded'             => 'Le fichier n\'a pas pu être envoyé. Le serveur peut ne pas accepter des fichiers de cette taille.',
index dfa54344281b87a35cf2c31b91f39c844e3ca615..c19825afe3458404ea9fd973d4dde1ecd44745c3 100644 (file)
@@ -47,6 +47,10 @@ return [
     'favourite_add_notification' => '":name" has been added to your favourites',
     'favourite_remove_notification' => '":name" has been removed from your favourites',
 
+    // MFA
+    'mfa_setup_method_notification' => 'Multi-factor method successfully configured',
+    'mfa_remove_method_notification' => 'Multi-factor method successfully removed',
+
     // Other
     'commented_on'                => 'commented on',
     'permissions_update'          => 'updated permissions',
index 733c84f9de45bca28b12d202924152ce72b7676e..a078cfcf8bc0eb515e74f3d6fe5b080f5711f40f 100644 (file)
@@ -73,5 +73,40 @@ return [
     '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_success' => 'Password set, you now have access to :appName!',
+
+    // Multi-factor Authentication
+    'mfa_setup' => 'Setup Multi-Factor Authentication',
+    'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
+    'mfa_setup_configured' => 'Already configured',
+    'mfa_setup_reconfigure' => 'Reconfigure',
+    'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?',
+    'mfa_setup_action' => 'Setup',
+    'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.',
+    'mfa_option_totp_title' => 'Mobile App',
+    'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
+    'mfa_option_backup_codes_title' => 'Backup Codes',
+    'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.',
+    'mfa_gen_confirm_and_enable' => 'Confirm and Enable',
+    'mfa_gen_backup_codes_title' => 'Backup Codes Setup',
+    'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.',
+    'mfa_gen_backup_codes_download' => 'Download Codes',
+    'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once',
+    'mfa_gen_totp_title' => 'Mobile App Setup',
+    'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
+    'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.',
+    'mfa_gen_totp_verify_setup' => 'Verify Setup',
+    'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:',
+    'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here',
+    'mfa_verify_access' => 'Verify Access',
+    'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.',
+    'mfa_verify_no_methods' => 'No Methods Configured',
+    'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.',
+    'mfa_verify_use_totp' => 'Verify using a mobile app',
+    'mfa_verify_use_backup_codes' => 'Verify using a backup code',
+    'mfa_verify_backup_code' => 'Backup Code',
+    'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:',
+    'mfa_verify_backup_code_enter_here' => 'Enter backup code here',
+    'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:',
+    'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.',
 ];
\ No newline at end of file
index 3ab175bae7e438b7dfb4b3abfe3e6b53e512e19b..475987f342ce9a4e92c2d026f614ab06bc65f97b 100644 (file)
@@ -39,6 +39,7 @@ return [
     'reset' => 'איפוס',
     'remove' => 'הסר',
     'add' => 'הוסף',
+    'configure' => 'Configure',
     'fullscreen' => 'Fullscreen',
     'favourite' => 'Favourite',
     'unfavourite' => 'Unfavourite',
@@ -60,6 +61,7 @@ return [
     'no_activity' => 'אין פעילות להציג',
     'no_items' => 'אין פריטים זמינים',
     'back_to_top' => 'בחזרה ללמעלה',
+    'skip_to_main_content' => 'Skip to main content',
     'toggle_details' => 'הצג/הסתר פרטים',
     'toggle_thumbnails' => 'הצג/הסתר תמונות',
     'details' => 'פרטים',
index a0b4918aee12e8dc2b81064f12cd545ea8eb5fe7..23aa50158c718c529cf343df724bf6decb0897c4 100644 (file)
@@ -36,6 +36,7 @@ return [
     'export_html' => 'דף אינטרנט',
     'export_pdf' => 'קובץ PDF',
     'export_text' => 'טקסט רגיל',
+    'export_md' => 'Markdown File',
 
     // Permissions and restrictions
     'permissions' => 'הרשאות',
@@ -98,6 +99,7 @@ return [
     'shelves_permissions' => 'הרשאות מדף',
     'shelves_permissions_updated' => 'הרשאות מדף עודכנו',
     'shelves_permissions_active' => 'הרשאות מדף פעילות',
+    'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',
     'shelves_copy_permissions_to_books' => 'העתק הרשאות מדף אל הספרים',
     'shelves_copy_permissions' => 'העתק הרשאות',
     'shelves_copy_permissions_explain' => 'פעולה זו תעתיק את כל הרשאות המדף לכל הספרים המשוייכים למדף זה. לפני הביצוע, יש לוודא שכל הרשאות המדף אכן נשמרו.',
index a648e2e0203b8016fb9a7920c048066ed9a23573..e158867797e8b89fb8e5134b135ebb570c0a7a0b 100755 (executable)
@@ -92,6 +92,7 @@ return [
     'recycle_bin' => 'סל המיחזור',
     'recycle_bin_desc' => 'כאן תוכלו לאחזר פריטים שנמחקו או לבחור למחוק אותם מהמערכת לצמיתות. רשימה זו לא מסוננת, בשונה מרשימות פעילות דומות במערכת, בהן מוחלים מסנני הרשאות.',
     'recycle_bin_deleted_item' => 'פריט שנמחק',
+    'recycle_bin_deleted_parent' => 'Parent',
     'recycle_bin_deleted_by' => 'נמחק על ידי',
     'recycle_bin_deleted_at' => 'זמן המחיקה',
     'recycle_bin_permanently_delete' => 'מחק לצמיתות',
@@ -104,6 +105,7 @@ return [
     'recycle_bin_restore_list' => 'פריטים שיאוחזרו',
     'recycle_bin_restore_confirm' => 'פעולה זו תאחזר את הפריט שנמחק, לרבות רכיבי-הבן שלו, למיקומו המקורי. אם המיקום המקורי נמחק מאז, וכעת נמצא בסל המיחזור, יש לאחזר גם את פריט-האב.',
     'recycle_bin_restore_deleted_parent' => 'פריט-האב של פריט זה נמחק. פריטים אלה יישארו מחוקים עד שפריט-אב זה יאוחזר.',
+    'recycle_bin_restore_parent' => 'Restore Parent',
     'recycle_bin_destroy_notification' => 'נמחקו בסה"כ :count פריטים מסל המיחזור.',
     'recycle_bin_restore_notification' => 'אוחזרו בסה"כ :count פריטים מסל המיחזור.',
 
@@ -117,6 +119,7 @@ return [
     'audit_table_user' => 'משתמש',
     'audit_table_event' => 'אירוע',
     'audit_table_related' => 'פריט או פרט קשור',
+    'audit_table_ip' => 'IP Address',
     'audit_table_date' => 'זמן הפעילות',
     'audit_date_from' => 'טווח תאריכים החל מ...',
     'audit_date_to' => 'טווח תאריכים עד ל...',
@@ -136,6 +139,7 @@ return [
     'role_details' => 'פרטי תפקיד',
     'role_name' => 'שם התפקיד',
     'role_desc' => 'תיאור קצר של התפקיד',
+    'role_mfa_enforced' => 'Requires Multi-Factor Authentication',
     'role_external_auth_id' => 'ID-י אותנטיקציה חיצוניים',
     'role_system' => 'הרשאות מערכת',
     'role_manage_users' => 'ניהול משתמשים',
@@ -145,6 +149,7 @@ return [
     'role_manage_page_templates' => 'נהל תבניות דפים',
     'role_access_api' => 'גש ל-API המערכת',
     'role_manage_settings' => 'ניהול הגדרות יישום',
+    'role_export_content' => 'Export content',
     'role_asset' => 'הרשאות משאבים',
     'roles_system_warning' => 'שימו לב לכך שגישה לכל אחת משלושת ההרשאות הנ"ל יכולה לאפשר למשתמש לשנות את הפריווילגיות שלהם או של אחרים במערכת. הגדירו תפקידים להרשאות אלה למשתמשים בהם אתם בוטחים בלבד.',
     'role_asset_desc' => 'הרשאות אלו שולטות בגישת ברירת המחדל למשאבים בתוך המערכת. הרשאות של ספרים, פרקים ודפים יגברו על הרשאות אלו.',
@@ -202,6 +207,10 @@ return [
     'users_api_tokens_create' => 'צור אסימון',
     'users_api_tokens_expires' => 'פג',
     'users_api_tokens_docs' => 'תיעוד API',
+    'users_mfa' => 'Multi-Factor Authentication',
+    'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
+    'users_mfa_x_methods' => ':count method configured|:count methods configured',
+    'users_mfa_configure' => 'Configure Methods',
 
     // API Tokens
     'user_api_token_create' => 'צור אסימון API',
@@ -247,6 +256,7 @@ return [
         'it' => 'Italian',
         'ja' => '日本語',
         'ko' => '한국어',
+        'lt' => 'Lietuvių Kalba',
         'lv' => 'Latviešu Valoda',
         'nl' => 'Nederlands',
         'nb' => 'Norsk (Bokmål)',
index 8a18ca27a21c3c85a81a18c891d3ba88553e0247..e52c28b42ec58ca2de43e469397716b27aa56b50 100644 (file)
@@ -15,6 +15,7 @@ return [
     'alpha_dash'           => 'שדה :attribute יכול להכיל אותיות, מספרים ומקפים בלבד.',
     'alpha_num'            => 'שדה :attribute יכול להכיל אותיות ומספרים בלבד.',
     'array'                => 'שדה :attribute חייב להיות מערך.',
+    'backup_codes'         => 'The provided code is not valid or has already been used.',
     'before'               => 'שדה :attribute חייב להיות תאריך לפני :date.',
     'between'              => [
         'numeric' => 'שדה :attribute חייב להיות בין :min ל-:max.',
@@ -98,6 +99,7 @@ return [
     ],
     'string'               => 'שדה :attribute חייב להיות מחרוזת.',
     'timezone'             => 'שדה :attribute חייב להיות איזור תקני.',
+    'totp'                 => 'The provided code is not valid or has expired.',
     'unique'               => 'שדה :attribute כבר תפוס.',
     'url'                  => 'שדה :attribute בעל פורמט שאינו תקין.',
     'uploaded'             => 'שדה :attribute ארעה שגיאה בעת ההעלאה.',
index a3340b4ddf1d8aea038800238c6cc4b17de90b5e..6609f552c435ffc7a3fc249b38555913f642f164 100644 (file)
@@ -47,6 +47,10 @@ return [
     'favourite_add_notification' => '":name" has been added to your favourites',
     'favourite_remove_notification' => '":name" has been removed from your favourites',
 
+    // MFA
+    'mfa_setup_method_notification' => 'Multi-factor method successfully configured',
+    'mfa_remove_method_notification' => 'Multi-factor method successfully removed',
+
     // Other
     'commented_on'                => 'komentirano',
     'permissions_update'          => 'ažurirana dopuštenja',
index 9680cc70062e9f65d96cce8d1262fb6f0e54936e..23014439a19248dcc76e528d195c8119ea62bd3b 100644 (file)
@@ -73,5 +73,40 @@ return [
     'user_invite_page_welcome' => 'Dobrodošli u :appName!',
     'user_invite_page_text' => 'Da biste postavili račun i dobili pristup trebate unijeti lozinku kojom ćete se ubuduće prijaviti na :appName.',
     'user_invite_page_confirm_button' => 'Potvrdite lozinku',
-    'user_invite_success' => 'Lozinka je postavljena, možete pristupiti :appName!'
+    'user_invite_success' => 'Lozinka je postavljena, možete pristupiti :appName!',
+
+    // Multi-factor Authentication
+    'mfa_setup' => 'Setup Multi-Factor Authentication',
+    'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
+    'mfa_setup_configured' => 'Already configured',
+    'mfa_setup_reconfigure' => 'Reconfigure',
+    'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?',
+    'mfa_setup_action' => 'Setup',
+    'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.',
+    'mfa_option_totp_title' => 'Mobile App',
+    'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
+    'mfa_option_backup_codes_title' => 'Backup Codes',
+    'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.',
+    'mfa_gen_confirm_and_enable' => 'Confirm and Enable',
+    'mfa_gen_backup_codes_title' => 'Backup Codes Setup',
+    'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.',
+    'mfa_gen_backup_codes_download' => 'Download Codes',
+    'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once',
+    'mfa_gen_totp_title' => 'Mobile App Setup',
+    'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
+    'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.',
+    'mfa_gen_totp_verify_setup' => 'Verify Setup',
+    'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:',
+    'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here',
+    'mfa_verify_access' => 'Verify Access',
+    'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.',
+    'mfa_verify_no_methods' => 'No Methods Configured',
+    'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.',
+    'mfa_verify_use_totp' => 'Verify using a mobile app',
+    'mfa_verify_use_backup_codes' => 'Verify using a backup code',
+    'mfa_verify_backup_code' => 'Backup Code',
+    'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:',
+    'mfa_verify_backup_code_enter_here' => 'Enter backup code here',
+    'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:',
+    'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.',
 ];
\ No newline at end of file
index 1df3c227ce614a3a84a22b125c892f86b0cd5946..bc51eed22abc2335bd2280d412f2da1e06cbf350 100644 (file)
@@ -39,6 +39,7 @@ return [
     'reset' => 'Ponovno postavi',
     'remove' => 'Ukloni',
     'add' => 'Dodaj',
+    'configure' => 'Configure',
     'fullscreen' => 'Cijeli zaslon',
     'favourite' => 'Favourite',
     'unfavourite' => 'Unfavourite',
@@ -60,6 +61,7 @@ return [
     'no_activity' => 'Nema aktivnosti za pregled',
     'no_items' => 'Nedostupno',
     'back_to_top' => 'Natrag na vrh',
+    'skip_to_main_content' => 'Skip to main content',
     'toggle_details' => 'Prebaci detalje',
     'toggle_thumbnails' => 'Uključi minijature',
     'details' => 'Detalji',
index e8a8004e23c82bbbbceb2f69571d4720559c8650..4bef3814f668610601f5669f76a9d9d0d9649086 100644 (file)
@@ -36,6 +36,7 @@ return [
     'export_html' => 'Web File',
     'export_pdf' => 'PDF File',
     'export_text' => 'Text File',
+    'export_md' => 'Markdown File',
 
     // Permissions and restrictions
     'permissions' => 'Dopuštenja',
@@ -98,6 +99,7 @@ return [
     'shelves_permissions' => 'Dopuštenja za policu',
     'shelves_permissions_updated' => 'Ažurirana dopuštenja za policu',
     'shelves_permissions_active' => 'Aktivirana dopuštenja za policu',
+    'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',
     'shelves_copy_permissions_to_books' => 'Kopiraj dopuštenja za knjige',
     'shelves_copy_permissions' => 'Kopiraj dopuštenja',
     'shelves_copy_permissions_explain' => 'Ovo će promijeniti trenutna dopuštenja za policu i knjige u njoj. Prije aktivacije provjerite jesu li sve dopuštenja za ovu policu spremljena.',
index 9c4d6ebe450cf7324715040fae90004d352eb7ad..547f27a83a2abbb885c07bfeef946771b7231f65 100644 (file)
@@ -92,6 +92,7 @@ return [
     'recycle_bin' => 'Recycle Bin',
     'recycle_bin_desc' => 'Ovdje možete vratiti izbrisane stavke ili ih trajno ukloniti iz sustava. Popis nije filtriran kao što su to popisi u kojima su omogućeni filteri.',
     'recycle_bin_deleted_item' => 'Izbrisane stavke',
+    'recycle_bin_deleted_parent' => 'Parent',
     'recycle_bin_deleted_by' => 'Izbrisano od',
     'recycle_bin_deleted_at' => 'Vrijeme brisanja',
     'recycle_bin_permanently_delete' => 'Trajno izbrisano',
@@ -104,6 +105,7 @@ return [
     'recycle_bin_restore_list' => 'Stavke koje treba vratiti',
     'recycle_bin_restore_confirm' => 'Ova radnja vraća izbrisane stavke i njene podređene elemente na prvobitnu lokaciju. Ako je nadređena stavka izbrisana i nju treba vratiti.',
     'recycle_bin_restore_deleted_parent' => 'S obzirom da je nadređena stavka obrisana najprije treba vratiti nju.',
+    'recycle_bin_restore_parent' => 'Restore Parent',
     'recycle_bin_destroy_notification' => 'Ukupno izbrisane :count stavke iz Recycle Bin',
     'recycle_bin_restore_notification' => 'Ukupno vraćene :count stavke iz Recycle Bin',
 
@@ -117,6 +119,7 @@ return [
     'audit_table_user' => 'Korisnik',
     'audit_table_event' => 'Događaj',
     'audit_table_related' => 'Povezana stavka ili detalj',
+    'audit_table_ip' => 'IP Address',
     'audit_table_date' => 'Datum aktivnosti',
     'audit_date_from' => 'Rangiraj datum od',
     'audit_date_to' => 'Rangiraj datum do',
@@ -136,6 +139,7 @@ return [
     'role_details' => 'Detalji uloge',
     'role_name' => 'Ime uloge',
     'role_desc' => 'Kratki opis uloge',
+    'role_mfa_enforced' => 'Requires Multi-Factor Authentication',
     'role_external_auth_id' => 'Autorizacija',
     'role_system' => 'Dopuštenja sustava',
     'role_manage_users' => 'Upravljanje korisnicima',
@@ -145,6 +149,7 @@ return [
     'role_manage_page_templates' => 'Upravljanje predlošcima stranica',
     'role_access_api' => 'API pristup',
     'role_manage_settings' => 'Upravljanje postavkama aplikacija',
+    'role_export_content' => 'Export content',
     'role_asset' => 'Upravljanje vlasništvom',
     'roles_system_warning' => 'Uzmite u obzir da pristup bilo kojem od ovih dopuštenja dozvoljavate korisniku upravljanje dopuštenjima ostalih u sustavu. Ova dopuštenja dodijelite pouzdanim korisnicima.',
     'role_asset_desc' => 'Ova dopuštenja kontroliraju zadane pristupe. Dopuštenja za knjige, poglavlja i stranice ih poništavaju.',
@@ -202,6 +207,10 @@ return [
     'users_api_tokens_create' => 'Stvori token',
     'users_api_tokens_expires' => 'Isteklo',
     'users_api_tokens_docs' => 'API dokumentacija',
+    'users_mfa' => 'Multi-Factor Authentication',
+    'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
+    'users_mfa_x_methods' => ':count method configured|:count methods configured',
+    'users_mfa_configure' => 'Configure Methods',
 
     // API Tokens
     'user_api_token_create' => 'Stvori API token',
@@ -247,6 +256,7 @@ return [
         'it' => 'Italian',
         'ja' => '日本語',
         'ko' => '한국어',
+        'lt' => 'Lietuvių Kalba',
         'lv' => 'Latviešu Valoda',
         'nl' => 'Nederlands',
         'nb' => 'Norsk (Bokmål)',
index 5b1849c5f1599687b42447803131f23b4222e37e..b88e872f1b8881fa22ecc5eaaaecb07fe1f1d66c 100644 (file)
@@ -15,6 +15,7 @@ return [
     'alpha_dash'           => ':attribute  može sadržavati samo slova, brojeve, crtice i donje crtice.',
     'alpha_num'            => ':attribute može sadržavati samo slova i brojeve.',
     'array'                => ':attribute mora biti niz.',
+    'backup_codes'         => 'The provided code is not valid or has already been used.',
     'before'               => ':attribute mora biti prije :date.',
     'between'              => [
         'numeric' => ':attribute mora biti između :min i :max.',
@@ -98,6 +99,7 @@ return [
     ],
     'string'               => ':attribute mora biti niz.',
     'timezone'             => ':attribute mora biti valjan.',
+    'totp'                 => 'The provided code is not valid or has expired.',
     'unique'               => ':attribute se već koristi.',
     'url'                  => 'Format :attribute nije valjan.',
     'uploaded'             => 'Datoteka se ne može prenijeti. Server možda ne prihvaća datoteke te veličine.',
index ff2ddb4908041f68b3ae3c6b4eae53edc3280fdc..98bdd798bac9e66e0ca1a9a6a9fda5a5fdde40a3 100644 (file)
@@ -47,6 +47,10 @@ return [
     'favourite_add_notification' => '":name" has been added to your favourites',
     'favourite_remove_notification' => '":name" has been removed from your favourites',
 
+    // MFA
+    'mfa_setup_method_notification' => 'Multi-factor method successfully configured',
+    'mfa_remove_method_notification' => 'Multi-factor method successfully removed',
+
     // Other
     'commented_on'                => 'megjegyzést fűzött hozzá:',
     'permissions_update'          => 'updated permissions',
index a13c8300e2084722c9cd4f623c9bf08e75ed8b3d..7aaedf92ddc7daddf5aba39825d6e4c80f4c1318 100644 (file)
@@ -73,5 +73,40 @@ return [
     'user_invite_page_welcome' => ':appName üdvözöl!',
     'user_invite_page_text' => 'A fiók véglegesítéséhez és a hozzáféréshez be kell állítani egy jelszót ami :appName weboldalon lesz használva a bejelentkezéshez.',
     'user_invite_page_confirm_button' => 'Jelszó megerősítése',
-    'user_invite_success' => 'Jelszó beállítva, :appName most már elérhető!'
+    'user_invite_success' => 'Jelszó beállítva, :appName most már elérhető!',
+
+    // Multi-factor Authentication
+    'mfa_setup' => 'Setup Multi-Factor Authentication',
+    'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
+    'mfa_setup_configured' => 'Already configured',
+    'mfa_setup_reconfigure' => 'Reconfigure',
+    'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?',
+    'mfa_setup_action' => 'Setup',
+    'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.',
+    'mfa_option_totp_title' => 'Mobile App',
+    'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
+    'mfa_option_backup_codes_title' => 'Backup Codes',
+    'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.',
+    'mfa_gen_confirm_and_enable' => 'Confirm and Enable',
+    'mfa_gen_backup_codes_title' => 'Backup Codes Setup',
+    'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.',
+    'mfa_gen_backup_codes_download' => 'Download Codes',
+    'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once',
+    'mfa_gen_totp_title' => 'Mobile App Setup',
+    'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
+    'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.',
+    'mfa_gen_totp_verify_setup' => 'Verify Setup',
+    'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:',
+    'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here',
+    'mfa_verify_access' => 'Verify Access',
+    'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.',
+    'mfa_verify_no_methods' => 'No Methods Configured',
+    'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.',
+    'mfa_verify_use_totp' => 'Verify using a mobile app',
+    'mfa_verify_use_backup_codes' => 'Verify using a backup code',
+    'mfa_verify_backup_code' => 'Backup Code',
+    'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:',
+    'mfa_verify_backup_code_enter_here' => 'Enter backup code here',
+    'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:',
+    'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.',
 ];
\ No newline at end of file
index 948bbaefd1a75ac9f4ffc5bf82faa600f0f13dae..2e850aa2eda08b1e65a8b9ce22a6ba69f4004ffb 100644 (file)
@@ -39,6 +39,7 @@ return [
     'reset' => 'Visszaállítás',
     'remove' => 'Eltávolítás',
     'add' => 'Hozzáadás',
+    'configure' => 'Configure',
     'fullscreen' => 'Teljes képernyő',
     'favourite' => 'Favourite',
     'unfavourite' => 'Unfavourite',
@@ -60,6 +61,7 @@ return [
     'no_activity' => 'Nincs megjeleníthető aktivitás',
     'no_items' => 'Nincsenek elérhető elemek',
     'back_to_top' => 'Oldal eleje',
+    'skip_to_main_content' => 'Skip to main content',
     'toggle_details' => 'Részletek átkapcsolása',
     'toggle_thumbnails' => 'Bélyegképek átkapcsolása',
     'details' => 'Részletek',
index 4d789bfd082bfa32b5ce7f1bc0f8da4959ef8d59..6050416e582f9bb9ad445026897bb382e6b21b77 100644 (file)
@@ -36,6 +36,7 @@ return [
     'export_html' => 'Webfájlt tartalmaz',
     'export_pdf' => 'PDF fájl',
     'export_text' => 'Egyszerű szövegfájl',
+    'export_md' => 'Markdown File',
 
     // Permissions and restrictions
     'permissions' => 'Jogosultságok',
@@ -98,6 +99,7 @@ return [
     'shelves_permissions' => 'Könyvespolc jogosultság',
     'shelves_permissions_updated' => 'Könyvespolc jogosultságok frissítve',
     'shelves_permissions_active' => 'Könyvespolc jogosultságok aktívak',
+    'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',
     'shelves_copy_permissions_to_books' => 'Jogosultság másolása könyvekre',
     'shelves_copy_permissions' => 'Jogosultság másolása',
     'shelves_copy_permissions_explain' => 'Ez alkalmazni fogja ennek a könyvespolcnak az aktuális jogosultság beállításait az összes benne található könyvön. Aktiválás előtt ellenőrizni kell, hogy a könyvespolc jogosultságain végzett összes módosítás el lett mentve.',
index d8f05200852f285585ac4a8ebabbc4490117b069..9cc3f840d433a8db7769ad7c279c624ec701c847 100644 (file)
@@ -92,6 +92,7 @@ return [
     '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_parent' => 'Parent',
     'recycle_bin_deleted_by' => 'Törölte',
     'recycle_bin_deleted_at' => 'Törlés ideje',
     'recycle_bin_permanently_delete' => 'Permanently Delete',
@@ -104,6 +105,7 @@ return [
     '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_restore_parent' => 'Restore Parent',
     'recycle_bin_destroy_notification' => 'Deleted :count total items from the recycle bin.',
     'recycle_bin_restore_notification' => 'Restored :count total items from the recycle bin.',
 
@@ -117,6 +119,7 @@ return [
     'audit_table_user' => 'Felhasználó',
     'audit_table_event' => 'Esemény',
     'audit_table_related' => 'Related Item or Detail',
+    'audit_table_ip' => 'IP Address',
     'audit_table_date' => 'Activity Date',
     'audit_date_from' => 'Date Range From',
     'audit_date_to' => 'Date Range To',
@@ -136,6 +139,7 @@ return [
     'role_details' => 'Szerepkör részletei',
     'role_name' => 'Szerepkör neve',
     'role_desc' => 'Szerepkör rövid leírása',
+    'role_mfa_enforced' => 'Requires Multi-Factor Authentication',
     'role_external_auth_id' => 'Külső hitelesítés azonosítók',
     'role_system' => 'Rendszer jogosultságok',
     'role_manage_users' => 'Felhasználók kezelése',
@@ -145,6 +149,7 @@ return [
     'role_manage_page_templates' => 'Oldalsablonok kezelése',
     'role_access_api' => 'Hozzáférés a rendszer API-hoz',
     'role_manage_settings' => 'Alkalmazás beállításainak kezelése',
+    'role_export_content' => 'Export content',
     'role_asset' => 'Eszköz jogosultságok',
     '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' => 'Ezek a jogosultság vezérlik a alapértelmezés szerinti hozzáférést a rendszerben található eszközökhöz. A könyvek, fejezetek és oldalak jogosultságai felülírják ezeket a jogosultságokat.',
@@ -202,6 +207,10 @@ return [
     'users_api_tokens_create' => 'Vezérjel létrehozása',
     'users_api_tokens_expires' => 'Lejárat',
     'users_api_tokens_docs' => 'API dokumentáció',
+    'users_mfa' => 'Multi-Factor Authentication',
+    'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
+    'users_mfa_x_methods' => ':count method configured|:count methods configured',
+    'users_mfa_configure' => 'Configure Methods',
 
     // API Tokens
     'user_api_token_create' => 'API vezérjel létrehozása',
@@ -247,6 +256,7 @@ return [
         'it' => 'Italian',
         'ja' => '日本語',
         'ko' => '한국어',
+        'lt' => 'Lietuvių Kalba',
         'lv' => 'Latviešu Valoda',
         'nl' => 'Nederlands',
         'nb' => 'Norsk (Bokmål)',
index d82e3263274961cdce7bfd5aac33e5d7644c935d..3d24cec3ca7ac9c625d3072e43646059b1e9051a 100644 (file)
@@ -15,6 +15,7 @@ return [
     'alpha_dash'           => ':attribute csak betűket, számokat és kötőjeleket tartalmazhat.',
     'alpha_num'            => ':attribute csak betűket és számokat tartalmazhat.',
     'array'                => ':attribute tömb kell legyen.',
+    'backup_codes'         => 'The provided code is not valid or has already been used.',
     'before'               => ':attribute dátumnak :date előttinek kell lennie.',
     'between'              => [
         'numeric' => ':attribute értékének :min és :max között kell lennie.',
@@ -98,6 +99,7 @@ return [
     ],
     'string'               => ':attribute karaktersorozatnak kell legyen.',
     'timezone'             => ':attribute érvényes zóna kell legyen.',
+    'totp'                 => 'The provided code is not valid or has expired.',
     'unique'               => ':attribute már elkészült.',
     'url'                  => ':attribute formátuma érvénytelen.',
     'uploaded'             => 'A fájlt nem lehet feltölteni. A kiszolgáló nem fogad el ilyen méretű fájlokat.',
index 1255e32dcf9c336687d24c8e0dca1a1c2a369ab4..bac965be433d4ef702001aedd7a14328dee9f257 100644 (file)
@@ -8,8 +8,8 @@ return [
     // Pages
     'page_create'                 => 'telah membuat halaman',
     'page_create_notification'    => 'Halaman Berhasil dibuat',
-    'page_update'                 => 'halaman diperbaharui',
-    'page_update_notification'    => 'Halaman Berhasil Diperbarui',
+    'page_update'                 => 'halaman telah diperbaharui',
+    'page_update_notification'    => 'Berhasil mengupdate halaman',
     'page_delete'                 => 'halaman dihapus',
     'page_delete_notification'    => 'Berhasil menghapus halaman',
     'page_restore'                => 'halaman telah dipulihkan',
@@ -47,6 +47,10 @@ return [
     'favourite_add_notification' => '":name" telah ditambahkan ke favorit Anda',
     'favourite_remove_notification' => '":name" telah dihapus dari favorit Anda',
 
+    // MFA
+    'mfa_setup_method_notification' => 'Multi-factor method successfully configured',
+    'mfa_remove_method_notification' => 'Multi-factor method successfully removed',
+
     // Other
     'commented_on'                => 'berkomentar pada',
     'permissions_update'          => 'izin diperbarui',
index 0fc965dae3b2bab2770290b71813bf695f88005d..d800ebbcc2c2c7c170a5aba3b1c73affc280370c 100644 (file)
@@ -73,5 +73,40 @@ return [
     'user_invite_page_welcome' => 'Selamat datang di :appName!',
     'user_invite_page_text' => 'Untuk menyelesaikan akun Anda dan mendapatkan akses, Anda perlu mengatur kata sandi yang akan digunakan untuk masuk ke :appName pada kunjungan berikutnya.',
     'user_invite_page_confirm_button' => 'Konfirmasi Kata sandi',
-    'user_invite_success' => 'Atur kata sandi, Anda sekarang memiliki akses ke :appName!'
+    'user_invite_success' => 'Atur kata sandi, Anda sekarang memiliki akses ke :appName!',
+
+    // Multi-factor Authentication
+    'mfa_setup' => 'Setup Multi-Factor Authentication',
+    'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
+    'mfa_setup_configured' => 'Already configured',
+    'mfa_setup_reconfigure' => 'Reconfigure',
+    'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?',
+    'mfa_setup_action' => 'Setup',
+    'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.',
+    'mfa_option_totp_title' => 'Mobile App',
+    'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
+    'mfa_option_backup_codes_title' => 'Backup Codes',
+    'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.',
+    'mfa_gen_confirm_and_enable' => 'Confirm and Enable',
+    'mfa_gen_backup_codes_title' => 'Backup Codes Setup',
+    'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.',
+    'mfa_gen_backup_codes_download' => 'Download Codes',
+    'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once',
+    'mfa_gen_totp_title' => 'Mobile App Setup',
+    'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
+    'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.',
+    'mfa_gen_totp_verify_setup' => 'Verify Setup',
+    'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:',
+    'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here',
+    'mfa_verify_access' => 'Verify Access',
+    'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.',
+    'mfa_verify_no_methods' => 'No Methods Configured',
+    'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.',
+    'mfa_verify_use_totp' => 'Verify using a mobile app',
+    'mfa_verify_use_backup_codes' => 'Verify using a backup code',
+    'mfa_verify_backup_code' => 'Backup Code',
+    'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:',
+    'mfa_verify_backup_code_enter_here' => 'Enter backup code here',
+    'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:',
+    'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.',
 ];
\ No newline at end of file
index 893ff76032b998cb56b62edb7fe6b3a68156f8dd..c9aac2ed2d4012f38dc32e124e22cfc94c584ace 100644 (file)
@@ -5,89 +5,91 @@
 return [
 
     // Buttons
-    'cancel' => 'Batalkan',
+    'cancel' => 'Batal',
     'confirm' => 'Konfirmasi',
     'back' => 'Kembali',
     'save' => 'Simpan',
-    'continue' => 'Selanjutnya',
+    'continue' => 'Lanjutkan',
     'select' => 'Pilih',
-    'toggle_all' => 'Alihkan semua',
-    'more' => 'Lebih',
+    'toggle_all' => 'Alihkan Semua',
+    'more' => 'Lebih banyak',
 
     // Form Labels
     'name' => 'Nama',
     'description' => 'Deskripsi',
-    'role' => 'Wewenang',
-    'cover_image' => 'Sampul Gambar',
+    'role' => 'Peran',
+    'cover_image' => 'Sampul gambar',
     'cover_image_description' => 'Gambar ini harus berukuran kira-kira 440x250 piksel.',
     
     // Actions
     'actions' => 'Tindakan',
-    'view' => 'Melihat',
+    'view' => 'Lihat',
     'view_all' => 'Lihat Semua',
     'create' => 'Buat',
-    'update' => 'Perbaharui',
+    'update' => 'Perbarui',
     'edit' => 'Sunting',
     'sort' => 'Sortir',
     'move' => 'Pindahkan',
     'copy' => 'Salin',
-    'reply' => 'Balasan',
+    'reply' => 'Balas',
     'delete' => 'Hapus',
     'delete_confirm' => 'Konfirmasi Penghapusan',
     'search' => 'Cari',
     'search_clear' => 'Hapus Pencarian',
-    'reset' => 'Setel Ulang',
+    'reset' => 'Atur ulang',
     'remove' => 'Hapus',
     'add' => 'Tambah',
+    'configure' => 'Configure',
     'fullscreen' => 'Layar Penuh',
     'favourite' => 'Favorit',
-    'unfavourite' => 'Tidak favorit',
-    'next' => 'Lanjut',
+    'unfavourite' => 'Batal favorit',
+    'next' => 'Selanjutnya',
     'previous' => 'Sebelumnya',
 
     // Sort Options
-    'sort_options' => 'Sortir Pilihan',
-    'sort_direction_toggle' => 'Urutkan Arah Toggle',
-    'sort_ascending' => 'Sortir Naik',
-    'sort_descending' => 'Urutkan Menurun',
+    'sort_options' => 'Opsi Sortir',
+    'sort_direction_toggle' => 'Urutkan Arah Alihan',
+    'sort_ascending' => 'Sortir Menaik',
+    'sort_descending' => 'Sortir Menurun',
     'sort_name' => 'Nama',
     'sort_default' => 'Bawaan',
-    'sort_created_at' => 'Tanggal dibuat',
-    'sort_updated_at' => 'Tanggal diperbaharui',
+    'sort_created_at' => 'Tanggal Dibuat',
+    'sort_updated_at' => 'Tanggal Diperbarui',
 
     // Misc
-    'deleted_user' => 'Pengguna terhapus',
-    'no_activity' => 'Tidak ada aktifitas untuk ditampilkan',
+    'deleted_user' => 'Pengguna yang Dihapus',
+    'no_activity' => 'Tidak ada aktivitas untuk ditampilkan',
     'no_items' => 'Tidak ada item yang tersedia',
     'back_to_top' => 'Kembali ke atas',
-    'toggle_details' => 'Detail Toggle',
+    'skip_to_main_content' => 'Lewatkan ke konten utama',
+    'toggle_details' => 'Rincian Alihan',
     'toggle_thumbnails' => 'Alihkan Gambar Mini',
-    'details' => 'Detail',
-    'grid_view' => 'Tampilan bergaris',
-    'list_view' => 'Daftar Tampilan',
-    'default' => 'Default',
+    'details' => 'Rincian',
+    'grid_view' => 'Tampilan Bergaris',
+    'list_view' => 'Tampilan Daftar',
+    'default' => 'Bawaan',
     'breadcrumb' => 'Breadcrumb',
 
     // Header
     'header_menu_expand' => 'Perluas Menu Tajuk',
-    'profile_menu' => 'Profile Menu',
-    'view_profile' => 'Tampilkan profil',
+    'profile_menu' => 'Menu Profil',
+    'view_profile' => 'Tampilkan Profil',
     'edit_profile' => 'Sunting Profil',
     'dark_mode' => 'Mode Gelap',
-    'light_mode' => 'Mode Cahaya',
+    'light_mode' => 'Mode Terang',
 
     // Layout tabs
     'tab_info' => 'Informasi',
-    'tab_info_label' => 'Tab Menampilkan Informasi Sekunder',
+    'tab_info_label' => 'Tab: Tampilkan Informasi Sekunder',
     'tab_content' => 'Konten',
-    'tab_content_label' => 'Tab Menampilkan Informasi Utama',
+    'tab_content_label' => 'Tab: Tampilkan Informasi Utama',
 
     // Email Content
-    'email_action_help' => 'Jika Anda mengalami masalah saat mengklik tombol ":actionText", salin dan tempel URL di bawah ini ke browser web Anda:',
-    'email_rights' => 'Seluruh hak cipta',
+    'email_action_help' => 'Jika Anda mengalami masalah saat mengklik tombol ":actionText", salin dan tempel URL di bawah ke dalam peramban web Anda:',
+    'email_rights' => 'Hak cipta dilindungi',
 
     // Footer Link Options
     // Not directly used but available for convenience to users.
-    'privacy_policy' => 'Rahasia pribadi',
-    'terms_of_service' => 'Persyaratan Layanan',
+    'privacy_policy' => 'Kebijakan Privasi',
+    'terms_of_service' => 'Ketentuan Layanan',
 ];
index 5f6afb807a34dc4c4ef39bf27b9d3ae8a33c4c57..aa92fd06f1c60d259fe41dc75962cb2b05449a92 100644 (file)
@@ -36,6 +36,7 @@ return [
     'export_html' => 'File Web Berisi',
     'export_pdf' => 'Dokumen PDF',
     'export_text' => 'Dokumen Teks Biasa',
+    'export_md' => 'File Markdown',
 
     // Permissions and restrictions
     'permissions' => 'Izin',
@@ -98,6 +99,7 @@ return [
     'shelves_permissions' => 'Izin Rak Buku',
     'shelves_permissions_updated' => 'Izin Rak Buku Diperbarui',
     'shelves_permissions_active' => 'Izin Rak Buku Aktif',
+    'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',
     'shelves_copy_permissions_to_books' => 'Salin Izin ke Buku',
     'shelves_copy_permissions' => 'Salin Izin',
     'shelves_copy_permissions_explain' => 'Ini akan menerapkan setelan izin rak buku ini saat ini ke semua buku yang ada di dalamnya. Sebelum mengaktifkan, pastikan setiap perubahan pada izin rak buku ini telah disimpan.',
index 80e8d38e15776f7df9c1f89f68472ac909d05949..c01cbdb016d369eded8c9f4847f6131e0d6f8785 100644 (file)
@@ -92,6 +92,7 @@ return [
     'recycle_bin' => 'Tempat Sampah',
     'recycle_bin_desc' => 'Di sini Anda dapat memulihkan item yang telah dihapus atau memilih untuk menghapusnya secara permanen dari sistem. Daftar ini tidak difilter, tidak seperti daftar aktivitas serupa di sistem tempat filter izin diterapkan.',
     'recycle_bin_deleted_item' => 'Item yang Dihapus',
+    'recycle_bin_deleted_parent' => 'Induk',
     'recycle_bin_deleted_by' => 'Dihapus Oleh',
     'recycle_bin_deleted_at' => 'Waktu Penghapusan',
     'recycle_bin_permanently_delete' => 'Hapus Permanen',
@@ -104,6 +105,7 @@ return [
     'recycle_bin_restore_list' => 'Item yang akan Dipulihkan',
     'recycle_bin_restore_confirm' => 'Tindakan ini akan memulihkan item yang dihapus, termasuk semua elemen anak, ke lokasi aslinya. Jika lokasi asli telah dihapus, dan sekarang berada di keranjang sampah, item induk juga perlu dipulihkan.',
     'recycle_bin_restore_deleted_parent' => 'Induk item ini juga telah dihapus. Ini akan tetap dihapus sampai induknya juga dipulihkan.',
+    'recycle_bin_restore_parent' => 'Pulihkan Induk',
     'recycle_bin_destroy_notification' => 'Total :count item dari tempat sampah.',
     'recycle_bin_restore_notification' => 'Total :count item yang dipulihkan dari tempat sampah.',
 
@@ -117,6 +119,7 @@ return [
     'audit_table_user' => 'Pengguna',
     'audit_table_event' => 'Peristiwa',
     'audit_table_related' => 'Item atau Detail Terkait',
+    'audit_table_ip' => 'IP Address',
     'audit_table_date' => 'Tanggal Kegiatan',
     'audit_date_from' => 'Rentang Tanggal Dari',
     'audit_date_to' => 'Rentang Tanggal Sampai',
@@ -136,6 +139,7 @@ return [
     'role_details' => 'Detail Peran',
     'role_name' => 'Nama peran',
     'role_desc' => 'Deskripsi Singkat Peran',
+    'role_mfa_enforced' => 'Requires Multi-Factor Authentication',
     'role_external_auth_id' => 'Otentikasi Eksternal IDs',
     'role_system' => 'Izin Sistem',
     'role_manage_users' => 'Kelola pengguna',
@@ -145,6 +149,7 @@ return [
     'role_manage_page_templates' => 'Kelola template halaman',
     'role_access_api' => 'Akses Sistem API',
     'role_manage_settings' => 'Kelola setelan aplikasi',
+    'role_export_content' => 'Export content',
     'role_asset' => 'Izin Aset',
     'roles_system_warning' => 'Ketahuilah bahwa akses ke salah satu dari tiga izin di atas dapat memungkinkan pengguna untuk mengubah hak mereka sendiri atau orang lain dalam sistem. Hanya tetapkan peran dengan izin ini untuk pengguna tepercaya.',
     'role_asset_desc' => 'Izin ini mengontrol akses default ke aset dalam sistem. Izin pada Buku, Bab, dan Halaman akan menggantikan izin ini.',
@@ -202,6 +207,10 @@ return [
     'users_api_tokens_create' => 'Buat Token',
     'users_api_tokens_expires' => 'Kedaluwarsa',
     'users_api_tokens_docs' => 'Dokumentasi API',
+    'users_mfa' => 'Multi-Factor Authentication',
+    'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
+    'users_mfa_x_methods' => ':count method configured|:count methods configured',
+    'users_mfa_configure' => 'Configure Methods',
 
     // API Tokens
     'user_api_token_create' => 'Buat Token API',
@@ -247,6 +256,7 @@ return [
         'it' => 'Italian',
         'ja' => '日本語',
         'ko' => '한국어',
+        'lt' => 'Lietuvių Kalba',
         'lv' => 'Latviešu Valoda',
         'nl' => 'Nederlands',
         'nb' => 'Norsk (Bokmål)',
index 2bd5cf7331a3373ae2ed3bdf88268961130c6038..992f403299a3ea53884071022e8e6bf8cc6f43bd 100644 (file)
@@ -15,6 +15,7 @@ return [
     'alpha_dash'           => ':attribute hanya boleh berisi huruf, angka, tanda hubung, dan garis bawah.',
     'alpha_num'            => ':attribute hanya boleh berisi huruf dan angka.',
     'array'                => ':attribute harus berupa larik.',
+    'backup_codes'         => 'The provided code is not valid or has already been used.',
     'before'               => ':attribute harus tanggal sebelum :date.',
     'between'              => [
         'numeric' => ':attribute harus di antara :min dan :max.',
@@ -98,6 +99,7 @@ return [
     ],
     'string'               => ':attribute harus berupa string.',
     'timezone'             => ':attribute harus menjadi zona yang valid.',
+    'totp'                 => 'The provided code is not valid or has expired.',
     'unique'               => ':attribute sudah diambil.',
     'url'                  => ':attribute format tidak valid.',
     'uploaded'             => 'Berkas tidak dapat diunggah. Server mungkin tidak menerima berkas dengan ukuran ini.',
index 11c52b696f403c27d8fbbbf63234be75705f49aa..96852f9220f9e6d26150abb65f6ec3b6bbf6df72 100755 (executable)
@@ -47,6 +47,10 @@ return [
     'favourite_add_notification' => '":name" è stato aggiunto ai tuoi preferiti',
     'favourite_remove_notification' => '":name" è stato rimosso dai tuoi preferiti',
 
+    // MFA
+    'mfa_setup_method_notification' => 'Metodo multi-fattore impostato con successo',
+    'mfa_remove_method_notification' => 'Metodo multi-fattore rimosso con successo',
+
     // Other
     'commented_on'                => 'ha commentato in',
     'permissions_update'          => 'autorizzazioni aggiornate',
index a1c4b704831450ac3f61ce34cf13674e1c5b723f..3e1500a6ff3b0b2cf7eb243cb61a7fa48959f292 100755 (executable)
@@ -73,5 +73,40 @@ return [
     'user_invite_page_welcome' => 'Benvenuto in :appName!',
     'user_invite_page_text' => 'Per completare il tuo account e ottenere l\'accesso devi impostare una password che verrà utilizzata per accedere a :appName in futuro.',
     'user_invite_page_confirm_button' => 'Conferma Password',
-    'user_invite_success' => 'Password impostata, ora hai accesso a :appName!'
+    'user_invite_success' => 'Password impostata, ora hai accesso a :appName!',
+
+    // Multi-factor Authentication
+    'mfa_setup' => 'Imposta Autenticazione Multi-Fattore',
+    'mfa_setup_desc' => 'Imposta l\'autenticazione multi-fattore come misura di sicurezza aggiuntiva per il tuo account.',
+    'mfa_setup_configured' => 'Già configurata',
+    'mfa_setup_reconfigure' => 'Riconfigura',
+    'mfa_setup_remove_confirmation' => 'Sei sicuro di voler rimuovere questo metodo di autenticazione multi-fattore?',
+    'mfa_setup_action' => 'Imposta',
+    'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.',
+    'mfa_option_totp_title' => 'Mobile App',
+    'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
+    'mfa_option_backup_codes_title' => 'Backup Codes',
+    'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.',
+    'mfa_gen_confirm_and_enable' => 'Confirm and Enable',
+    'mfa_gen_backup_codes_title' => 'Backup Codes Setup',
+    'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.',
+    'mfa_gen_backup_codes_download' => 'Download Codes',
+    'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once',
+    'mfa_gen_totp_title' => 'Mobile App Setup',
+    'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
+    'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.',
+    'mfa_gen_totp_verify_setup' => 'Verify Setup',
+    'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:',
+    'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here',
+    'mfa_verify_access' => 'Verify Access',
+    'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.',
+    'mfa_verify_no_methods' => 'No Methods Configured',
+    'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.',
+    'mfa_verify_use_totp' => 'Verify using a mobile app',
+    'mfa_verify_use_backup_codes' => 'Verify using a backup code',
+    'mfa_verify_backup_code' => 'Backup Code',
+    'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:',
+    'mfa_verify_backup_code_enter_here' => 'Enter backup code here',
+    'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:',
+    'mfa_setup_login_notification' => 'Metodo multi-fattore configurato, si prega di effettuare nuovamente il login utilizzando il metodo configurato.',
 ];
\ No newline at end of file
index f33aa4cd3b16a614b0346999e124c61603a5d1a4..bcd3aadf92ad6f251e3b1923af9fd9f9f62afedd 100755 (executable)
@@ -39,6 +39,7 @@ return [
     'reset' => 'Azzera',
     'remove' => 'Rimuovi',
     'add' => 'Aggiungi',
+    'configure' => 'Configura',
     'fullscreen' => 'Schermo intero',
     'favourite' => 'Aggiungi ai Preferiti',
     'unfavourite' => 'Rimuovi dai preferiti',
@@ -60,6 +61,7 @@ return [
     'no_activity' => 'Nessuna attività da mostrare',
     'no_items' => 'Nessun elemento disponibile',
     'back_to_top' => 'Torna in alto',
+    'skip_to_main_content' => 'Passa al contenuto principale',
     'toggle_details' => 'Mostra Dettagli',
     'toggle_thumbnails' => 'Mostra Miniature',
     'details' => 'Dettagli',
index 9d5e8c2ab20741363d3420ec6a2b54ff43031414..2088ed1ff4ce0244fbb53a6bc20a22eaf756ca39 100755 (executable)
@@ -36,6 +36,7 @@ return [
     'export_html' => 'File Contenuto Web',
     'export_pdf' => 'File PDF',
     'export_text' => 'File di testo',
+    'export_md' => 'File Markdown',
 
     // Permissions and restrictions
     'permissions' => 'Permessi',
@@ -98,6 +99,7 @@ return [
     'shelves_permissions' => 'Permessi Libreria',
     'shelves_permissions_updated' => 'Permessi Libreria Aggiornati',
     'shelves_permissions_active' => 'Permessi Attivi Libreria',
+    'shelves_permissions_cascade_warning' => 'I permessi sugli scaffali non si estendono automaticamente ai libri contenuti. Questo avviene in quanto un libro può essere presente su più scaffali. I permessi possono comunque essere copiati ai libri contenuti usando l\'opzione qui sotto.',
     'shelves_copy_permissions_to_books' => 'Copia Permessi ai Libri',
     'shelves_copy_permissions' => 'Copia Permessi',
     'shelves_copy_permissions_explain' => 'Verranno applicati tutti i permessi della libreria ai libri contenuti. Prima di attivarlo, assicurati che ogni permesso di questa libreria sia salvato.',
index 6aaca5a23206883039dc874bc1176ad503ac02e1..c5e016b35c835af8b8bfa065af89e1a501c4ec47 100755 (executable)
@@ -92,6 +92,7 @@ return [
     'recycle_bin' => 'Cestino',
     'recycle_bin_desc' => 'Qui è possibile ripristinare gli elementi che sono stati eliminati o scegliere di rimuoverli definitivamente dal sistema. Questo elenco non è filtrato a differenza di elenchi di attività simili nel sistema in cui vengono applicati i filtri autorizzazioni.',
     'recycle_bin_deleted_item' => 'Elimina Elemento',
+    'recycle_bin_deleted_parent' => 'Superiore',
     'recycle_bin_deleted_by' => 'Cancellato da',
     'recycle_bin_deleted_at' => 'Orario Cancellazione',
     'recycle_bin_permanently_delete' => 'Elimina Definitivamente',
@@ -104,6 +105,7 @@ return [
     'recycle_bin_restore_list' => 'Elementi da Ripristinare',
     'recycle_bin_restore_confirm' => 'Questa azione ripristinerà l\'elemento eliminato, compresi gli elementi figli, nella loro posizione originale. Se la posizione originale è stata eliminata, ed è ora nel cestino, anche l\'elemento padre dovrà essere ripristinato.',
     'recycle_bin_restore_deleted_parent' => 'L\'elemento padre di questo elemento è stato eliminato. Questo elemento rimarrà eliminato fino a che l\'elemento padre non sarà ripristinato.',
+    'recycle_bin_restore_parent' => 'Ripristina Superiore',
     'recycle_bin_destroy_notification' => 'Eliminati :count elementi dal cestino.',
     'recycle_bin_restore_notification' => 'Ripristinati :count elementi dal cestino.',
 
@@ -117,6 +119,7 @@ return [
     'audit_table_user' => 'Utente',
     'audit_table_event' => 'Evento',
     'audit_table_related' => 'Elemento o Dettaglio correlato',
+    'audit_table_ip' => 'Indirizzo IP',
     'audit_table_date' => 'Data attività',
     'audit_date_from' => 'Dalla data',
     'audit_date_to' => 'Alla data',
@@ -136,6 +139,7 @@ return [
     'role_details' => 'Dettagli Ruolo',
     'role_name' => 'Nome Ruolo',
     'role_desc' => 'Breve Descrizione del Ruolo',
+    'role_mfa_enforced' => 'Richiesta autenticazione multi-fattore',
     'role_external_auth_id' => 'ID Autenticazione Esterna',
     'role_system' => 'Permessi di Sistema',
     'role_manage_users' => 'Gestire gli utenti',
@@ -145,6 +149,7 @@ return [
     'role_manage_page_templates' => 'Gestisci template pagine',
     'role_access_api' => 'API sistema d\'accesso',
     'role_manage_settings' => 'Gestire impostazioni app',
+    'role_export_content' => 'Esporta contenuto',
     'role_asset' => 'Permessi Entità',
     'roles_system_warning' => 'Siate consapevoli che l\'accesso a uno dei tre permessi qui sopra, può consentire a un utente di modificare i propri privilegi o i privilegi di altri nel sistema. Assegna ruoli con questi permessi solo ad utenti fidati.',
     'role_asset_desc' => 'Questi permessi controllano l\'accesso di default alle entità. I permessi nei Libri, Capitoli e Pagine sovrascriveranno questi.',
@@ -173,7 +178,7 @@ return [
     'users_send_invite_text' => 'Puoi scegliere di inviare a questo utente un\'email di invito che permette loro di impostare la propria password altrimenti puoi impostare la password tu stesso.',
     'users_send_invite_option' => 'Invia email di invito',
     'users_external_auth_id' => 'ID Autenticazioni Esterna',
-    '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' => 'Questo è l\'ID usato per abbinare questo utente quando si comunica con il sistema di autenticazione esterno.',
     'users_password_warning' => 'Riempi solo se desideri cambiare la tua password:',
     'users_system_public' => 'Questo utente rappresente qualsiasi ospite che visita il sito. Non può essere usato per effettuare il login ma è assegnato automaticamente.',
     'users_delete' => 'Elimina Utente',
@@ -198,25 +203,29 @@ return [
     'users_social_connected' => 'L\'account :socialAccount è stato connesso correttamente al tuo profilo.',
     'users_social_disconnected' => 'L\'account :socialAccount è stato disconnesso correttamente dal tuo profilo.',
     'users_api_tokens' => 'Token API',
-    'users_api_tokens_none' => 'No API tokens have been created for this user',
+    'users_api_tokens_none' => 'Nessun token API è stato creato per questo utente',
     'users_api_tokens_create' => 'Crea Token',
     'users_api_tokens_expires' => 'Scade',
     'users_api_tokens_docs' => 'Documentazione API',
+    'users_mfa' => 'Autenticazione multi-fattore',
+    'users_mfa_desc' => 'Imposta l\'autenticazione multi-fattore come misura di sicurezza aggiuntiva per il tuo account.',
+    'users_mfa_x_methods' => ':count metodo configurato|:count metodi configurati',
+    'users_mfa_configure' => 'Configura metodi',
 
     // API Tokens
     'user_api_token_create' => 'Crea Token API',
     'user_api_token_name' => 'Nome',
-    'user_api_token_name_desc' => 'Give your token a readable name as a future reminder of its intended purpose.',
+    'user_api_token_name_desc' => 'Assegna al tuo token un nome leggibile per ricordarne la funzionalità in futuro.',
     'user_api_token_expiry' => 'Data di scadenza',
-    '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_expiry_desc' => 'Imposta una data di scadenza per questo token. Dopo questa data, le richieste che utilizzeranno questo token non funzioneranno più. Lasciando questo campo vuoto si imposterà la scadenza tra 100 anni.',
+    'user_api_token_create_secret_message' => 'Immediatamente dopo aver creato questo token, un "Token ID" e un "Segreto Token" saranno generati e mostrati. Il segreto verrà mostrato unicamente questa volta, assicurati, quindi, di copiare il valore in un posto sicuro prima di procedere.',
     'user_api_token_create_success' => 'Token API creato correttamente',
     'user_api_token_update_success' => 'Token API aggiornato correttamente',
     'user_api_token' => 'Token API',
     '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_id_desc' => 'Questo è un identificativo non modificabile generato dal sistema per questo token e che sarà necessario fornire per le richieste tramite API.',
     'user_api_token_secret' => 'Token Segreto',
-    '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' => 'Questo è un segreto generato dal sistema per questo token che sarà necessario fornire per le richieste via API. Questo valore sarà visibile unicamente in questo momento pertanto copialo in un posto sicuro.',
     'user_api_token_created' => 'Token Aggiornato :timeAgo',
     'user_api_token_updated' => 'Token Aggiornato :timeAgo',
     'user_api_token_delete' => 'Elimina Token',
@@ -247,6 +256,7 @@ return [
         'it' => 'Italian',
         'ja' => '日本語',
         'ko' => '한국어',
+        'lt' => 'Lietuvių Kalba',
         'lv' => 'Latviešu Valoda',
         'nl' => 'Nederlands',
         'nb' => 'Norsk (Bokmål)',
index 69d023688ea0357270b3ba007e8091f53ca47005..62f0701198d9aebb52ee1687d24ab4b885ec5946 100755 (executable)
@@ -15,6 +15,7 @@ return [
     'alpha_dash'           => ':attribute deve contenere solo lettere, numeri e meno.',
     'alpha_num'            => ':attribute deve contenere solo lettere e numeri.',
     'array'                => ':attribute deve essere un array.',
+    'backup_codes'         => 'Il codice fornito non è valido o è già stato utilizzato.',
     'before'               => ':attribute deve essere una data prima del :date.',
     'between'              => [
         'numeric' => 'Il campo :attribute deve essere tra :min e :max.',
@@ -98,6 +99,7 @@ return [
     ],
     'string'               => ':attribute deve essere una stringa.',
     'timezone'             => ':attribute deve essere una zona valida.',
+    'totp'                 => 'Il codice fornito non è valido o è scaduto.',
     'unique'               => ':attribute è già preso.',
     'url'                  => 'Il formato :attribute non è valido.',
     'uploaded'             => 'Il file non può essere caricato. Il server potrebbe non accettare file di questa dimensione.',
index fd119a30472feb3db4db04431d4ab9929cf64b78..3dc749b6746d3621dcf612292e933fb124a0fc0e 100644 (file)
@@ -47,6 +47,10 @@ return [
     'favourite_add_notification' => '":name" has been added to your favourites',
     'favourite_remove_notification' => '":name" has been removed from your favourites',
 
+    // MFA
+    'mfa_setup_method_notification' => 'Multi-factor method successfully configured',
+    'mfa_remove_method_notification' => 'Multi-factor method successfully removed',
+
     // Other
     'commented_on'                => 'コメントする',
     'permissions_update'          => 'updated permissions',
index 6163a5fc1f739b171b332661d7112fb282a13d4e..8dcac7aa4ed8a81a4fd582cd16003f9977b3d2b9 100644 (file)
@@ -73,5 +73,40 @@ return [
     '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_success' => 'Password set, you now have access to :appName!',
+
+    // Multi-factor Authentication
+    'mfa_setup' => 'Setup Multi-Factor Authentication',
+    'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
+    'mfa_setup_configured' => 'Already configured',
+    'mfa_setup_reconfigure' => 'Reconfigure',
+    'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?',
+    'mfa_setup_action' => 'Setup',
+    'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.',
+    'mfa_option_totp_title' => 'Mobile App',
+    'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
+    'mfa_option_backup_codes_title' => 'Backup Codes',
+    'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.',
+    'mfa_gen_confirm_and_enable' => 'Confirm and Enable',
+    'mfa_gen_backup_codes_title' => 'Backup Codes Setup',
+    'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.',
+    'mfa_gen_backup_codes_download' => 'Download Codes',
+    'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once',
+    'mfa_gen_totp_title' => 'Mobile App Setup',
+    'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
+    'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.',
+    'mfa_gen_totp_verify_setup' => 'Verify Setup',
+    'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:',
+    'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here',
+    'mfa_verify_access' => 'Verify Access',
+    'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.',
+    'mfa_verify_no_methods' => 'No Methods Configured',
+    'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.',
+    'mfa_verify_use_totp' => 'Verify using a mobile app',
+    'mfa_verify_use_backup_codes' => 'Verify using a backup code',
+    'mfa_verify_backup_code' => 'Backup Code',
+    'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:',
+    'mfa_verify_backup_code_enter_here' => 'Enter backup code here',
+    'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:',
+    'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.',
 ];
\ No newline at end of file
index e52da90a176ec332d46374a94c0184597c0a29dc..a5f2f942984580cc946a390e927bd45b9a215fea 100644 (file)
@@ -39,6 +39,7 @@ return [
     'reset' => 'リセット',
     'remove' => '削除',
     'add' => '追加',
+    'configure' => 'Configure',
     'fullscreen' => 'Fullscreen',
     'favourite' => 'Favourite',
     'unfavourite' => 'Unfavourite',
@@ -60,6 +61,7 @@ return [
     'no_activity' => '表示するアクティビティがありません',
     'no_items' => 'アイテムはありません',
     'back_to_top' => '上に戻る',
+    'skip_to_main_content' => 'Skip to main content',
     'toggle_details' => '概要の表示切替',
     'toggle_thumbnails' => 'Toggle Thumbnails',
     'details' => '詳細',
index 6515305e478608c0b3a04bf220f574cb9d411abc..1c9dafc438ae1e0a76925a446bc0d02327801c20 100644 (file)
@@ -36,6 +36,7 @@ return [
     'export_html' => 'Webページ',
     'export_pdf' => 'PDF',
     'export_text' => 'テキストファイル',
+    'export_md' => 'Markdown File',
 
     // Permissions and restrictions
     'permissions' => '権限',
@@ -98,6 +99,7 @@ return [
     'shelves_permissions' => 'Bookshelf Permissions',
     'shelves_permissions_updated' => 'Bookshelf Permissions Updated',
     'shelves_permissions_active' => 'Bookshelf Permissions Active',
+    'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',
     '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.',
index b972c9b61a4151178962c9b8e0791d08534acfed..c769174e7be40e5de2b5e733639c78cad7afa795 100644 (file)
@@ -92,6 +92,7 @@ return [
     '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_parent' => 'Parent',
     'recycle_bin_deleted_by' => 'Deleted By',
     'recycle_bin_deleted_at' => 'Deletion Time',
     'recycle_bin_permanently_delete' => 'Permanently Delete',
@@ -104,6 +105,7 @@ return [
     '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_restore_parent' => 'Restore Parent',
     'recycle_bin_destroy_notification' => 'Deleted :count total items from the recycle bin.',
     'recycle_bin_restore_notification' => 'Restored :count total items from the recycle bin.',
 
@@ -117,6 +119,7 @@ return [
     'audit_table_user' => 'User',
     'audit_table_event' => 'Event',
     'audit_table_related' => 'Related Item or Detail',
+    'audit_table_ip' => 'IP Address',
     'audit_table_date' => 'Activity Date',
     'audit_date_from' => 'Date Range From',
     'audit_date_to' => 'Date Range To',
@@ -136,6 +139,7 @@ return [
     'role_details' => '概要',
     'role_name' => '役割名',
     'role_desc' => '役割の説明',
+    'role_mfa_enforced' => 'Requires Multi-Factor Authentication',
     'role_external_auth_id' => 'External Authentication IDs',
     'role_system' => 'システム権限',
     'role_manage_users' => 'ユーザ管理',
@@ -145,6 +149,7 @@ return [
     'role_manage_page_templates' => 'Manage page templates',
     'role_access_api' => 'Access system API',
     'role_manage_settings' => 'アプリケーション設定の管理',
+    'role_export_content' => 'Export content',
     '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.',
     'role_asset_desc' => '各アセットに対するデフォルトの権限を設定します。ここで設定した権限が優先されます。',
@@ -202,6 +207,10 @@ return [
     'users_api_tokens_create' => 'Create Token',
     'users_api_tokens_expires' => 'Expires',
     'users_api_tokens_docs' => 'API Documentation',
+    'users_mfa' => 'Multi-Factor Authentication',
+    'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
+    'users_mfa_x_methods' => ':count method configured|:count methods configured',
+    'users_mfa_configure' => 'Configure Methods',
 
     // API Tokens
     'user_api_token_create' => 'Create API Token',
@@ -247,6 +256,7 @@ return [
         'it' => 'Italian',
         'ja' => '日本語',
         'ko' => '한국어',
+        'lt' => 'Lietuvių Kalba',
         'lv' => 'Latviešu Valoda',
         'nl' => 'Nederlands',
         'nb' => 'Norsk (Bokmål)',
index 7d9987ce073e22a0137fdef9a216cf9f4cc84d5c..2ef8b011912bd8c87cdb2d026d3ebd0cd2914234 100644 (file)
@@ -15,6 +15,7 @@ return [
     'alpha_dash'           => ':attributeは文字, 数値, ハイフンのみが含められます。',
     'alpha_num'            => ':attributeは文字と数値のみが含められます。',
     'array'                => ':attributeは配列である必要があります。',
+    'backup_codes'         => 'The provided code is not valid or has already been used.',
     'before'               => ':attributeは:date以前である必要があります。',
     'between'              => [
         'numeric' => ':attributeは:min〜:maxである必要があります。',
@@ -98,6 +99,7 @@ return [
     ],
     'string'               => ':attributeは文字列である必要があります。',
     'timezone'             => ':attributeは正しいタイムゾーンである必要があります。',
+    'totp'                 => 'The provided code is not valid or has expired.',
     'unique'               => ':attributeは既に使用されています。',
     'url'                  => ':attributeのフォーマットは不正です。',
     'uploaded'             => 'The file could not be uploaded. The server may not accept files of this size.',
index ed0b66112c6f0fbe6763e2a6458ed8fcdb56db99..ee694f0736b351cf06c03f5c5df06fe0c7c9b2f8 100644 (file)
@@ -47,6 +47,10 @@ return [
     'favourite_add_notification' => '":name" has been added to your favourites',
     'favourite_remove_notification' => '":name" has been removed from your favourites',
 
+    // MFA
+    'mfa_setup_method_notification' => 'Multi-factor method successfully configured',
+    'mfa_remove_method_notification' => 'Multi-factor method successfully removed',
+
     // Other
     'commented_on'                => '댓글 쓰기',
     'permissions_update'          => 'updated permissions',
index 0bff8724c955ed3f840e29c17cb02108e0d3bd31..ce65f3ecc86bd369ef485000366242fea8fef582 100644 (file)
@@ -73,5 +73,40 @@ return [
     'user_invite_page_welcome' => ':appName에 오신 것을 환영합니다!',
     'user_invite_page_text' => ':appName에 로그인할 때 입력할 비밀번호를 설정하세요.',
     'user_invite_page_confirm_button' => '비밀번호 확인',
-    'user_invite_success' => '암호가 설정되었고, 이제 :appName에 접근할 수 있습니다.'
+    'user_invite_success' => '암호가 설정되었고, 이제 :appName에 접근할 수 있습니다.',
+
+    // Multi-factor Authentication
+    'mfa_setup' => 'Setup Multi-Factor Authentication',
+    'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
+    'mfa_setup_configured' => 'Already configured',
+    'mfa_setup_reconfigure' => 'Reconfigure',
+    'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?',
+    'mfa_setup_action' => 'Setup',
+    'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.',
+    'mfa_option_totp_title' => 'Mobile App',
+    'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
+    'mfa_option_backup_codes_title' => 'Backup Codes',
+    'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.',
+    'mfa_gen_confirm_and_enable' => 'Confirm and Enable',
+    'mfa_gen_backup_codes_title' => 'Backup Codes Setup',
+    'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.',
+    'mfa_gen_backup_codes_download' => 'Download Codes',
+    'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once',
+    'mfa_gen_totp_title' => 'Mobile App Setup',
+    'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
+    'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.',
+    'mfa_gen_totp_verify_setup' => 'Verify Setup',
+    'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:',
+    'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here',
+    'mfa_verify_access' => 'Verify Access',
+    'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.',
+    'mfa_verify_no_methods' => 'No Methods Configured',
+    'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.',
+    'mfa_verify_use_totp' => 'Verify using a mobile app',
+    'mfa_verify_use_backup_codes' => 'Verify using a backup code',
+    'mfa_verify_backup_code' => 'Backup Code',
+    'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:',
+    'mfa_verify_backup_code_enter_here' => 'Enter backup code here',
+    'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:',
+    'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.',
 ];
\ No newline at end of file
index 3cc46ab49e6fb0ebc993b24074e72b2a55c6b91a..2349ab735d06969ffe65d0c845a4757ef815a1ce 100644 (file)
@@ -39,6 +39,7 @@ return [
     'reset' => '리셋',
     'remove' => '제거',
     'add' => '추가',
+    'configure' => 'Configure',
     'fullscreen' => '전체화면',
     'favourite' => 'Favourite',
     'unfavourite' => 'Unfavourite',
@@ -60,6 +61,7 @@ return [
     'no_activity' => '활동 없음',
     'no_items' => '항목 없음',
     'back_to_top' => '맨 위로',
+    'skip_to_main_content' => 'Skip to main content',
     'toggle_details' => '내용 보기',
     'toggle_thumbnails' => '섬네일 보기',
     'details' => '정보',
index e286fee8ca1d4012f722424b5cfca56b515bcf49..aa25aa64614d479d8338da7b1652e2ed598b7602 100644 (file)
@@ -36,6 +36,7 @@ return [
     'export_html' => 'Contained Web(.html) 파일',
     'export_pdf' => 'PDF 파일',
     'export_text' => 'Plain Text(.txt) 파일',
+    'export_md' => 'Markdown File',
 
     // Permissions and restrictions
     'permissions' => '권한',
@@ -98,6 +99,7 @@ return [
     'shelves_permissions' => '서가 권한',
     'shelves_permissions_updated' => '서가 권한 바꿈',
     'shelves_permissions_active' => '서가 권한 허용함',
+    'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',
     'shelves_copy_permissions_to_books' => '권한 맞춤',
     'shelves_copy_permissions' => '실행',
     'shelves_copy_permissions_explain' => '서가의 모든 책자에 이 권한을 적용합니다. 서가의 권한을 저장했는지 확인하세요.',
index 95a681eca16ed58fabee580e68e42c3b9ca6d997..6c81bc7355ef1c423ba822b0e00e72bdb4400211 100755 (executable)
@@ -92,6 +92,7 @@ return [
     '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_parent' => 'Parent',
     'recycle_bin_deleted_by' => 'Deleted By',
     'recycle_bin_deleted_at' => 'Deletion Time',
     'recycle_bin_permanently_delete' => 'Permanently Delete',
@@ -104,6 +105,7 @@ return [
     '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_restore_parent' => 'Restore Parent',
     'recycle_bin_destroy_notification' => 'Deleted :count total items from the recycle bin.',
     'recycle_bin_restore_notification' => 'Restored :count total items from the recycle bin.',
 
@@ -117,6 +119,7 @@ return [
     'audit_table_user' => '사용자',
     'audit_table_event' => '이벤트',
     'audit_table_related' => 'Related Item or Detail',
+    'audit_table_ip' => 'IP Address',
     'audit_table_date' => '활동 날짜',
     'audit_date_from' => '날짜 범위 시작',
     'audit_date_to' => '날짜 범위 끝',
@@ -136,6 +139,7 @@ return [
     'role_details' => '권한 정보',
     'role_name' => '권한 이름',
     'role_desc' => '설명',
+    'role_mfa_enforced' => 'Requires Multi-Factor Authentication',
     'role_external_auth_id' => 'LDAP 확인',
     'role_system' => '시스템 권한',
     'role_manage_users' => '사용자 관리',
@@ -145,6 +149,7 @@ return [
     'role_manage_page_templates' => '템플릿 관리',
     'role_access_api' => '시스템 접근 API',
     'role_manage_settings' => '사이트 설정 관리',
+    'role_export_content' => 'Export content',
     '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.',
     'role_asset_desc' => '책자, 챕터, 문서별 권한은 이 설정에 우선합니다.',
@@ -202,6 +207,10 @@ return [
     'users_api_tokens_create' => '토큰 만들기',
     'users_api_tokens_expires' => '만료',
     'users_api_tokens_docs' => 'API 설명서',
+    'users_mfa' => 'Multi-Factor Authentication',
+    'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
+    'users_mfa_x_methods' => ':count method configured|:count methods configured',
+    'users_mfa_configure' => 'Configure Methods',
 
     // API Tokens
     'user_api_token_create' => 'API 토큰 만들기',
@@ -247,6 +256,7 @@ return [
         'it' => 'Italian',
         'ja' => '日本語',
         'ko' => '한국어',
+        'lt' => 'Lietuvių Kalba',
         'lv' => 'Latviešu Valoda',
         'nl' => 'Nederlands',
         'nb' => 'Norsk (Bokmål)',
index 6754d962069082ea2884d8875a5a8980f143ae15..ef8328103fe990081b50eb574f0e7e180c9aa937 100644 (file)
@@ -15,6 +15,7 @@ return [
     'alpha_dash'           => ':attribute(을)를 문자, 숫자, -, _로만 구성하세요.',
     'alpha_num'            => ':attribute(을)를 문자, 숫자로만 구성하세요.',
     'array'                => ':attribute(을)를 배열로 구성하세요.',
+    'backup_codes'         => 'The provided code is not valid or has already been used.',
     'before'               => ':attribute(을)를 :date 전으로 설정하세요.',
     'between'              => [
         'numeric' => ':attribute(을)를 :min~:max(으)로 구성하세요.',
@@ -98,6 +99,7 @@ return [
     ],
     'string'               => ':attribute(을)를 문자로 구성하세요.',
     'timezone'             => ':attribute(을)를 유효한 시간대로 구성하세요.',
+    'totp'                 => 'The provided code is not valid or has expired.',
     'unique'               => ':attribute(은)는 이미 있습니다.',
     'url'                  => ':attribute(은)는 유효하지 않은 형식입니다.',
     'uploaded'             => '파일 크기가 서버에서 허용하는 수치를 넘습니다.',
diff --git a/resources/lang/lt/activities.php b/resources/lang/lt/activities.php
new file mode 100644 (file)
index 0000000..b128934
--- /dev/null
@@ -0,0 +1,57 @@
+<?php
+/**
+ * Activity text strings.
+ * Is used for all the text within activity logs & notifications.
+ */
+return [
+
+    // Pages
+    'page_create'                 => 'sukurtas puslapis',
+    'page_create_notification'    => 'Puslapis sukurtas sėkmingai',
+    'page_update'                 => 'atnaujintas puslapis',
+    'page_update_notification'    => 'Puslapis sėkmingai atnaujintas',
+    'page_delete'                 => 'ištrintas puslapis',
+    'page_delete_notification'    => 'Puslapis sėkmingai ištrintas',
+    'page_restore'                => 'atkurtas puslapis',
+    'page_restore_notification'   => 'Puslapis sėkmingai atkurtas',
+    'page_move'                   => 'perkeltas puslapis',
+
+    // Chapters
+    'chapter_create'              => 'sukurtas skyrius',
+    'chapter_create_notification' => 'Skyrius sėkmingai sukurtas',
+    'chapter_update'              => 'atnaujintas skyrius',
+    'chapter_update_notification' => 'Skyrius sekmingai atnaujintas',
+    'chapter_delete'              => 'ištrintas skyrius',
+    'chapter_delete_notification' => 'Skyrius sėkmingai ištrintas',
+    'chapter_move'                => 'perkeltas skyrius',
+
+    // Books
+    'book_create'                 => 'sukurta knyga',
+    'book_create_notification'    => 'Knyga sėkmingai sukurta',
+    'book_update'                 => 'atnaujinta knyga',
+    'book_update_notification'    => 'Knyga sėkmingai atnaujinta',
+    'book_delete'                 => 'ištrinta knyga',
+    'book_delete_notification'    => 'Knyga sėkmingai ištrinta',
+    'book_sort'                   => 'surūšiuota knyga',
+    'book_sort_notification'      => 'Knyga sėkmingai perrūšiuota',
+
+    // Bookshelves
+    'bookshelf_create'            => 'sukurta knygų lentyna',
+    'bookshelf_create_notification'    => 'Knygų lentyna sėkmingai sukurta',
+    'bookshelf_update'                 => 'atnaujinta knygų lentyna',
+    'bookshelf_update_notification'    => 'Knygų lentyna sėkmingai atnaujinta',
+    'bookshelf_delete'                 => 'ištrinta knygų lentyna',
+    'bookshelf_delete_notification'    => 'Knygų lentyna sėkmingai ištrinta',
+
+    // Favourites
+    'favourite_add_notification' => '":name" has been added to your favourites',
+    'favourite_remove_notification' => '":name" has been removed from your favourites',
+
+    // MFA
+    'mfa_setup_method_notification' => 'Multi-factor method successfully configured',
+    'mfa_remove_method_notification' => 'Multi-factor method successfully removed',
+
+    // Other
+    'commented_on'                => 'pakomentavo',
+    'permissions_update'          => 'atnaujinti leidimai',
+];
diff --git a/resources/lang/lt/auth.php b/resources/lang/lt/auth.php
new file mode 100644 (file)
index 0000000..f9bf271
--- /dev/null
@@ -0,0 +1,112 @@
+<?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' => 'Šie įgaliojimai neatitinka mūsų įrašų.',
+    'throttle' => 'Per daug prisijungimo bandymų. Prašome pabandyti dar kartą po :seconds sekundžių.',
+
+    // Login & Register
+    'sign_up' => 'Užsiregistruoti',
+    'log_in' => 'Prisijungti',
+    'log_in_with' => 'Prisijungti su :socialDriver',
+    'sign_up_with' => 'Užsiregistruoti su :socialDriver',
+    'logout' => 'Atsijungti',
+
+    'name' => 'Pavadinimas',
+    'username' => 'Vartotojo vardas',
+    'email' => 'Elektroninis paštas',
+    'password' => 'Slaptažodis',
+    'password_confirm' => 'Patvirtinti slaptažodį',
+    'password_hint' => 'Privalo būti daugiau nei 7 simboliai',
+    'forgot_password' => 'Pamiršote slaptažodį?',
+    'remember_me' => 'Prisimink mane',
+    'ldap_email_hint' => 'Prašome įvesti elektroninį paštą, kad galėtume naudotis šia paskyra.',
+    'create_account' => 'Sukurti paskyrą',
+    'already_have_account' => 'Jau turite paskyrą?',
+    'dont_have_account' => 'Neturite paskyros?',
+    'social_login' => 'Socialinis prisijungimas',
+    'social_registration' => 'Socialinė registracija',
+    'social_registration_text' => 'Užsiregistruoti ir prisijungti naudojantis kita paslauga.',
+
+    'register_thanks' => 'Ačiū, kad užsiregistravote!',
+    'register_confirm' => 'Prašome patikrinti savo elektroninį paštą ir paspausti patvirtinimo mygtuką, kad gautumėte leidimą į :appName.',
+    'registrations_disabled' => 'Registracijos šiuo metu negalimos',
+    'registration_email_domain_invalid' => 'Elektroninio pašto domenas neturi prieigos prie šios programos',
+    'register_success' => 'Ačiū už prisijungimą! Dabar jūs užsiregistravote ir prisijungėte.',
+
+
+    // Password Reset
+    'reset_password' => 'Pakeisti slaptažodį',
+    'reset_password_send_instructions' => 'Įveskite savo elektroninį paštą žemiau ir jums bus išsiųstas elektroninis laiškas su slaptažodžio nustatymo nuoroda.',
+    'reset_password_send_button' => 'Atsiųsti atsatymo nuorodą',
+    'reset_password_sent' => 'Slaptažodžio nustatymo nuoroda bus išsiųsta :email jeigu elektroninio pašto adresas bus rastas sistemoje.',
+    'reset_password_success' => 'Jūsų slaptažodis buvo sėkmingai atnaujintas.',
+    'email_reset_subject' => 'Atnaujinti jūsų :appName slaptažodį',
+    'email_reset_text' => 'Šį laišką gaunate, nes mes gavome slaptažodžio atnaujinimo užklausą iš jūsų paskyros.',
+    'email_reset_not_requested' => 'Jeigu jums nereikia slaptažodžio atnaujinimo, tolimesnių veiksmų atlikti nereikia.',
+
+
+    // Email Confirmation
+    'email_confirm_subject' => 'Patvirtinkite savo elektroninį paštą :appName',
+    'email_confirm_greeting' => 'Ačiū už prisijungimą prie :appName!',
+    'email_confirm_text' => 'Prašome patvirtinti savo elektroninio pašto adresą paspaudus mygtuką žemiau:',
+    'email_confirm_action' => 'Patvirtinkite elektroninį paštą',
+    'email_confirm_send_error' => 'Būtinas elektroninio laiško patviritnimas, bet sistema negali išsiųsti laiško. Susisiekite su administratoriumi, kad užtikrintumėte, jog elektroninis paštas atsinaujino teisingai.',
+    'email_confirm_success' => 'Jūsų elektroninis paštas buvo patvirtintas!',
+    'email_confirm_resent' => 'Elektroninio pašto patvirtinimas persiųstas, prašome patikrinti pašto dėžutę.',
+
+    'email_not_confirmed' => 'Elektroninis paštas nepatvirtintas',
+    'email_not_confirmed_text' => 'Jūsų elektroninis paštas dar vis nepatvirtintas.',
+    'email_not_confirmed_click_link' => 'Prašome paspausti nuorodą elektroniniame pašte, kuri buvo išsiųsta iš karto po registracijos.',
+    'email_not_confirmed_resend' => 'Jeigu nerandate elektroninio laiško, galite dar kartą išsiųsti patvirtinimo elektroninį laišką, pateikdami žemiau esančią formą.',
+    'email_not_confirmed_resend_button' => 'Persiųsti patvirtinimo laišką',
+
+    // User Invite
+    'user_invite_email_subject' => 'Jūs buvote pakviestas prisijungti prie :appName!',
+    'user_invite_email_greeting' => 'Paskyra buvo sukurta jums :appName.',
+    'user_invite_email_text' => 'Paspauskite mygtuką žemiau, kad sukurtumėte paskyros slaptažodį ir gautumėte prieigą:',
+    'user_invite_email_action' => 'Sukurti paskyros slaptažodį',
+    'user_invite_page_welcome' => 'Sveiki atvykę į :appName!',
+    'user_invite_page_text' => 'Norėdami galutinai pabaigti paskyrą ir gauti prieigą jums reikia nustatyti slaptažodį, kuris bus naudojamas prisijungiant prie :appName ateities vizitų metu.',
+    'user_invite_page_confirm_button' => 'Patvirtinti slaptažodį',
+    'user_invite_success' => 'Slaptažodis nustatytas, dabar turite prieigą prie :appName!',
+
+    // Multi-factor Authentication
+    'mfa_setup' => 'Setup Multi-Factor Authentication',
+    'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
+    'mfa_setup_configured' => 'Already configured',
+    'mfa_setup_reconfigure' => 'Reconfigure',
+    'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?',
+    'mfa_setup_action' => 'Setup',
+    'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.',
+    'mfa_option_totp_title' => 'Mobile App',
+    'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
+    'mfa_option_backup_codes_title' => 'Backup Codes',
+    'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.',
+    'mfa_gen_confirm_and_enable' => 'Confirm and Enable',
+    'mfa_gen_backup_codes_title' => 'Backup Codes Setup',
+    'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.',
+    'mfa_gen_backup_codes_download' => 'Download Codes',
+    'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once',
+    'mfa_gen_totp_title' => 'Mobile App Setup',
+    'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
+    'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.',
+    'mfa_gen_totp_verify_setup' => 'Verify Setup',
+    'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:',
+    'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here',
+    'mfa_verify_access' => 'Verify Access',
+    'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.',
+    'mfa_verify_no_methods' => 'No Methods Configured',
+    'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.',
+    'mfa_verify_use_totp' => 'Verify using a mobile app',
+    'mfa_verify_use_backup_codes' => 'Verify using a backup code',
+    'mfa_verify_backup_code' => 'Backup Code',
+    'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:',
+    'mfa_verify_backup_code_enter_here' => 'Enter backup code here',
+    'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:',
+    'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.',
+];
\ No newline at end of file
diff --git a/resources/lang/lt/common.php b/resources/lang/lt/common.php
new file mode 100644 (file)
index 0000000..7ed434b
--- /dev/null
@@ -0,0 +1,95 @@
+<?php
+/**
+ * Common elements found throughout many areas of BookStack.
+ */
+return [
+
+    // Buttons
+    'cancel' => 'Atšaukti',
+    'confirm' => 'Patvirtinti',
+    'back' => 'Grįžti',
+    'save' => 'Išsaugoti',
+    'continue' => 'Praleisti',
+    'select' => 'Pasirinkti',
+    'toggle_all' => 'Perjungti visus',
+    'more' => 'Daugiau',
+
+    // Form Labels
+    'name' => 'Pavadinimas',
+    'description' => 'Apibūdinimas',
+    'role' => 'Vaidmuo',
+    'cover_image' => 'Viršelio nuotrauka',
+    'cover_image_description' => 'Ši nuotrauka turi būti maždaug 440x250px.',
+    
+    // Actions
+    'actions' => 'Veiksmai',
+    'view' => 'Rodyti',
+    'view_all' => 'Rodyti visus',
+    'create' => 'Sukurti',
+    'update' => 'Atnaujinti',
+    'edit' => 'Redaguoti',
+    'sort' => 'Rūšiuoti',
+    'move' => 'Perkelti',
+    'copy' => 'Kopijuoti',
+    'reply' => 'Atsakyti',
+    'delete' => 'Ištrinti',
+    'delete_confirm' => 'Patvirtinti ištrynimą',
+    'search' => 'Paieška',
+    'search_clear' => 'Išvalyti paiešką',
+    'reset' => 'Atsatyti',
+    'remove' => 'Pašalinti',
+    'add' => 'Pridėti',
+    'configure' => 'Configure',
+    'fullscreen' => 'Visas ekranas',
+    'favourite' => 'Favourite',
+    'unfavourite' => 'Unfavourite',
+    'next' => 'Next',
+    'previous' => 'Previous',
+
+    // Sort Options
+    'sort_options' => 'Rūšiuoti pasirinkimus',
+    'sort_direction_toggle' => 'Rūšiuoti krypties perjungimus',
+    'sort_ascending' => 'Rūšiuoti didėjančia tvarka',
+    'sort_descending' => 'Rūšiuoti mažėjančia tvarka',
+    'sort_name' => 'Pavadinimas',
+    'sort_default' => 'Numatytas',
+    'sort_created_at' => 'Sukurta data',
+    'sort_updated_at' => 'Atnaujinta data',
+
+    // Misc
+    'deleted_user' => 'Ištrinti naudotoją',
+    'no_activity' => 'Nėra veiklų',
+    'no_items' => 'Nėra elementų',
+    'back_to_top' => 'Grįžti į pradžią',
+    'skip_to_main_content' => 'Skip to main content',
+    'toggle_details' => 'Perjungti detales',
+    'toggle_thumbnails' => 'Perjungti miniatūras',
+    'details' => 'Detalės',
+    'grid_view' => 'Tinklelio vaizdas',
+    'list_view' => 'Sąrašas',
+    'default' => 'Numatytas',
+    'breadcrumb' => 'Duonos rėžis',
+
+    // Header
+    'header_menu_expand' => 'Plėsti antraštės meniu',
+    'profile_menu' => 'Profilio meniu',
+    'view_profile' => 'Rodyti porofilį',
+    'edit_profile' => 'Redaguoti profilį',
+    'dark_mode' => 'Tamsus rėžimas',
+    'light_mode' => 'Šviesus rėžimas',
+
+    // Layout tabs
+    'tab_info' => 'Informacija',
+    'tab_info_label' => 'Skirtukas: Rodyti antrinę informaciją',
+    'tab_content' => 'Turinys',
+    'tab_content_label' => 'Skirtukas: Rodyti pirminę informaciją',
+
+    // Email Content
+    'email_action_help' => 'Jeigu kyla problemų spaudžiant :actionText: mygtuką, nukopijuokite ir įklijuokite URL į savo naršyklę.',
+    'email_rights' => 'Visos teisės rezervuotos',
+
+    // Footer Link Options
+    // Not directly used but available for convenience to users.
+    'privacy_policy' => 'Privatumo politika',
+    'terms_of_service' => 'Paslaugų teikimo paslaugos',
+];
diff --git a/resources/lang/lt/components.php b/resources/lang/lt/components.php
new file mode 100644 (file)
index 0000000..ce573da
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+/**
+ * Text used in custom JavaScript driven components.
+ */
+return [
+
+    // Image Manager
+    'image_select' => 'Nuotraukų pasirinkimas',
+    'image_all' => 'Visi',
+    'image_all_title' => 'Rodyti visas nuotraukas',
+    'image_book_title' => 'Peržiūrėti nuotraukas, įkeltas į šią knygą',
+    'image_page_title' => 'Peržiūrėti nuotraukas, įkeltas į šį puslapį',
+    'image_search_hint' => 'Ieškoti pagal nuotraukos pavadinimą',
+    'image_uploaded' => 'Įkelta :uploadedDate',
+    'image_load_more' => 'Rodyti daugiau',
+    'image_image_name' => 'Nuotraukos pavadinimas',
+    'image_delete_used' => 'Ši nuotrauka yra naudojama puslapyje žemiau.',
+    'image_delete_confirm_text' => 'Ar jūs esate tikri, kad norite ištrinti šią nuotrauką?',
+    'image_select_image' => 'Pasirinkti nuotrauką',
+    'image_dropzone' => 'Tempkite nuotraukas arba spauskite šia, kad įkeltumėte',
+    'images_deleted' => 'Nuotraukos ištrintos',
+    'image_preview' => 'Nuotraukų peržiūra',
+    'image_upload_success' => 'Nuotrauka įkelta sėkmingai',
+    'image_update_success' => 'Nuotraukos detalės sėkmingai atnaujintos',
+    'image_delete_success' => 'Nuotrauka sėkmingai ištrinti',
+    'image_upload_remove' => 'Pašalinti',
+
+    // Code Editor
+    'code_editor' => 'Redaguoti kodą',
+    'code_language' => 'Kodo kalba',
+    'code_content' => 'Kodo turinys',
+    'code_session_history' => 'Sesijos istorija',
+    'code_save' => 'Išsaugoti kodą',
+];
diff --git a/resources/lang/lt/entities.php b/resources/lang/lt/entities.php
new file mode 100644 (file)
index 0000000..2b9a5e2
--- /dev/null
@@ -0,0 +1,324 @@
+<?php
+/**
+ * Text used for 'Entities' (Document Structure Elements) such as
+ * Books, Shelves, Chapters & Pages
+ */
+return [
+
+    // Shared
+    'recently_created' => 'Neseniai sukurtas',
+    'recently_created_pages' => 'Neseniai sukurti puslapiai',
+    'recently_updated_pages' => 'Neseniai atnaujinti puslapiai',
+    'recently_created_chapters' => 'Neseniai sukurti skyriai',
+    'recently_created_books' => 'Neseniai sukurtos knygos',
+    'recently_created_shelves' => 'Neseniai sukurtos lentynos',
+    'recently_update' => 'Neseniai atnaujinta',
+    'recently_viewed' => 'Neseniai peržiūrėta',
+    'recent_activity' => 'Paskutiniai veiksmai',
+    'create_now' => 'Sukurti vieną dabar',
+    'revisions' => 'Pataisymai',
+    'meta_revision' => 'Pataisymas #:revisionCount',
+    'meta_created' => 'Sukurta :timeLength',
+    'meta_created_name' => 'Sukurta :timeLength naudotojo :user',
+    'meta_updated' => 'Atnaujintas :timeLength',
+    'meta_updated_name' => 'Atnaujinta :timeLength naudotojo :user',
+    'meta_owned_name' => 'Priklauso :user',
+    'entity_select' => 'Pasirinkti subjektą',
+    'images' => 'Nuotraukos',
+    'my_recent_drafts' => 'Naujausi išsaugoti juodraščiai',
+    'my_recently_viewed' => 'Neseniai peržiūrėti',
+    'my_most_viewed_favourites' => 'My Most Viewed Favourites',
+    'my_favourites' => 'My Favourites',
+    'no_pages_viewed' => 'Jūs neperžiūrėjote nei vieno puslapio',
+    'no_pages_recently_created' => 'Nebuvos sukurta jokių puslapių',
+    'no_pages_recently_updated' => 'Nebuvo atnaujinta jokių puslapių',
+    'export' => 'Eksportuoti',
+    'export_html' => 'Sudėtinis žiniatinklio failas',
+    'export_pdf' => 'PDF failas',
+    'export_text' => 'Paprastas failo tekstas',
+    'export_md' => 'Markdown File',
+
+    // Permissions and restrictions
+    'permissions' => 'Leidimai',
+    'permissions_intro' => 'Įgalinus šias teises, pirmenybė bus teikiama visiems nustatytiems vaidmenų leidimams.',
+    'permissions_enable' => 'Įgalinti pasirinktus leidimus',
+    'permissions_save' => 'Išsaugoti leidimus',
+    'permissions_owner' => 'Savininkas',
+
+    // Search
+    'search_results' => 'Ieškoti rezultatų',
+    'search_total_results_found' => ':count rastas rezultatas|:count iš viso rezultatų rasta',
+    'search_clear' => 'Išvalyti paiešką',
+    'search_no_pages' => 'Nėra puslapių pagal šią paiešką',
+    'search_for_term' => 'Ieškoti pagal :term',
+    'search_more' => 'Daugiau rezultatų',
+    'search_advanced' => 'Išplėstinė paieška',
+    'search_terms' => 'Ieškoti terminų',
+    'search_content_type' => 'Turinio tipas',
+    'search_exact_matches' => 'Tikslūs atitikmenys',
+    'search_tags' => 'Žymių paieškos',
+    'search_options' => 'Parinktys',
+    'search_viewed_by_me' => 'Mano peržiūrėta',
+    'search_not_viewed_by_me' => 'Mano neperžiūrėta',
+    'search_permissions_set' => 'Nustatyti leidimus',
+    'search_created_by_me' => 'Mano sukurta',
+    'search_updated_by_me' => 'Mano atnaujinimas',
+    'search_owned_by_me' => 'Priklauso man',
+    'search_date_options' => 'Datos parinktys',
+    'search_updated_before' => 'Atnaujinta prieš',
+    'search_updated_after' => 'Atnaujinta po',
+    'search_created_before' => 'Sukurta prieš',
+    'search_created_after' => 'Sukurta po',
+    'search_set_date' => 'Nustatyti datą',
+    'search_update' => 'Atnaujinti paiešką',
+
+    // Shelves
+    'shelf' => 'Lentyna',
+    'shelves' => 'Lentynos',
+    'x_shelves' => ':count lentyna|:count lentynos',
+    'shelves_long' => 'Knygų lentynos',
+    'shelves_empty' => 'Nebuvo sukurtos jokios lentynos',
+    'shelves_create' => 'Sukurti naują lentyną',
+    'shelves_popular' => 'Populiarios lentynos',
+    'shelves_new' => 'Naujos lentynos',
+    'shelves_new_action' => 'Nauja lentyna',
+    'shelves_popular_empty' => 'Populiariausios knygos pasirodys čia.',
+    'shelves_new_empty' => 'Visai neseniai sukurtos lentynos pasirodys čia.',
+    'shelves_save' => 'Išsaugoti lenyną',
+    'shelves_books' => 'Knygos šioje lentynoje',
+    'shelves_add_books' => 'Pridėti knygas į šią lentyną',
+    'shelves_drag_books' => 'Vilkite knygas čia, kad pridėtumėte jas į šią lentyną',
+    'shelves_empty_contents' => 'Ši lentyną neturi jokių pridėtų knygų',
+    'shelves_edit_and_assign' => 'Redaguoti lentyną, kad pridėti knygų',
+    'shelves_edit_named' => 'Redaguoti knygų lentyną :name',
+    'shelves_edit' => 'Redaguoti knygų lentyną',
+    'shelves_delete' => 'Ištrinti knygų lentyną',
+    'shelves_delete_named' => 'Ištrinti knygų lentyną :name',
+    'shelves_delete_explain' => "This will delete the bookshelf with the name ':name'. Contained books will not be deleted.",
+    'shelves_delete_confirmation' => 'Ar jūs esate tikri, kad norite ištrinti šią knygų lentyną?',
+    'shelves_permissions' => 'Knygų lentynos leidimai',
+    'shelves_permissions_updated' => 'Knygų lentynos leidimai atnaujinti',
+    'shelves_permissions_active' => 'Knygų lentynos leidimai aktyvūs',
+    'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',
+    'shelves_copy_permissions_to_books' => 'Kopijuoti leidimus knygoms',
+    'shelves_copy_permissions' => 'Kopijuoti leidimus',
+    'shelves_copy_permissions_explain' => 'Visoms knygoms, esančioms šioje knygų lentynoje, bus taikomi dabartiniai leidimų nustatymai. Prieš suaktyvindami įsitikinkite, kad visi šios knygų lentynos leidimų pakeitimai buvo išsaugoti.',
+    'shelves_copy_permission_success' => 'Knygų lentynos leidimai nukopijuoti į :count knygas',
+
+    // Books
+    'book' => 'Knyga',
+    'books' => 'Knygos',
+    'x_books' => ':count knyga|:count knygos',
+    'books_empty' => 'Nebuvo sukurta jokių knygų',
+    'books_popular' => 'Populiarios knygos',
+    'books_recent' => 'Naujos knygos',
+    'books_new' => 'Naujos knygos',
+    'books_new_action' => 'Nauja knyga',
+    'books_popular_empty' => 'Čia pasirodys pačios populiariausios knygos.',
+    'books_new_empty' => 'Čia pasirodys naujausios sukurtos knygos',
+    'books_create' => 'Sukurti naują knygą',
+    'books_delete' => 'Ištrinti knygą',
+    'books_delete_named' => 'Ištrinti knygą :bookName',
+    'books_delete_explain' => 'Tai ištrins knygą su pavadinimu \':bookName\'. Visi puslapiai ir skyriai bus pašalinti.',
+    'books_delete_confirmation' => 'Ar jūs esate tikri, kad norite ištrinti šią knygą?',
+    'books_edit' => 'Redaguoti knygą',
+    'books_edit_named' => 'Redaguoti knygą :bookName',
+    'books_form_book_name' => 'Knygos pavadinimas',
+    'books_save' => 'Išsaugoti knygą',
+    'books_permissions' => 'Knygos leidimas',
+    'books_permissions_updated' => 'Knygos leidimas atnaujintas',
+    'books_empty_contents' => 'Jokių puslapių ar skyrių nebuvo skurta šiai knygai',
+    'books_empty_create_page' => 'Sukurti naują puslapį',
+    'books_empty_sort_current_book' => 'Rūšiuoti dabartinę knygą',
+    'books_empty_add_chapter' => 'Pridėti skyrių',
+    'books_permissions_active' => 'Knygos leidimas aktyvus',
+    'books_search_this' => 'Ieškoti šioje knygoje',
+    'books_navigation' => 'Knygos naršymas',
+    'books_sort' => 'Rūšiuoti pagal knygos turinį',
+    'books_sort_named' => 'Rūšiuoti knygą :bookName',
+    'books_sort_name' => 'Rūšiuoti pagal vardą',
+    'books_sort_created' => 'Rūšiuoti pagal sukūrimo datą',
+    'books_sort_updated' => 'Rūšiuoti pagal atnaujinimo datą',
+    'books_sort_chapters_first' => 'Skyriaus pradžia',
+    'books_sort_chapters_last' => 'Skyriaus pabaiga',
+    'books_sort_show_other' => 'Rodyti kitas knygas',
+    'books_sort_save' => 'Išsaugoti naują įsakymą',
+
+    // Chapters
+    'chapter' => 'Skyrius',
+    'chapters' => 'Skyriai',
+    'x_chapters' => ':count skyrius|:count skyriai',
+    'chapters_popular' => 'Populiarūs skyriai',
+    'chapters_new' => 'Naujas skyrius',
+    'chapters_create' => 'Sukurti naują skyrių',
+    'chapters_delete' => 'Ištrinti skyrių',
+    'chapters_delete_named' => 'Ištrinti skyrių :chapterName',
+    'chapters_delete_explain' => 'Tai ištrins skyrių su pavadinimu \':chapterName\. Visi puslapiai, esantys šiame skyriuje, taip pat bus ištrinti.',
+    'chapters_delete_confirm' => 'Ar esate tikri, jog norite ištrinti šį skyrių?',
+    'chapters_edit' => 'Redaguoti skyrių',
+    'chapters_edit_named' => 'Redaguoti skyrių :chapterName',
+    'chapters_save' => 'Išsaugoti skyrių',
+    'chapters_move' => 'Perkelti skyrių',
+    'chapters_move_named' => 'Perkelti skyrių :chapterName',
+    'chapter_move_success' => 'Skyrius perkeltas į :bookName',
+    'chapters_permissions' => 'Skyriaus leidimai',
+    'chapters_empty' => 'Šiuo metu skyriuje nėra puslapių',
+    'chapters_permissions_active' => 'Skyriaus leidimai aktyvūs',
+    'chapters_permissions_success' => 'Skyriaus leidimai atnaujinti',
+    'chapters_search_this' => 'Ieškoti šio skyriaus',
+
+    // Pages
+    'page' => 'Puslapis',
+    'pages' => 'Puslapiai',
+    'x_pages' => ':count puslapis|:count puslapiai',
+    'pages_popular' => 'Populiarūs puslapiai',
+    'pages_new' => 'Naujas puslapis',
+    'pages_attachments' => 'Priedai',
+    'pages_navigation' => 'Puslapių navigacija',
+    'pages_delete' => 'Ištrinti puslapį',
+    'pages_delete_named' => 'Ištrinti puslapį :pageName',
+    'pages_delete_draft_named' => 'Ištrinti juodraščio puslapį :pageName',
+    'pages_delete_draft' => 'Ištrinti juodraščio puslapį',
+    'pages_delete_success' => 'Puslapis ištrintas',
+    'pages_delete_draft_success' => 'Juodraščio puslapis ištrintas',
+    'pages_delete_confirm' => 'Ar esate tikri, kad norite ištrinti šį puslapį?',
+    'pages_delete_draft_confirm' => 'Ar esate tikri, kad norite ištrinti šį juodraščio puslapį?',
+    'pages_editing_named' => 'Redaguojamas puslapis :pageName',
+    'pages_edit_draft_options' => 'Juodrasčio pasirinkimai',
+    'pages_edit_save_draft' => 'Išsaugoti juodraštį',
+    'pages_edit_draft' => 'Redaguoti juodraščio puslapį',
+    'pages_editing_draft' => 'Redaguojamas juodraštis',
+    'pages_editing_page' => 'Redaguojamas puslapis',
+    'pages_edit_draft_save_at' => 'Juodraštis išsaugotas',
+    'pages_edit_delete_draft' => 'Ištrinti juodraštį',
+    'pages_edit_discard_draft' => 'Išmesti juodraštį',
+    'pages_edit_set_changelog' => 'Nustatyti keitimo žurnalą',
+    'pages_edit_enter_changelog_desc' => 'Įveskite trumpus, jūsų atliktus, pokyčių aprašymus',
+    'pages_edit_enter_changelog' => 'Įeiti į keitimo žurnalą',
+    'pages_save' => 'Išsaugoti puslapį',
+    'pages_title' => 'Puslapio antraštė',
+    'pages_name' => 'Puslapio pavadinimas',
+    'pages_md_editor' => 'Redaguotojas',
+    'pages_md_preview' => 'Peržiūra',
+    'pages_md_insert_image' => 'Įterpti nuotrauką',
+    'pages_md_insert_link' => 'Įterpti subjekto nuorodą',
+    'pages_md_insert_drawing' => 'Įterpti piešinį',
+    'pages_not_in_chapter' => 'Puslapio nėra skyriuje',
+    'pages_move' => 'Perkelti puslapį',
+    'pages_move_success' => 'Puslapis perkeltas į ":parentName"',
+    'pages_copy' => 'Nukopijuoti puslapį',
+    'pages_copy_desination' => 'Nukopijuoti tikslą',
+    'pages_copy_success' => 'Puslapis sėkmingai nukopijuotas',
+    'pages_permissions' => 'Puslapio leidimai',
+    'pages_permissions_success' => 'Puslapio leidimai atnaujinti',
+    'pages_revision' => 'Peržiūra',
+    'pages_revisions' => 'Puslapio peržiūros',
+    'pages_revisions_named' => 'Peržiūros puslapio :pageName',
+    'pages_revision_named' => 'Peržiūra puslapio :pageName',
+    'pages_revision_restored_from' => 'Atkurta iš #:id; :summary',
+    'pages_revisions_created_by' => 'Sukurta',
+    'pages_revisions_date' => 'Peržiūros data',
+    'pages_revisions_number' => '#',
+    'pages_revisions_numbered' => 'Peržiūra #:id',
+    'pages_revisions_numbered_changes' => 'Peržiūros #:id pokyčiai',
+    'pages_revisions_changelog' => 'Keitimo žurnalas',
+    'pages_revisions_changes' => 'Pakeitimai',
+    'pages_revisions_current' => 'Dabartinė versija',
+    'pages_revisions_preview' => 'Peržiūra',
+    'pages_revisions_restore' => 'Atkurti',
+    'pages_revisions_none' => 'Šis puslapis neturi peržiūrų',
+    'pages_copy_link' => 'Kopijuoti nuorodą',
+    'pages_edit_content_link' => 'Redaguoti turinį',
+    'pages_permissions_active' => 'Puslapio leidimai aktyvūs',
+    'pages_initial_revision' => 'Pradinis skelbimas',
+    'pages_initial_name' => 'Naujas puslapis',
+    'pages_editing_draft_notification' => 'Dabar jūs redaguojate juodraštį, kuris paskutinį kartą buvo išsaugotas :timeDiff',
+    'pages_draft_edited_notification' => 'Šis puslapis buvo redaguotas iki to laiko. Rekomenduojame jums išmesti šį juodraštį.',
+    'pages_draft_edit_active' => [
+        'start_a' => ':count naudotojai pradėjo redaguoti šį puslapį',
+        'start_b' => ':userName pradėjo redaguoti šį puslapį',
+        'time_a' => 'nuo puslapio paskutinio atnaujinimo',
+        'time_b' => 'paskutinėmis :minCount minutėmis',
+        'message' => ':start :time. Pasistenkite neperrašyti vienas kito atnaujinimų!',
+    ],
+    'pages_draft_discarded' => 'Juodraštis atmestas, redaguotojas atnaujintas dabartinis puslapio turinys',
+    'pages_specific' => 'Specifinis puslapis',
+    'pages_is_template' => 'Puslapio šablonas',
+
+    // Editor Sidebar
+    'page_tags' => 'Puslapio žymos',
+    'chapter_tags' => 'Skyriaus žymos',
+    'book_tags' => 'Knygos žymos',
+    'shelf_tags' => 'Lentynų žymos',
+    'tag' => 'Žymos',
+    'tags' =>  'Tags',
+    'tag_name' =>  'Tag Name',
+    'tag_value' => 'Žymos vertė (neprivaloma)',
+    'tags_explain' => "Add some tags to better categorise your content. \n You can assign a value to a tag for more in-depth organisation.",
+    'tags_add' => 'Pridėti kitą žymą',
+    'tags_remove' => 'Pridėti kitą žymą',
+    'attachments' => 'Priedai',
+    'attachments_explain' => 'Įkelkite kelis failus arba pridėkite nuorodas savo puslapyje. Jie matomi puslapio šoninėje juostoje.',
+    'attachments_explain_instant_save' => 'Pakeitimai čia yra išsaugomi akimirksniu.',
+    'attachments_items' => 'Pridėti elementai',
+    'attachments_upload' => 'Įkelti failą',
+    'attachments_link' => 'Pridėti nuorodą',
+    'attachments_set_link' => 'Nustatyti nuorodą',
+    'attachments_delete' => 'Ar esate tikri, kad norite ištrinti šį priedą?',
+    'attachments_dropzone' => 'Numesti failus arba paspausti čia ir pridėti failą',
+    'attachments_no_files' => 'Failai nebuvo įkelti',
+    'attachments_explain_link' => 'Jūs galite pridėti nuorodas, jei nenorite įkelti failo. Tai gali būti nuoroda į kitą puslapį arba nuoroda į failą debesyje.',
+    'attachments_link_name' => 'Nuorodos pavadinimas',
+    'attachment_link' => 'Priedo nuoroda',
+    'attachments_link_url' => 'Nuoroda į failą',
+    'attachments_link_url_hint' => 'URL į failą',
+    'attach' => 'Pridėti',
+    'attachments_insert_link' => 'Pridėti priedo nuorodą į puslapį',
+    'attachments_edit_file' => 'Redaguoti failą',
+    'attachments_edit_file_name' => 'Failo pavadinimas',
+    'attachments_edit_drop_upload' => 'Numesti failus arba spausti čia ir atsisiųsti ir perrašyti',
+    'attachments_order_updated' => 'Atnaujintas priedų išsidėstymas',
+    'attachments_updated_success' => 'Priedų detalės atnaujintos',
+    'attachments_deleted' => 'Priedas ištrintas',
+    'attachments_file_uploaded' => 'Failas sėkmingai įkeltas',
+    'attachments_file_updated' => 'Failas sėkmingai atnaujintas',
+    'attachments_link_attached' => 'Nuoroda sėkmingai pridėta puslapyje',
+    'templates' => 'Šablonai',
+    'templates_set_as_template' => 'Puslapis yra šablonas',
+    'templates_explain_set_as_template' => 'Jūs galite nustatyti šį puslapį kaip šabloną, jo turinys bus panaudotas, kuriant kitus puslapius. Kiti naudotojai galės naudotis šiuo šablonu, jei turės peržiūros leidimą šiam puslapiui.',
+    'templates_replace_content' => 'Pakeisti puslapio turinį',
+    'templates_append_content' => 'Papildyti puslapio turinį',
+    'templates_prepend_content' => 'Priklauso nuo puslapio turinio',
+
+    // Profile View
+    'profile_user_for_x' => 'Naudotojas :time',
+    'profile_created_content' => 'Sukurtas tyrinys',
+    'profile_not_created_pages' => ':userName nesukūrė jokio puslapio',
+    'profile_not_created_chapters' => ':userName nesukūrė jokio skyriaus',
+    'profile_not_created_books' => ':userName nesukūrė jokios knygos',
+    'profile_not_created_shelves' => ':userName nesukūrė jokių lentynų',
+
+    // Comments
+    'comment' => 'Komentaras',
+    'comments' => 'Komentarai',
+    'comment_add' => 'Pridėti komentarą',
+    'comment_placeholder' => 'Palikite komentarą čia',
+    'comment_count' => '{0} Nėra komentarų|{1} 1 komentaras|[2,*] :count komentarai',
+    'comment_save' => 'Išsaugoti komentarą',
+    'comment_saving' => 'Komentaras išsaugojamas...',
+    'comment_deleting' => 'Komentaras ištrinamas...',
+    'comment_new' => 'Naujas komentaras',
+    'comment_created' => 'Pakomentuota :createDiff',
+    'comment_updated' => 'Atnaujinta :updateDiff pagal :username',
+    'comment_deleted_success' => 'Komentaras ištrintas',
+    'comment_created_success' => 'Komentaras pridėtas',
+    'comment_updated_success' => 'Komentaras atnaujintas',
+    'comment_delete_confirm' => 'Esate tikri, kad norite ištrinti šį komentarą?',
+    'comment_in_reply_to' => 'Atsakydamas į :commentId',
+
+    // Revision
+    'revision_delete_confirm' => 'Esate tikri, kad norite ištrinti šią peržiūrą?',
+    'revision_restore_confirm' => 'Esate tikri, kad norite atkurti šią peržiūrą? Dabartinis puslapio turinys bus pakeistas.',
+    'revision_delete_success' => 'Peržiūra ištrinta',
+    'revision_cannot_delete_latest' => 'Negalima išrinti vėliausios peržiūros'
+];
diff --git a/resources/lang/lt/errors.php b/resources/lang/lt/errors.php
new file mode 100644 (file)
index 0000000..c16c37a
--- /dev/null
@@ -0,0 +1,105 @@
+<?php
+/**
+ * Text shown in error messaging.
+ */
+return [
+
+    // Permissions
+    'permission' => 'Jūs neturite leidimo atidaryti šio puslapio.',
+    'permissionJson' => 'Jūs neturite leidimo atlikti prašomo veiksmo.',
+
+    // Auth
+    'error_user_exists_different_creds' => 'Naudotojo elektroninis paštas :email jau egzistuoja, bet su kitokiais įgaliojimais.',
+    'email_already_confirmed' => 'Elektroninis paštas jau buvo patvirtintas, pabandykite prisijungti.',
+    'email_confirmation_invalid' => 'Šis patvirtinimo prieigos raktas negalioja arba jau buvo panaudotas, prašome bandykite vėl registruotis.',
+    'email_confirmation_expired' => 'Šis patvirtinimo prieigos raktas baigė galioti, naujas patvirtinimo laiškas jau išsiųstas elektroniniu paštu.',
+    'email_confirmation_awaiting' => 'Elektroninio pašto adresą paskyrai reikia patvirtinti',
+    'ldap_fail_anonymous' => 'Nepavyko pasiekti LDAP naudojant anoniminį susiejimą',
+    'ldap_fail_authed' => 'Nepavyko pasiekti LDAP naudojant išsamią dn ir slaptažodžio informaciją',
+    'ldap_extension_not_installed' => 'LDAP PHP išplėtimas neįdiegtas',
+    'ldap_cannot_connect' => 'Negalima prisijungti prie LDAP serverio, nepavyko prisijungti',
+    'saml_already_logged_in' => 'Jau prisijungta',
+    'saml_user_not_registered' => 'Naudotojas :name neužregistruotas ir automatinė registracija yra išjungta',
+    'saml_no_email_address' => 'Nerandamas šio naudotojo elektroninio pašto adresas išorinės autentifikavimo sistemos pateiktuose duomenyse',
+    'saml_invalid_response_id' => 'Prašymas iš išorinės autentifikavimo sistemos nėra atpažintas proceso, kurį pradėjo ši programa. Naršymas po prisijungimo gali sukelti šią problemą.',
+    'saml_fail_authed' => 'Prisijungimas, naudojant :system nepavyko, sistema nepateikė sėkmingo leidimo.',
+    'social_no_action_defined' => 'Neapibrėžtas joks veiksmas',
+    'social_login_bad_response' => "Error received during :socialAccount login: \n:error",
+    'social_account_in_use' => 'Ši :socialAccount paskyra jau yra naudojama, pabandykite prisijungti per :socialAccount pasirinkimą.',
+    'social_account_email_in_use' => 'Elektroninis paštas :email jau yra naudojamas. Jei jūs jau turite paskyrą, galite prijungti savo :socialAccount paskyrą iš savo profilio nustatymų.',
+    'social_account_existing' => 'Šis :socialAccount jau yra pridėtas prie jūsų profilio.',
+    'social_account_already_used_existing' => 'Ši :socialAccount paskyra jau yra naudojama kito naudotojo.',
+    'social_account_not_used' => 'Ši :socialAccount paskyra nėra susieta su jokiais naudotojais. Prašome, pridėkite ją į savo profilio nustatymus.',
+    'social_account_register_instructions' => 'Jei dar neturite paskyros, galite užregistruoti paskyrą, naudojant :socialAccount pasirinkimą.',
+    'social_driver_not_found' => 'Socialinis diskas nerastas',
+    'social_driver_not_configured' => 'Jūsų :socialAccount socaliniai nustatymai sukonfigūruoti neteisingai.',
+    'invite_token_expired' => 'Ši kvietimo nuoroda baigė galioti. Vietoj to, jūs galite bandyti iš naujo nustatyti savo paskyros slaptažodį.',
+
+    // System
+    'path_not_writable' => 'Į failo kelią :filePath negalima įkelti. Įsitikinkite, kad jis yra įrašomas į serverį.',
+    'cannot_get_image_from_url' => 'Negalima gauti vaizdo iš :url',
+    'cannot_create_thumbs' => 'Serveris negali sukurti miniatiūros. Prašome patikrinkite, ar turite įdiegtą GD PHP plėtinį.',
+    'server_upload_limit' => 'Serveris neleidžia įkelti tokio dydžio failų. Prašome bandykite mažesnį failo dydį.',
+    'uploaded'  => 'Serveris neleidžia įkelti tokio dydžio failų. Prašome bandykite mažesnį failo dydį.',
+    'image_upload_error' => 'Įvyko klaida įkeliant vaizdą',
+    'image_upload_type_error' => 'Vaizdo tipas, kurį norima įkelti, yra neteisingas',
+    'file_upload_timeout' => 'Failo įkėlimo laikas baigėsi',
+
+    // Attachments
+    'attachment_not_found' => 'Priedas nerastas',
+
+    // Pages
+    'page_draft_autosave_fail' => 'Juodraščio išsaugoti nepavyko. Įsitikinkite, jog turite interneto ryšį prieš išsaugant šį paslapį.',
+    'page_custom_home_deletion' => 'Negalima ištrinti šio puslapio, kol jis yra nustatytas kaip pagrindinis puslapis',
+
+    // Entities
+    'entity_not_found' => 'Subjektas nerastas',
+    'bookshelf_not_found' => 'Knygų lentyna nerasta',
+    'book_not_found' => 'Knyga nerasta',
+    'page_not_found' => 'Puslapis nerastas',
+    'chapter_not_found' => 'Skyrius nerastas',
+    'selected_book_not_found' => 'Pasirinkta knyga nerasta',
+    'selected_book_chapter_not_found' => 'Pasirinkta knyga ar skyrius buvo nerasti',
+    'guests_cannot_save_drafts' => 'Svečiai negali išsaugoti juodraščių',
+
+    // Users
+    'users_cannot_delete_only_admin' => 'Negalite ištrinti vienintelio administratoriaus',
+    'users_cannot_delete_guest' => 'Negalite ištrinti svečio naudotojo',
+
+    // Roles
+    'role_cannot_be_edited' => 'Šio vaidmens negalima redaguoti',
+    'role_system_cannot_be_deleted' => 'Šis vaidmuo yra sistemos vaidmuo ir jo negalima ištrinti',
+    'role_registration_default_cannot_delete' => 'Šis vaidmuo negali būti ištrintas, kai yra nustatytas kaip numatytasis registracijos vaidmuo',
+    'role_cannot_remove_only_admin' => 'Šis naudotojas yra vienintelis naudotojas, kuriam yra paskirtas administratoriaus vaidmuo. Paskirkite administratoriaus vaidmenį kitam naudotojui prieš bandant jį pašalinti.',
+
+    // Comments
+    'comment_list' => 'Gaunant komentarus įvyko klaida.',
+    'cannot_add_comment_to_draft' => 'Negalite pridėti komentaro juodraštyje',
+    'comment_add' => 'Klaido įvyko pridedant/atnaujinant komantarą.',
+    'comment_delete' => 'Trinant komentarą įvyko klaida.',
+    'empty_comment' => 'Negalite pridėti tuščio komentaro.',
+
+    // Error pages
+    '404_page_not_found' => 'Puslapis nerastas',
+    'sorry_page_not_found' => 'Atleiskite, puslapis, kurio ieškote, nerastas.',
+    'sorry_page_not_found_permission_warning' => 'Jei tikėjotės, kad šis puslapis egzistuoja, galbūt neturite leidimo jo peržiūrėti.',
+    'image_not_found' => 'Image Not Found',
+    'image_not_found_subtitle' => 'Sorry, The image file you were looking for could not be found.',
+    'image_not_found_details' => 'If you expected this image to exist it might have been deleted.',
+    'return_home' => 'Grįžti į namus',
+    'error_occurred' => 'Įvyko klaida',
+    'app_down' => ':appName dabar yra apačioje',
+    'back_soon' => 'Tai sugrįž greitai',
+
+    // API errors
+    'api_no_authorization_found' => 'Užklausoje nerastas įgaliojimo prieigos raktas',
+    'api_bad_authorization_format' => 'Užklausoje rastas prieigos raktas, tačiau formatas yra neteisingas',
+    'api_user_token_not_found' => 'Pateiktam prieigos raktui nebuvo rastas atitinkamas API prieigos raktas',
+    'api_incorrect_token_secret' => 'Pateiktas panaudoto API žetono slėpinys yra neteisingas',
+    'api_user_no_api_permission' => 'API prieigos rakto savininkas neturi leidimo daryti API skambučius',
+    'api_user_token_expired' => 'Prieigos rakto naudojimas baigė galioti',
+
+    // Settings & Maintenance
+    'maintenance_test_email_failure' => 'Siunčiant bandymo email: įvyko klaida',
+
+];
diff --git a/resources/lang/lt/pagination.php b/resources/lang/lt/pagination.php
new file mode 100644 (file)
index 0000000..f962f12
--- /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; Ankstesnis',
+    'next'     => 'Kitas &raquo;',
+
+];
diff --git a/resources/lang/lt/passwords.php b/resources/lang/lt/passwords.php
new file mode 100644 (file)
index 0000000..672620d
--- /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' => 'Slaptažodis privalo būti mažiausiai aštuonių simbolių ir atitikti patvirtinimą.',
+    'user' => "We can't find a user with that e-mail address.",
+    'token' => 'Slaptažodžio nustatymo raktas yra neteisingas šiam elektroninio pašto adresui.',
+    'sent' => 'Elektroniu paštu jums atsiuntėme slaptažodžio atkūrimo nuorodą!',
+    'reset' => 'Jūsų slaptažodis buvo atkurtas!',
+
+];
diff --git a/resources/lang/lt/settings.php b/resources/lang/lt/settings.php
new file mode 100644 (file)
index 0000000..f5795ed
--- /dev/null
@@ -0,0 +1,277 @@
+<?php
+/**
+ * Settings text strings
+ * Contains all text strings used in the general settings sections of BookStack
+ * including users and roles.
+ */
+return [
+
+    // Common Messages
+    'settings' => 'Nustatymai',
+    'settings_save' => 'Išsaugoti nustatymus',
+    'settings_save_success' => 'Nustatymai išsaugoti',
+
+    // App Settings
+    'app_customization' => 'Tinkinimas',
+    'app_features_security' => 'Funkcijos ir sauga',
+    'app_name' => 'Programos pavadinimas',
+    'app_name_desc' => 'Šis pavadinimas yra rodomas antraštėje ir bet kuriuose sistemos siunčiamuose elektroniniuose laiškuose.',
+    'app_name_header' => 'Rodyti pavadinimą antraštėje',
+    'app_public_access' => 'Vieša prieiga',
+    'app_public_access_desc' => 'Įjungus šią parinktį lankytojai, kurie nėra prisijungę, galės pasiekti BookStack egzemplioriaus turinį.',
+    'app_public_access_desc_guest' => 'Prieiga viešiems lankytojams gali būti kontroliuojama per "Svečio" naudotoją.',
+    'app_public_access_toggle' => 'Leisti viešą prieigą',
+    'app_public_viewing' => 'Leisti viešą žiūrėjimą?',
+    'app_secure_images' => 'Didesnio saugumo vaizdų įkėlimai',
+    'app_secure_images_toggle' => 'Įgalinti didesnio saugumo vaizdų įkėlimus',
+    'app_secure_images_desc' => 'Dėl veiklos priežasčių, visi vaizdai yra vieši. Šis pasirinkimas prideda atsitiktinę, sunkiai atspėjamą eilutę prieš vaizdo URL. Įsitikinkite, kad katalogų rodyklės neįgalintos, kad prieiga būtų lengvesnė.',
+    'app_editor' => 'Puslapio redaktorius',
+    'app_editor_desc' => 'Pasirinkite, kuris redaktorius bus naudojamas visų vartotojų redaguoti puslapiams.',
+    'app_custom_html' => 'Pasirinktinis HTL antraštės turinys',
+    'app_custom_html_desc' => 'Bet koks čia pridedamas turinys bus prisegamas apačioje <antraštės> kiekvieno puslapio skyriuje. Tai yra patogu svarbesniems stiliams arba pridedant analizės kodą.',
+    'app_custom_html_disabled_notice' => 'Pasirinktinis HTML antraštės turinys yra išjungtas šiame nustatymų puslapyje užtikrinti, kad bet kokie negeri pokyčiai galėtų būti anuliuojami.',
+    'app_logo' => 'Programos logotipas',
+    'app_logo_desc' => 'Šis vaizdas turėtų būti 43px aukščio. <br> Dideli vaizdai bus sumažinti.',
+    'app_primary_color' => 'Programos pagrindinė spalva',
+    'app_primary_color_desc' => 'Nustato pagrindinę spalvą programai, įskaitant reklamjuostę, mygtukus ir nuorodas.',
+    'app_homepage' => 'Programos pagrindinis puslapis',
+    'app_homepage_desc' => 'Pasirinkite vaizdą rodyti pagrindiniame paslapyje vietoj numatyto vaizdo. Puslapio leidimai yra ignoruojami pasirinktiems puslapiams.',
+    'app_homepage_select' => 'Pasirinkti puslapį',
+    'app_footer_links' => 'Poraštės nuorodos',
+    'app_footer_links_desc' => 'Pridėkite nuorodas, kurias norite pridėti svetainės poraštėje. Jos bus rodomos daugelio puslapių apačioje, įskaitant ir tuos, kurie nereikalauja prisijungimo. Jūs galite naudoti etiktę "trans::<key>", kad naudotis sistemos apibrėžtais vertimais. Pavyzdžiui: naudojimasis "trans::common.privacy_policy" bus pateiktas išverstu tekstu "Privatumo Politika" ir ""trans::common.terms_of_service" bus pateikta išverstu tekstu "Paslaugų Teikimo Sąlygos".',
+    'app_footer_links_label' => 'Etiketės nuoroda',
+    'app_footer_links_url' => 'Nuoroda URL',
+    'app_footer_links_add' => 'Pridėti poraštes nuorodą',
+    'app_disable_comments' => 'Išjungti komentarus',
+    'app_disable_comments_toggle' => 'Išjungti komentarus',
+    'app_disable_comments_desc' => 'Išjungti komentarus visuose programos puslapiuose. <br> Esantys komentarai nerodomi.',
+
+    // Color settings
+    'content_colors' => 'Turinio spalvos',
+    'content_colors_desc' => 'Nustato spalvas visiems elementams puslapio organizacijos herarchijoje. Rekomenduojama pasirinkti spalvas su panačiu šviesumu kaip numatytos spalvos, kad būtų lengviau skaityti.',
+    'bookshelf_color' => 'Lentynos spalva',
+    'book_color' => 'Knygos spalva',
+    'chapter_color' => 'Skyriaus spalva',
+    'page_color' => 'Puslapio spalva',
+    'page_draft_color' => 'Puslapio juodraščio spalva',
+
+    // Registration Settings
+    'reg_settings' => 'Registracija',
+    'reg_enable' => 'Įgalinti registraciją',
+    'reg_enable_toggle' => 'Įgalinti registraciją',
+    'reg_enable_desc' => 'Kai registracija yra įgalinta, naudotojai gali prisiregistruoti kaip programos naudotojai. Registruojantis jiems suteikiamas vienintelis, nematytasis naudotojo vaidmuo.',
+    'reg_default_role' => 'Numatytasis naudotojo vaidmuo po registracijos',
+    'reg_enable_external_warning' => 'Ankstesnė parinktis nepaisoma, kai išorinis LDAP arba SAML autentifikavimas yra aktyvus. Vartotojo paskyra neegzistuojantiems nariams bus automatiškai sukurta, jei autentifikavimas naudojant naudojamą išorinę sistemą bus sėkmingas.',
+    'reg_email_confirmation' => 'Elektroninio pašto patvirtinimas',
+    'reg_email_confirmation_toggle' => 'Reikalauja elektroninio pašto patvirtinimo',
+    'reg_confirm_email_desc' => 'Jei naudojamas domeno apribojimas, tada elektroninio pašto patvirtinimas bus reikalaujamas ir ši parinktis bus ignoruojama.',
+    'reg_confirm_restrict_domain' => 'Domeno apribojimas',
+    'reg_confirm_restrict_domain_desc' => 'Įveskite kableliais atskirtą elektroninio pašto domenų, kurių registravimą norite apriboti, sąrašą. Vartotojai išsiųs elektorinį laišką, kad patvirtintumėte jų adresą prieš leidžiant naudotis programa. <br> Prisiminkite, kad vartotojai galės pakeisti savo elektroninius paštus po sėkmingos registracijos.',
+    'reg_confirm_restrict_domain_placeholder' => 'Nėra jokių apribojimų',
+
+    // Maintenance settings
+    'maint' => 'Priežiūra',
+    'maint_image_cleanup' => 'Išvalykite vaizdus',
+    '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_delete_images_only_in_revisions' => 'Taip pat ištrinkite vaizdus, kurie yra tik senuose puslapių pataisymuose',
+    'maint_image_cleanup_run' => 'Paleisti valymą',
+    'maint_image_cleanup_warning' => ':count potencialiai nepanaudoti vaizdai rasti. Ar esate tikri, kad norite ištrinti šiuos vaizdus?',
+    'maint_image_cleanup_success' => ':count potencialiai nepanaudoti vaizdai rasti ir ištrinti!',
+    'maint_image_cleanup_nothing_found' => 'Nerasta nepanaudotų vaizdų, niekas neištrinta!',
+    'maint_send_test_email' => 'Siųsti bandomąjį elektroninį laišką',
+    'maint_send_test_email_desc' => 'ai siunčia bandomąjį elektroninį laišką elektroninio pašto adresu, nurodytu jūsų profilyje.',
+    'maint_send_test_email_run' => 'Siųsti bandomąjį elektroninį laišką',
+    'maint_send_test_email_success' => 'Elektroninis laiškas išsiųstas :address',
+    'maint_send_test_email_mail_subject' => 'Bandomasis elektroninis laiškas',
+    'maint_send_test_email_mail_greeting' => 'Elektroninio laiško pristatymas veikia!',
+    'maint_send_test_email_mail_text' => 'Sveikiname! Kadangi gavote šį elektroninio pašto pranešimą, jūsų elektroninio pašto nustatymai buvo sukonfigūruoti teisingai.',
+    'maint_recycle_bin_desc' => 'Ištrintos lentynos, knygos, skyriai ir puslapiai yra perkeliami į šiukšliadėžę tam, kad jie galėtų būti atkurti arba ištrinti visam laikui. Senesni elementai, esantys šiukšliadėžėje, gali būti automatiškai panaikinti po tam tikro laiko priklausomai nuo sistemos konfigūracijos.',
+    'maint_recycle_bin_open' => 'Atidaryti šiukšliadėžę',
+
+    // Recycle Bin
+    'recycle_bin' => 'Šiukšliadėžė',
+    'recycle_bin_desc' => 'Čia gali atkurti elementus, kurie buvo ištrinti arba pasirinkti pašalinti juos iš sistemos visam laikui. Šis sąrašas yra nefiltruotas kaip kitie panašus veiklos sąrašai sistemoje, kuriems yra taikomi leidimo filtrai.',
+    'recycle_bin_deleted_item' => 'Ištrintas elementas',
+    'recycle_bin_deleted_parent' => 'Parent',
+    'recycle_bin_deleted_by' => 'Ištrynė',
+    'recycle_bin_deleted_at' => 'Panaikinimo laikas',
+    'recycle_bin_permanently_delete' => 'Ištrinti visam laikui',
+    'recycle_bin_restore' => 'Atkurti',
+    'recycle_bin_contents_empty' => 'Šiukšliadėžė šiuo metu yra tuščia',
+    'recycle_bin_empty' => 'Ištuštinti šiukšliadėžę',
+    'recycle_bin_empty_confirm' => 'Tai visam laikui sunaikins visus elementus, esančius šiukšliadėžėje, įskaitant kiekvieno elemento turinį. Ar esate tikri, jog norite ištuštinti šiukšliadėžę?',
+    'recycle_bin_destroy_confirm' => 'Šis veiksmas visam laikui ištrins šį elementą iš sistemos kartu su bet kuriais elementais įvardintais žemiau ir jūs nebegalėsite atkurti jo bei jo turinio. Ar esate tikri, jog norite visam laikui ištrinti šį elementą?',
+    'recycle_bin_destroy_list' => 'Elementai panaikinimui',
+    'recycle_bin_restore_list' => 'Elementai atkūrimui',
+    'recycle_bin_restore_confirm' => 'Šis veiksmas atkurs ištrintą elementą ir perkels jį atgal į jo originalią vietą. Jei originali vieta buvo ištrinta ir šiuo metu yra šiukšliadėžėje, ji taip pat turės būti atkurta.',
+    'recycle_bin_restore_deleted_parent' => 'Pagrindinis elementas buvo ištrintas. Šie elementai liks ištrinti iki tol, kol bus atkurtas pagrindinis elementas.',
+    'recycle_bin_restore_parent' => 'Restore Parent',
+    'recycle_bin_destroy_notification' => 'Ištrinti :count visus elementus, esančius šiukšliadėžėje.',
+    'recycle_bin_restore_notification' => 'Atkurti :count visus elementus, esančius šiukšliadėžėje.',
+
+    // Audit Log
+    'audit' => 'Audito seka',
+    'audit_desc' => 'Ši audito seka rodo sąrašą veiklų, rastų sistemoje. Šis sąrašas yra nefiltruotas kaip kitie panašus veiklos sąrašai sistemoje, kuriems yra taikomi leidimo filtrai.',
+    'audit_event_filter' => 'Įvykio filtras',
+    'audit_event_filter_no_filter' => 'Be filtrų',
+    'audit_deleted_item' => 'Ištrintas elementas',
+    'audit_deleted_item_name' => 'Vardas: :name',
+    'audit_table_user' => 'Naudotojas',
+    'audit_table_event' => 'Įvykis',
+    'audit_table_related' => 'Susijęs elementas arba detalė',
+    'audit_table_ip' => 'IP Address',
+    'audit_table_date' => 'Veiklos data',
+    'audit_date_from' => 'Datos seka nuo',
+    'audit_date_to' => 'Datos seka iki',
+
+    // Role Settings
+    'roles' => 'Vaidmenys',
+    'role_user_roles' => 'Naudotojo vaidmenys',
+    'role_create' => 'Sukurti naują vaidmenį',
+    'role_create_success' => 'Vaidmuo sukurtas sėkmingai',
+    'role_delete' => 'Ištrinti vaidmenį',
+    'role_delete_confirm' => 'Tai ištrins vaidmenį vardu\':roleName\'.',
+    'role_delete_users_assigned' => 'Šis vaidmuo turi :userCount naudotojus priskirtus prie jo. Jeigu norite naudotojus perkelti iš šio vaidmens, pasirinkite naują vaidmenį apačioje.',
+    'role_delete_no_migration' => "Don't migrate users",
+    'role_delete_sure' => 'Ar esate tikri, jog norite ištrinti šį vaidmenį?',
+    'role_delete_success' => 'Vaidmuo ištrintas sėkmingai',
+    'role_edit' => 'Redaguoti vaidmenį',
+    'role_details' => 'Vaidmens detalės',
+    'role_name' => 'Vaidmens pavadinimas',
+    'role_desc' => 'Trumpas vaidmens aprašymas',
+    'role_mfa_enforced' => 'Requires Multi-Factor Authentication',
+    'role_external_auth_id' => 'Išorinio autentifikavimo ID',
+    'role_system' => 'Sistemos leidimai',
+    'role_manage_users' => 'Tvarkyti naudotojus',
+    'role_manage_roles' => 'Tvarkyti vaidmenis ir vaidmenų leidimus',
+    'role_manage_entity_permissions' => 'Tvarkyti visus knygų, skyrių ir puslapių leidimus',
+    'role_manage_own_entity_permissions' => 'Tvarkyti savo knygos, skyriaus ir puslapių leidimus',
+    'role_manage_page_templates' => 'Tvarkyti puslapių šablonus',
+    'role_access_api' => 'Gauti prieigą prie sistemos API',
+    'role_manage_settings' => 'Tvarkyti programos nustatymus',
+    'role_export_content' => 'Export content',
+    'role_asset' => 'Nuosavybės leidimai',
+    'roles_system_warning' => 'Būkite sąmoningi, kad prieiga prie bet kurio iš trijų leidimų viršuje gali leisti naudotojui pakeisti jų pačių privilegijas arba kitų privilegijas sistemoje. Paskirkite vaidmenis su šiais leidimais tik patikimiems naudotojams.',
+    'role_asset_desc' => 'Šie leidimai kontroliuoja numatytą prieigą į nuosavybę, esančią sistemoje. Knygų, skyrių ir puslapių leidimai nepaisys šių leidimų.',
+    'role_asset_admins' => 'Administratoriams automatiškai yra suteikiama prieiga prie viso turinio, tačiau šie pasirinkimai gali rodyti arba slėpti vartotojo sąsajos parinktis.',
+    'role_all' => 'Visi',
+    'role_own' => 'Nuosavi',
+    'role_controlled_by_asset' => 'Kontroliuojami nuosavybės, į kurią yra įkelti',
+    'role_save' => 'Išsaugoti vaidmenį',
+    'role_update_success' => 'Vaidmuo atnaujintas sėkmingai',
+    'role_users' => 'Naudotojai šiame vaidmenyje',
+    'role_users_none' => 'Šiuo metu prie šio vaidmens nėra priskirta naudotojų',
+
+    // Users
+    'users' => 'Naudotojai',
+    'user_profile' => 'Naudotojo profilis',
+    'users_add_new' => 'Pridėti naują naudotoją',
+    'users_search' => 'Ieškoti naudotojų',
+    'users_latest_activity' => 'Naujausia veikla',
+    'users_details' => 'Naudotojo detalės',
+    'users_details_desc' => 'Nustatykite rodomąjį vardą ir elektroninio pašto adresą šiam naudotojui. Šis elektroninio pašto adresas bus naudojamas prisijungimui prie aplikacijos.',
+    'users_details_desc_no_email' => 'Nustatykite rodomąjį vardą šiam naudotojui, kad kiti galėtų jį atpažinti.',
+    'users_role' => 'Naudotojo vaidmenys',
+    'users_role_desc' => 'Pasirinkite, prie kokių vaidmenų bus priskirtas šis naudotojas. Jeigu naudotojas yra priskirtas prie kelių vaidmenų, leidimai iš tų vaidmenų susidės ir jie gaus visus priskirtų vaidmenų gebėjimus.',
+    'users_password' => 'Naudotojo slaptažodis',
+    'users_password_desc' => 'Susikurkite slaptažodį, kuris bus naudojamas prisijungti prie aplikacijos. Slaptažodis turi būti bent 6 simbolių ilgio.',
+    'users_send_invite_text' => 'Jūs galite pasirinkti nusiųsti šiam naudotojui kvietimą elektroniniu paštu, kuris leistų jiems patiems susikurti slaptažodį. Priešingu atveju slaptažodį galite sukurti patys.',
+    'users_send_invite_option' => 'Nusiųsti naudotojui kvietimą elektroniniu paštu',
+    'users_external_auth_id' => 'Išorinio autentifikavimo ID',
+    'users_external_auth_id_desc' => 'Tai yra ID, naudojamas norint suderinti šį naudotoją bendraujant su jūsų išorinio autentifikavimo sistema.',
+    'users_password_warning' => 'Užpildykite laukelį apačioje tik tuo atveju, jeigu norite pakeisti savo slaptažodį.',
+    'users_system_public' => 'Šis naudotojas atstovauja svečius, kurie aplanko jūsų egzempliorių. Jis negali būti naudojamas prisijungimui, tačiau yra priskiriamas automatiškai.',
+    'users_delete' => 'Ištrinti naudotoją',
+    'users_delete_named' => 'Ištrinti naudotoją :userName',
+    'users_delete_warning' => 'Tai pilnai ištrins šį naudotoją vardu \':userName\' iš sistemos.',
+    'users_delete_confirm' => 'Ar esate tikri, jog norite ištrinti šį naudotoją?',
+    'users_migrate_ownership' => 'Perkelti nuosavybę',
+    'users_migrate_ownership_desc' => 'Pasirinkite naudotoją, jeigu norite, kad kitas naudotojas taptų visų elementų, šiuo metu priklausančių šiam naudotojui, savininku.',
+    'users_none_selected' => 'Naudotojas nepasirinktas',
+    'users_delete_success' => 'Naudotojas sėkmingai pašalintas',
+    'users_edit' => 'Redaguoti naudotoją',
+    'users_edit_profile' => 'Redaguoti profilį',
+    'users_edit_success' => 'Naudotojas sėkmingai atnaujintas',
+    'users_avatar' => 'Naudotojo pseudoportretas',
+    'users_avatar_desc' => 'Pasirinkite nuotrauką, pavaizduojančią šį naudotoją. Nuotrauka turi būti maždaug 256px kvadratas.',
+    'users_preferred_language' => 'Norima kalba',
+    'users_preferred_language_desc' => 'Ši parinktis pakeis kalbą, naudojamą naudotojo sąsajoje aplikacijoje. Tai neturės įtakos jokiam vartotojo sukurtam turiniui.',
+    'users_social_accounts' => 'Socialinės paskyros',
+    'users_social_accounts_info' => 'Čia galite susieti savo kitas paskyras greitesniam ir lengvesniam prisijungimui. Atjungus paskyrą čia neatšaukiama anksčiau leista prieiga. Atšaukite prieigą iš profilio nustatymų prijungtoje socialinėje paskyroje.',
+    'users_social_connect' => 'Susieti paskyrą',
+    'users_social_disconnect' => 'Atskirti paskyrą',
+    'users_social_connected' => ':socialAccount paskyra buvo sėkmingai susieta su jūsų profiliu.',
+    'users_social_disconnected' => ':socialAccount paskyra buvo sėkmingai atskirta nuo jūsu profilio.',
+    'users_api_tokens' => 'API sąsajos prieigos raktai',
+    'users_api_tokens_none' => 'Jokie API sąsajos prieigos raktai nebuvo sukurti šiam naudotojui',
+    'users_api_tokens_create' => 'Sukurti prieigos raktą',
+    'users_api_tokens_expires' => 'Baigia galioti',
+    'users_api_tokens_docs' => 'API dokumentacija',
+    'users_mfa' => 'Multi-Factor Authentication',
+    'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
+    'users_mfa_x_methods' => ':count method configured|:count methods configured',
+    'users_mfa_configure' => 'Configure Methods',
+
+    // API Tokens
+    'user_api_token_create' => 'Sukurti API sąsajos prieigos raktą',
+    'user_api_token_name' => 'Pavadinimas',
+    'user_api_token_name_desc' => 'Suteikite savo prieigos raktui perskaitomą pavadinimą kaip priminimą ateičiai apie jo numatytą tikslą.',
+    'user_api_token_expiry' => 'Galiojimo laikas',
+    'user_api_token_expiry_desc' => 'Nustatykite datą kada šis prieigos raktas baigs galioti. Po šios datos, prašymai, atlikti naudojant šį prieigos raktą daugiau nebeveiks. Jeigu šį laukelį paliksite tuščią, galiojimo laikas bus nustatytas 100 metų į ateitį.',
+    'user_api_token_create_secret_message' => 'Iš karto sukūrus šį prieigos raktą, bus sukurtas ir rodomas "Priegos rakto ID" ir "Prieigos rakto slėpinys". Prieigos rakto slėpinys bus rodomas tik vieną kartą, todėl būtinai nukopijuokite jį kur nors saugioje vietoje.',
+    'user_api_token_create_success' => 'API sąsajos prieigos raktas sėkmingai sukurtas',
+    'user_api_token_update_success' => 'API sąsajos prieigos raktas sėkmingai atnaujintas',
+    'user_api_token' => 'API sąsajos prieigos raktas',
+    'user_api_token_id' => 'Prieigos rakto ID',
+    'user_api_token_id_desc' => 'Tai neredaguojamas sistemos sugeneruotas identifikatorius šiam prieigos raktui, kurį reikės pateikti API užklausose.',
+    'user_api_token_secret' => 'Priegos rakto slėpinys',
+    'user_api_token_secret_desc' => 'Tai yra sistemos sukurtas šio priegos rakto slėpinys, kurią reikės pateikti API užklausose. Tai bus rodoma tik šį kartą, todėl nukopijuokite šią vertę į saugią vietą.',
+    'user_api_token_created' => 'Prieigos raktas sukurtas :timeAgo',
+    'user_api_token_updated' => 'Prieigos raktas atnaujintas :timeAgo',
+    'user_api_token_delete' => 'Ištrinti prieigos raktą',
+    'user_api_token_delete_warning' => 'Tai pilnai ištrins šį API sąsajos prieigos raktą pavadinimu \':tokenName\' iš sistemos.',
+    'user_api_token_delete_confirm' => 'Ar esate tikri, jog norite ištrinti šį API sąsajos prieigos raktą?',
+    'user_api_token_delete_success' => 'API sąsajos prieigos raktas sėkmingai ištrintas',
+
+    //! 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',
+        'bs' => 'Bosanski',
+        'ca' => 'Català',
+        'cs' => 'Česky',
+        'da' => 'Dansk',
+        'de' => 'Deutsch (Sie)',
+        'de_informal' => 'Deutsch (Du)',
+        'es' => 'Español',
+        'es_AR' => 'Español Argentina',
+        'fr' => 'Français',
+        'he' => 'עברית',
+        'hr' => 'Hrvatski',
+        'hu' => 'Magyar',
+        'id' => 'Bahasa Indonesia',
+        'it' => 'Italian',
+        'ja' => '日本語',
+        'ko' => '한국어',
+        'lt' => 'Lietuvių Kalba',
+        'lv' => 'Latviešu Valoda',
+        'nl' => 'Nederlands',
+        'nb' => 'Norsk (Bokmål)',
+        'pl' => 'Polski',
+        'pt' => 'Português',
+        '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/lt/validation.php b/resources/lang/lt/validation.php
new file mode 100644 (file)
index 0000000..8fa9234
--- /dev/null
@@ -0,0 +1,116 @@
+<?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 turi būti priimtas.',
+    'active_url'           => ':attribute nėra tinkamas URL.',
+    'after'                => ':attribute turi būti data po :date.',
+    'alpha'                => ':attribute turi būti sudarytis tik iš raidžių.',
+    'alpha_dash'           => ':attribute turi būti sudarytas tik iš raidžių, skaičių, brūkšnelių ir pabraukimų.',
+    'alpha_num'            => ':attribute turi būti sudarytas tik iš raidžių ir skaičių.',
+    'array'                => ':attribute turi būti masyvas.',
+    'backup_codes'         => 'The provided code is not valid or has already been used.',
+    'before'               => ':attribute turi būti data anksčiau negu :date.',
+    'between'              => [
+        'numeric' => ':attribute turi būti tarp :min ir :max.',
+        'file'    => ':attribute turi būti tarp :min ir :max kilobaitų.',
+        'string'  => ':attribute turi būti tarp :min ir :max simbolių.',
+        'array'   => ':attribute turi turėti tarp :min ir :max elementų.',
+    ],
+    'boolean'              => ':attribute laukas turi būti tiesa arba melas.',
+    'confirmed'            => ':attribute patvirtinimas nesutampa.',
+    'date'                 => ':attribute nėra tinkama data.',
+    'date_format'          => ':attribute neatitinka formato :format.',
+    'different'            => ':attribute ir :other turi būti skirtingi.',
+    'digits'               => ':attribute turi būti :digits skaitmenų.',
+    'digits_between'       => ':attribute turi būti tarp :min ir :max skaitmenų.',
+    'email'                => ':attribute turi būti tinkamas elektroninio pašto adresas.',
+    'ends_with' => ':attribute turi pasibaigti vienu iš šių: :values',
+    'filled'               => ':attribute laukas yra privalomas.',
+    'gt'                   => [
+        'numeric' => ':attribute turi būti didesnis negu :value.',
+        'file'    => ':attribute turi būti didesnis negu :value kilobaitai.',
+        'string'  => ':attribute turi būti didesnis negu :value simboliai.',
+        'array'   => ':attribute turi turėti daugiau negu :value elementus.',
+    ],
+    'gte'                  => [
+        'numeric' => ':attribute turi būti didesnis negu arba lygus :value.',
+        'file'    => ':attribute turi būti didesnis negu arba lygus :value kilobaitams.',
+        'string'  => ':attribute turi būti didesnis negu arba lygus :value simboliams.',
+        'array'   => ':attribute turi turėti :value elementus arba daugiau.',
+    ],
+    'exists'               => 'Pasirinktas :attribute yra klaidingas.',
+    'image'                => ':attribute turi būti paveikslėlis.',
+    'image_extension'      => ':attribute turi būti tinkamas ir palaikomas vaizdo plėtinys.',
+    'in'                   => 'Pasirinktas :attribute yra klaidingas.',
+    'integer'              => ':attribute turi būti sveikasis skaičius.',
+    'ip'                   => ':attribute turi būti tinkamas IP adresas.',
+    'ipv4'                 => ':attribute turi būti tinkamas IPv4 adresas.',
+    'ipv6'                 => ':attribute turi būti tinkamas IPv6 adresas.',
+    'json'                 => ':attribute turi būti tinkama JSON eilutė.',
+    'lt'                   => [
+        'numeric' => ':attribute turi būti mažiau negu :value.',
+        'file'    => ':attribute turi būti mažiau negu :value kilobaitai.',
+        'string'  => ':attribute turi būti mažiau negu :value simboliai.',
+        'array'   => ':attribute turi turėti mažiau negu :value elementus.',
+    ],
+    'lte'                  => [
+        'numeric' => ':attribute turi būti mažiau arba lygus :value.',
+        'file'    => ':attribute turi būti mažiau arba lygus :value kilobaitams.',
+        'string'  => ':attribute turi būti mažiau arba lygus :value simboliams.',
+        'array'   => ':attribute negali turėti daugiau negu :value elementų.',
+    ],
+    'max'                  => [
+        'numeric' => ':attribute negali būti didesnis negu :max.',
+        'file'    => ':attribute negali būti didesnis negu :max kilobaitai.',
+        'string'  => ':attribute negali būti didesnis negu :max simboliai.',
+        'array'   => ':attribute negali turėti daugiau negu :max elementų.',
+    ],
+    'mimes'                => ':attribute turi būti tipo failas: :values.',
+    'min'                  => [
+        'numeric' => ':attribute turi būti mažiausiai :min.',
+        'file'    => ':attribute turi būti mažiausiai :min kilobaitų.',
+        'string'  => ':attribute turi būti mažiausiai :min simbolių.',
+        'array'   => ':attribute turi turėti mažiausiai :min elementus.',
+    ],
+    'not_in'               => 'Pasirinktas :attribute yra klaidingas.',
+    'not_regex'            => ':attribute formatas yra klaidingas.',
+    'numeric'              => ':attribute turi būti skaičius.',
+    'regex'                => ':attribute formatas yra klaidingas.',
+    'required'             => ':attribute laukas yra privalomas.',
+    'required_if'          => ':attribute laukas yra privalomas kai :other yra :value.',
+    'required_with'        => ':attribute laukas yra privalomas kai :values yra.',
+    'required_with_all'    => ':attribute laukas yra privalomas kai :values yra.',
+    'required_without'     => ':attribute laukas yra privalomas kai nėra :values.',
+    'required_without_all' => ':attribute laukas yra privalomas kai nėra nei vienos :values.',
+    'same'                 => ':attribute ir :other turi sutapti.',
+    'safe_url'             => 'Pateikta nuoroda gali būti nesaugi.',
+    'size'                 => [
+        'numeric' => ':attribute turi būti :size.',
+        'file'    => ':attribute turi būti :size kilobaitų.',
+        'string'  => ':attribute turi būti :size simbolių.',
+        'array'   => ':attribute turi turėti :size elementus.',
+    ],
+    'string'               => ':attribute turi būti eilutė.',
+    'timezone'             => ':attribute turi būti tinkama zona.',
+    'totp'                 => 'The provided code is not valid or has expired.',
+    'unique'               => ':attribute jau yra paimtas.',
+    'url'                  => ':attribute formatas yra klaidingas.',
+    'uploaded'             => 'Šis failas negali būti įkeltas. Serveris gali nepriimti tokio dydžio failų.',
+
+    // Custom validation lines
+    'custom' => [
+        'password-confirm' => [
+            'required_with' => 'Reikalingas slaptažodžio patvirtinimas',
+        ],
+    ],
+
+    // Custom validation attributes
+    'attributes' => [],
+];
index e424efa1d1b92845a7bd4e32e6dc05e32e464f2d..fc2d876e58c68affea87f8f646cf826ce172b145 100644 (file)
@@ -47,6 +47,10 @@ return [
     'favourite_add_notification' => '":name" ir pievienots jūsu favorītiem',
     'favourite_remove_notification' => '":name" ir izņemts no jūsu favorītiem',
 
+    // MFA
+    'mfa_setup_method_notification' => '2FA funkcija aktivizēta',
+    'mfa_remove_method_notification' => '2FA funkcija noņemta',
+
     // Other
     'commented_on'                => 'komentēts',
     'permissions_update'          => 'atjaunoja atļaujas',
index dc84a2d978040d24c332247fefb5b7d78bdf0cfe..497509d54cd6061952feca8d79408aa9d056b131 100644 (file)
@@ -73,5 +73,40 @@ return [
     'user_invite_page_welcome' => 'Sveicināti :appName!',
     'user_invite_page_text' => 'Lai pabeigtu profila izveidi un piekļūtu :appName ir jāizveido parole.',
     'user_invite_page_confirm_button' => 'Apstiprināt paroli',
-    'user_invite_success' => 'Parole iestatīta, tagad varat piekļūt :appName!'
+    'user_invite_success' => 'Parole iestatīta, tagad varat piekļūt :appName!',
+
+    // Multi-factor Authentication
+    'mfa_setup' => 'Iestati divfaktoru autentifikāciju (2FA)',
+    'mfa_setup_desc' => 'Iestati divfaktoru autentifikāciju kā papildus drošību tavam lietotāja kontam.',
+    'mfa_setup_configured' => 'Divfaktoru autentifikācija jau ir nokonfigurēta',
+    'mfa_setup_reconfigure' => 'Mainīt 2FA konfigurāciju',
+    'mfa_setup_remove_confirmation' => 'Vai esi drošs, ka vēlies noņemt divfaktoru autentifikāciju?',
+    'mfa_setup_action' => 'Setup',
+    'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.',
+    'mfa_option_totp_title' => 'Mobile App',
+    'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
+    'mfa_option_backup_codes_title' => 'Backup Codes',
+    'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.',
+    'mfa_gen_confirm_and_enable' => 'Confirm and Enable',
+    'mfa_gen_backup_codes_title' => 'Backup Codes Setup',
+    'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.',
+    'mfa_gen_backup_codes_download' => 'Download Codes',
+    'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once',
+    'mfa_gen_totp_title' => 'Mobile App Setup',
+    'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
+    'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.',
+    'mfa_gen_totp_verify_setup' => 'Verify Setup',
+    'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:',
+    'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here',
+    'mfa_verify_access' => 'Verify Access',
+    'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.',
+    'mfa_verify_no_methods' => 'No Methods Configured',
+    'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.',
+    'mfa_verify_use_totp' => 'Verify using a mobile app',
+    'mfa_verify_use_backup_codes' => 'Verify using a backup code',
+    'mfa_verify_backup_code' => 'Backup Code',
+    'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:',
+    'mfa_verify_backup_code_enter_here' => 'Enter backup code here',
+    'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:',
+    'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.',
 ];
\ No newline at end of file
index f99419c93e96767babbebcf442858fb4c0dcd15e..23cd07d7d1a264ad6b5ca88cc2fefbb259e77a40 100644 (file)
@@ -39,6 +39,7 @@ return [
     'reset' => 'Atiestatīt',
     'remove' => 'Noņemt',
     'add' => 'Pievienot',
+    'configure' => 'Mainīt konfigurāciju',
     'fullscreen' => 'Pilnekrāns',
     'favourite' => 'Pievienot favorītiem',
     'unfavourite' => 'Noņemt no favorītiem',
@@ -60,6 +61,7 @@ return [
     'no_activity' => 'Nav skatāmu darbību',
     'no_items' => 'Vienumi nav pieejami',
     'back_to_top' => 'Uz augšu',
+    'skip_to_main_content' => 'Pāriet uz saturu',
     'toggle_details' => 'Rādīt aprakstu',
     'toggle_thumbnails' => 'Iezīmēt sīkatēlus',
     'details' => 'Sīkāka informācija',
index 0a01b9bf32522e2443b780cc450aca09f63ac615..a28829f51a2a96c2d9da046e9a044683573750b4 100644 (file)
@@ -36,6 +36,7 @@ return [
     'export_html' => 'Pilna satura web fails',
     'export_pdf' => 'PDF fails',
     'export_text' => 'Vienkāršs teksta fails',
+    'export_md' => 'Markdown fails',
 
     // Permissions and restrictions
     'permissions' => 'Atļaujas',
@@ -98,6 +99,7 @@ return [
     'shelves_permissions' => 'Grāmatplaukta atļaujas',
     'shelves_permissions_updated' => 'Grāmatplaukta atļaujas atjauninātas',
     'shelves_permissions_active' => 'Grāmatplaukta atļaujas ir aktīvas',
+    'shelves_permissions_cascade_warning' => 'Grāmatu plauktu atļaujas netiek automātiski pārvietotas uz grāmatām. Tas ir tāpēc, ka grāmata var atrasties vairākos plauktos. Tomēr atļaujas var nokopēt uz plauktam pievienotajām grāmatām, izmantojot zemāk norādīto opciju.',
     'shelves_copy_permissions_to_books' => 'Kopēt grāmatplaukta atļaujas uz grāmatām',
     'shelves_copy_permissions' => 'Kopēt atļaujas',
     'shelves_copy_permissions_explain' => 'Šis piemēros pašreizējās grāmatplaukta piekļuves tiesības visām tajā esošajām grāmatām. Pirms ieslēgšanas pārliecinieties, ka ir saglabātas izmaiņas grāmatplaukta piekļuves tiesībām.',
index dc00a23b31b72c670dea0e20c00f678d51ea5010..0108a9aa5d54149b447b968d7f11b5a2fb8abcf4 100644 (file)
@@ -92,6 +92,7 @@ return [
     'recycle_bin' => 'Miskaste',
     'recycle_bin_desc' => 'Te jūs varat atjaunot dzēstās vienības vai arī izdzēst tās no sistēmas pilnībā. Šis saraksts nav filtrēts atšķirībā no līdzīgiem darbību sarakstiem sistēmā, kur ir piemēroti piekļuves tiesību filtri.',
     'recycle_bin_deleted_item' => 'Dzēsta vienība',
+    'recycle_bin_deleted_parent' => 'Parent',
     'recycle_bin_deleted_by' => 'Izdzēsa',
     'recycle_bin_deleted_at' => 'Dzēšanas laiks',
     'recycle_bin_permanently_delete' => 'Neatgriezeniski izdzēst',
@@ -104,6 +105,7 @@ return [
     'recycle_bin_restore_list' => 'Atjaunojamās vienības',
     'recycle_bin_restore_confirm' => 'Šī darbība atjaunos dzēsto vienību, tai skaitā visus tai pakārtotos elementus, uz tās sākotnējo atrašanās vietu. Ja sākotnējā atrašanās vieta ir izdzēsta un atrodas miskastē, būs nepieciešams atjaunot arī to.',
     'recycle_bin_restore_deleted_parent' => 'Šo elementu saturošā vienība arī ir dzēsta. Tas paliks dzēsts līdz šī saturošā vienība arī ir atjaunota.',
+    'recycle_bin_restore_parent' => 'Restore Parent',
     'recycle_bin_destroy_notification' => 'Dzēstas kopā :count vienības no miskastes.',
     'recycle_bin_restore_notification' => 'Atjaunotas kopā :count vienības no miskastes.',
 
@@ -117,6 +119,7 @@ return [
     'audit_table_user' => 'Lietotājs',
     'audit_table_event' => 'Notikums',
     'audit_table_related' => 'Saistīta vienība vai detaļa',
+    'audit_table_ip' => 'IP Address',
     'audit_table_date' => 'Notikuma datums',
     'audit_date_from' => 'Datums no',
     'audit_date_to' => 'Datums līdz',
@@ -136,6 +139,7 @@ return [
     'role_details' => 'Informācija par grupu',
     'role_name' => 'Grupas nosaukums',
     'role_desc' => 'Īss grupas apaksts',
+    'role_mfa_enforced' => 'Requires Multi-Factor Authentication',
     'role_external_auth_id' => 'Ārējais autentifikācijas ID',
     'role_system' => 'Sistēmas atļaujas',
     'role_manage_users' => 'Pārvaldīt lietotājus',
@@ -145,6 +149,7 @@ return [
     'role_manage_page_templates' => 'Pārvaldīt lapas veidnes',
     'role_access_api' => 'Piekļūt sistēmas API',
     'role_manage_settings' => 'Pārvaldīt iestatījumus',
+    'role_export_content' => 'Export content',
     'role_asset' => 'Resursa piekļuves tiesības',
     'roles_system_warning' => 'Jebkuras no trīs augstāk redzamajām atļaujām dod iespēju lietotājam mainīt savas un citu lietotāju sistēmas atļaujas. Pievieno šīs grupu atļaujas tikai tiem lietotājiem, kuriem uzticies.',
     'role_asset_desc' => 'Šīs piekļuves tiesības kontrolē noklusēto piekļuvi sistēmas resursiem. Grāmatām, nodaļām un lapām norādītās tiesības būs pārākas par šīm.',
@@ -202,6 +207,10 @@ return [
     'users_api_tokens_create' => 'Izveidot žetonu',
     'users_api_tokens_expires' => 'Derīguma termiņš',
     'users_api_tokens_docs' => 'API dokumentācija',
+    'users_mfa' => 'Multi-Factor Authentication',
+    'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
+    'users_mfa_x_methods' => ':count method configured|:count methods configured',
+    'users_mfa_configure' => 'Configure Methods',
 
     // API Tokens
     'user_api_token_create' => 'Izveidot API žetonu',
@@ -247,6 +256,7 @@ return [
         'it' => 'Italian',
         'ja' => '日本語',
         'ko' => '한국어',
+        'lt' => 'Lietuvių Kalba',
         'lv' => 'Latviešu Valoda',
         'nl' => 'Nederlands',
         'nb' => 'Norsk (Bokmål)',
index 6de2b39732706588184380893619f0cfad7906f0..8dd93974615ec6b3c2ad1075af1d9e994d90c86b 100644 (file)
@@ -15,6 +15,7 @@ return [
     'alpha_dash'           => ':attribute var saturēt tikai burtus, ciparus, domuzīmes un apakš svītras.',
     'alpha_num'            => ':attribute var saturēt tikai burtus un ciparus.',
     'array'                => ':attribute ir jābūt masīvam.',
+    'backup_codes'         => 'Ievadītais kods nav derīgs vai arī jau ir izmantots.',
     'before'               => ':attribute jābūt datumam pirms :date.',
     'between'              => [
         'numeric' => ':attribute jābūt starp :min un :max.',
@@ -98,6 +99,7 @@ return [
     ],
     'string'               => ':attribute jābūt teksta virknei.',
     'timezone'             => ':attribute jābūt derīgai zonai.',
+    'totp'                 => 'Ievadītais kods nav derīgs.',
     'unique'               => ':attribute jau ir aizņemts.',
     'url'                  => ':attribute formāts nav derīgs.',
     'uploaded'             => 'Fails netika ielādēts. Serveris nevar pieņemt šāda izmēra failus.',
index 12d0dba6217d35032dd4c31d79ba0c5ece4a1e5b..7313f37f1d3f22b26a2f7c7e3df1a24d50835ea4 100644 (file)
@@ -45,8 +45,12 @@ return [
     'bookshelf_delete_notification'    => 'Bokhyllen ble slettet',
 
     // Favourites
-    'favourite_add_notification' => '":name" has been added to your favourites',
-    'favourite_remove_notification' => '":name" has been removed from your favourites',
+    'favourite_add_notification' => '«:name» ble lagt til i dine favoritter',
+    'favourite_remove_notification' => '«:name» ble fjernet fra dine favoritter',
+
+    // MFA
+    'mfa_setup_method_notification' => 'Flerfaktor-metoden ble konfigurert',
+    'mfa_remove_method_notification' => 'Flerfaktor-metoden ble fjernet',
 
     // Other
     'commented_on'                => 'kommenterte på',
index ae145d28be00f34b8d95af82db42078155d612c2..4c1f5557732ce02074757b8bd9bb6df197330946 100644 (file)
@@ -73,5 +73,40 @@ return [
     '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!'
+    'user_invite_success' => 'Passordet er angitt, du kan nå bruke :appName!',
+
+    // Multi-factor Authentication
+    'mfa_setup' => 'Konfigurer flerfaktor-autentisering',
+    'mfa_setup_desc' => 'Konfigurer flerfaktor-autentisering som et ekstra lag med sikkerhet for brukerkontoen din.',
+    'mfa_setup_configured' => 'Allerede konfigurert',
+    'mfa_setup_reconfigure' => 'Omkonfigurer',
+    'mfa_setup_remove_confirmation' => 'Er du sikker på at du vil deaktivere denne flerfaktor-autentiseringsmetoden?',
+    'mfa_setup_action' => 'Konfigurasjon',
+    'mfa_backup_codes_usage_limit_warning' => 'Du har mindre enn 5 sikkerhetskoder igjen; vennligst generer og lagre ett nytt sett før du går tom for koder, for å unngå å bli låst ute av kontoen din.',
+    'mfa_option_totp_title' => 'Mobilapplikasjon',
+    'mfa_option_totp_desc' => 'For å bruke flerfaktorautentisering trenger du en mobilapplikasjon som støtter TOTP-teknologien, slik som Google Authenticator, Authy eller Microsoft Authenticator.',
+    'mfa_option_backup_codes_title' => 'Sikkerhetskoder',
+    'mfa_option_backup_codes_desc' => 'Lagre sikkerhetskoder til engangsbruk på et trygt sted, disse kan du bruke for å verifisere identiteten din.',
+    'mfa_gen_confirm_and_enable' => 'Bekreft og aktiver',
+    'mfa_gen_backup_codes_title' => 'Konfigurasjon av sikkerhetskoder',
+    'mfa_gen_backup_codes_desc' => 'Lagre nedeforstående liste med koder på et trygt sted. Når du skal ha tilgang til systemet kan du bruke en av disse som en faktor under innlogging.',
+    'mfa_gen_backup_codes_download' => 'Last ned koder',
+    'mfa_gen_backup_codes_usage_warning' => 'Hver kode kan kun brukes en gang',
+    'mfa_gen_totp_title' => 'Oppsett for mobilapplikasjon',
+    'mfa_gen_totp_desc' => 'For å bruke flerfaktorautentisering trenger du en mobilapplikasjon som støtter TOTP-teknologien, slik som Google Authenticator, Authy eller Microsoft Authenticator.',
+    'mfa_gen_totp_scan' => 'Scan QR-koden nedenfor med valgt TOTP-applikasjon for å starte.',
+    'mfa_gen_totp_verify_setup' => 'Bekreft oppsett',
+    'mfa_gen_totp_verify_setup_desc' => 'Bekreft at oppsettet fungerer ved å skrive inn koden fra TOTP-applikasjonen i boksen nedenfor:',
+    'mfa_gen_totp_provide_code_here' => 'Skriv inn den genererte koden her',
+    'mfa_verify_access' => 'Bekreft tilgang',
+    'mfa_verify_access_desc' => 'Brukerkontoen din krever at du bekrefter din identitet med en ekstra autentiseringsfaktor før du får tilgang. Bekreft identiteten med en av dine konfigurerte metoder for å fortsette.',
+    'mfa_verify_no_methods' => 'Ingen metoder er konfigurert',
+    'mfa_verify_no_methods_desc' => 'Ingen flerfaktorautentiseringsmetoder er satt opp for din konto. Du må sette opp minst en metode for å få tilgang.',
+    'mfa_verify_use_totp' => 'Bekreft med mobilapplikasjon',
+    'mfa_verify_use_backup_codes' => 'Bekreft med sikkerhetskode',
+    'mfa_verify_backup_code' => 'Sikkerhetskode',
+    'mfa_verify_backup_code_desc' => 'Skriv inn en av dine ubrukte sikkerhetskoder under:',
+    'mfa_verify_backup_code_enter_here' => 'Skriv inn sikkerhetskode her',
+    'mfa_verify_totp_desc' => 'Skriv inn koden, generert ved hjelp av mobilapplikasjonen, nedenfor:',
+    'mfa_setup_login_notification' => 'Flerfaktorautentisering er konfigurert, vennligst logg inn på nytt med denne metoden.',
 ];
\ No newline at end of file
index 3aadd805aeddacb1a4c05dc9cdb9e7db8dd0e256..8ba4e74745df47daec83d6326751a12c40c96244 100644 (file)
@@ -39,11 +39,12 @@ return [
     'reset' => 'Nullstill',
     'remove' => 'Fjern',
     'add' => 'Legg til',
+    'configure' => 'Konfigurer',
     'fullscreen' => 'Fullskjerm',
-    'favourite' => 'Favourite',
-    'unfavourite' => 'Unfavourite',
-    'next' => 'Next',
-    'previous' => 'Previous',
+    'favourite' => 'Favorisér',
+    'unfavourite' => 'Avfavorisér',
+    'next' => 'Neste',
+    'previous' => 'Forrige',
 
     // Sort Options
     'sort_options' => 'Sorteringsalternativer',
@@ -51,7 +52,7 @@ return [
     'sort_ascending' => 'Stigende sortering',
     'sort_descending' => 'Synkende sortering',
     'sort_name' => 'Navn',
-    'sort_default' => 'Default',
+    'sort_default' => 'Standard',
     'sort_created_at' => 'Dato opprettet',
     'sort_updated_at' => 'Dato oppdatert',
 
@@ -60,6 +61,7 @@ return [
     'no_activity' => 'Ingen aktivitet å vise',
     'no_items' => 'Ingen ting å vise',
     'back_to_top' => 'Hopp til toppen',
+    'skip_to_main_content' => 'Gå til hovedinnhold',
     'toggle_details' => 'Vis/skjul detaljer',
     'toggle_thumbnails' => 'Vis/skjul miniatyrbilder',
     'details' => 'Detaljer',
@@ -69,7 +71,7 @@ return [
     'breadcrumb' => 'Brødsmuler',
 
     // Header
-    'header_menu_expand' => 'Expand Header Menu',
+    'header_menu_expand' => 'Utvid toppmeny',
     'profile_menu' => 'Profilmeny',
     'view_profile' => 'Vis profil',
     'edit_profile' => 'Endre Profile',
@@ -78,9 +80,9 @@ return [
 
     // Layout tabs
     'tab_info' => 'Informasjon',
-    'tab_info_label' => 'Tab: Show Secondary Information',
+    'tab_info_label' => 'Fane: Vis tilleggsinfo',
     'tab_content' => 'Innhold',
-    'tab_content_label' => 'Tab: Show Primary Content',
+    'tab_content_label' => 'Fane: Vis hovedinnhold',
 
     // Email Content
     'email_action_help' => 'Om du har problemer med å trykke på «:actionText»-knappen, bruk nettadressen under for å gå direkte dit:',
@@ -88,6 +90,6 @@ return [
 
     // Footer Link Options
     // Not directly used but available for convenience to users.
-    'privacy_policy' => 'Privacy Policy',
-    'terms_of_service' => 'Terms of Service',
+    'privacy_policy' => 'Personvernregler',
+    'terms_of_service' => 'Bruksvilkår',
 ];
index 0b3d1b4164a95a434747cc5734933cc70fa4b4d1..a50aa71a81bbe91cc48c2f6de8372ed964735ae2 100644 (file)
@@ -27,8 +27,8 @@ return [
     'images' => 'Bilder',
     'my_recent_drafts' => 'Mine nylige utkast',
     'my_recently_viewed' => 'Mine nylige visninger',
-    'my_most_viewed_favourites' => 'My Most Viewed Favourites',
-    'my_favourites' => 'My Favourites',
+    'my_most_viewed_favourites' => 'Mine mest viste favoritter',
+    'my_favourites' => 'Mine favoritter',
     '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',
@@ -36,6 +36,7 @@ return [
     'export_html' => 'Nettside med alt',
     'export_pdf' => 'PDF Fil',
     'export_text' => 'Tekstfil',
+    'export_md' => 'Markdownfil',
 
     // Permissions and restrictions
     'permissions' => 'Tilganger',
@@ -62,7 +63,7 @@ return [
     'search_permissions_set' => 'Tilganger er angitt',
     'search_created_by_me' => 'Opprettet av meg',
     'search_updated_by_me' => 'Oppdatert av meg',
-    'search_owned_by_me' => 'Owned by me',
+    'search_owned_by_me' => 'Eid av meg',
     'search_date_options' => 'Datoalternativer',
     'search_updated_before' => 'Oppdatert før',
     'search_updated_after' => 'Oppdatert etter',
@@ -98,6 +99,7 @@ return [
     'shelves_permissions' => 'Tilganger til hylla',
     'shelves_permissions_updated' => 'Hyllas tilganger er oppdatert',
     'shelves_permissions_active' => 'Hyllas tilganger er aktive',
+    'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',
     '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.',
index 971dbf1cadb2a3302c6b9a359ad2b745330933d0..4713be3a4672c3f55077ff9e6f6e66bf9ba44b8b 100644 (file)
@@ -83,9 +83,9 @@ return [
     '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.',
-    'image_not_found' => 'Image Not Found',
-    'image_not_found_subtitle' => 'Sorry, The image file you were looking for could not be found.',
-    'image_not_found_details' => 'If you expected this image to exist it might have been deleted.',
+    'image_not_found' => 'Bildet ble ikke funnet',
+    'image_not_found_subtitle' => 'Beklager, bildefilen du ser etter ble ikke funnet.',
+    'image_not_found_details' => 'Om du forventet at dette bildet skal eksistere, er det mulig det er slettet.',
     'return_home' => 'Gå til hovedside',
     'error_occurred' => 'En feil oppsto',
     'app_down' => ':appName er nede for øyeblikket',
index 1451cff7098f1890b762aabfc8d861b148c86e27..cfa82f87c5b70717682c8cb4b494cacca14e6f33 100644 (file)
@@ -37,11 +37,11 @@ return [
     '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_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_footer_links' => 'Fotlenker',
+    'app_footer_links_desc' => 'Legg til fotlenker i sidens fotområde. Disse vil vises nederst på de fleste sider, inkludert sider som ikke krever innlogging. Du kan bruke «trans::<key>» etiketter for system-definerte oversettelser. For eksempel: Bruk «trans::common.privacy_policy» for å vise teksten «Personvernregler» og «trans::common.terms_of_service» for å vise teksten «Bruksvilkår».',
+    'app_footer_links_label' => 'Lenketekst',
+    'app_footer_links_url' => 'Lenke',
+    'app_footer_links_add' => 'Legg til fotlenke',
     '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.',
@@ -92,6 +92,7 @@ return [
     'recycle_bin' => 'Papirkurven',
     'recycle_bin_desc' => 'Her kan du gjenopprette ting du har kastet i papirkurven eller velge å slette dem permanent fra systemet. Denne listen er ikke filtrert i motsetning til lignende lister i systemet hvor tilgangskontroll overholdes.',
     'recycle_bin_deleted_item' => 'Kastet element',
+    'recycle_bin_deleted_parent' => 'Overordnet',
     'recycle_bin_deleted_by' => 'Kastet av',
     'recycle_bin_deleted_at' => 'Kastet den',
     'recycle_bin_permanently_delete' => 'Slett permanent',
@@ -104,6 +105,7 @@ return [
     'recycle_bin_restore_list' => 'Elementer som skal gjenopprettes',
     'recycle_bin_restore_confirm' => 'Denne handlingen vil hente opp elementet fra papirkurven, inkludert underliggende innhold, til sin opprinnelige sted. Om den opprinnelige plassen har blitt slettet i mellomtiden og nå befinner seg i papirkurven, vil også dette bli hentet opp igjen.',
     'recycle_bin_restore_deleted_parent' => 'Det overordnede elementet var også kastet i papirkurven. Disse elementene vil forbli kastet inntil det overordnede også hentes opp igjen.',
+    'recycle_bin_restore_parent' => 'Gjenopprett overodnet',
     'recycle_bin_destroy_notification' => 'Slettet :count elementer fra papirkurven.',
     'recycle_bin_restore_notification' => 'Gjenopprettet :count elementer fra papirkurven.',
 
@@ -117,6 +119,7 @@ return [
     'audit_table_user' => 'Kontoholder',
     'audit_table_event' => 'Hendelse',
     'audit_table_related' => 'Relaterte elementer eller detaljer',
+    'audit_table_ip' => 'IP Address',
     'audit_table_date' => 'Aktivitetsdato',
     'audit_date_from' => 'Datoperiode fra',
     'audit_date_to' => 'Datoperiode til',
@@ -136,6 +139,7 @@ return [
     'role_details' => 'Rolledetaljer',
     'role_name' => 'Rollenavn',
     'role_desc' => 'Kort beskrivelse av rolle',
+    'role_mfa_enforced' => 'Krever flerfaktorautentisering',
     'role_external_auth_id' => 'Ekstern godkjennings-ID',
     'role_system' => 'Systemtilganger',
     'role_manage_users' => 'Behandle kontoer',
@@ -145,6 +149,7 @@ return [
     'role_manage_page_templates' => 'Behandle sidemaler',
     'role_access_api' => 'Systemtilgang API',
     'role_manage_settings' => 'Behandle applikasjonsinnstillinger',
+    'role_export_content' => 'Export content',
     '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.',
@@ -202,6 +207,10 @@ return [
     'users_api_tokens_create' => 'Opprett nøkkel',
     'users_api_tokens_expires' => 'Utløper',
     'users_api_tokens_docs' => 'API-dokumentasjon',
+    'users_mfa' => 'Flerfaktorautentisering',
+    'users_mfa_desc' => 'Konfigurer flerfaktorautentisering som et ekstra lag med sikkerhet for din konto.',
+    'users_mfa_x_methods' => ':count metode konfigurert|:count metoder konfigurert',
+    'users_mfa_configure' => 'Konfigurer metoder',
 
     // API Tokens
     'user_api_token_create' => 'Opprett API-nøkkel',
@@ -247,6 +256,7 @@ return [
         'it' => 'Italian',
         'ja' => '日本語',
         'ko' => '한국어',
+        'lt' => 'Lietuvių Kalba',
         'lv' => 'Latviešu Valoda',
         'nl' => 'Nederlands',
         'nb' => 'Norsk (Bokmål)',
index d06240c7cef1dd11337d66b962062089fe628c08..684645729870e35f9175c683e54cbd5f20a4f80a 100644 (file)
@@ -15,6 +15,7 @@ return [
     '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.',
+    'backup_codes'         => 'Den angitte koden er ikke gyldig, eller er allerede benyttet.',
     'before'               => ':attribute må være en dato før :date.',
     'between'              => [
         'numeric' => ':attribute må være mellom :min og :max.',
@@ -98,6 +99,7 @@ return [
     ],
     'string'               => ':attribute må være en tekststreng.',
     'timezone'             => ':attribute må være en tidssone.',
+    'totp'                 => 'Den angitte koden er ikke gyldig eller har utløpt.',
     '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.',
index fcbad2400d8c24cc16b733c70d0c7bbce146174e..f45ea074ad77d4ff5d59b1eb56c67ec32af1e065 100644 (file)
@@ -47,6 +47,10 @@ return [
     'favourite_add_notification' => '":name" is toegevoegd aan je favorieten',
     'favourite_remove_notification' => '":name" is verwijderd uit je favorieten',
 
+    // MFA
+    'mfa_setup_method_notification' => 'Multi-factor method successfully configured',
+    'mfa_remove_method_notification' => 'Multi-factor method successfully removed',
+
     // Other
     'commented_on'                => 'reageerde op',
     'permissions_update'          => 'wijzigde permissies',
index fcff3fd48f0a5b8c4bdf4bbb29595c86dabe0b18..f57b2ecc770750b0b2e341b6be4bfc8a551d133d 100644 (file)
@@ -73,5 +73,40 @@ return [
     'user_invite_page_welcome' => 'Welkom bij :appName!',
     'user_invite_page_text' => 'Om je account af te ronden en toegang te krijgen moet je een wachtwoord instellen dat gebruikt wordt om in te loggen op :appName bij toekomstige bezoeken.',
     'user_invite_page_confirm_button' => 'Bevestig wachtwoord',
-    'user_invite_success' => 'Wachtwoord ingesteld, je hebt nu toegang tot :appName!'
+    'user_invite_success' => 'Wachtwoord ingesteld, je hebt nu toegang tot :appName!',
+
+    // Multi-factor Authentication
+    'mfa_setup' => 'Setup Multi-Factor Authentication',
+    'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
+    'mfa_setup_configured' => 'Already configured',
+    'mfa_setup_reconfigure' => 'Reconfigure',
+    'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?',
+    'mfa_setup_action' => 'Setup',
+    'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.',
+    'mfa_option_totp_title' => 'Mobile App',
+    'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
+    'mfa_option_backup_codes_title' => 'Backup Codes',
+    'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.',
+    'mfa_gen_confirm_and_enable' => 'Confirm and Enable',
+    'mfa_gen_backup_codes_title' => 'Backup Codes Setup',
+    'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.',
+    'mfa_gen_backup_codes_download' => 'Download Codes',
+    'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once',
+    'mfa_gen_totp_title' => 'Mobile App Setup',
+    'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
+    'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.',
+    'mfa_gen_totp_verify_setup' => 'Verify Setup',
+    'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:',
+    'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here',
+    'mfa_verify_access' => 'Verify Access',
+    'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.',
+    'mfa_verify_no_methods' => 'No Methods Configured',
+    'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.',
+    'mfa_verify_use_totp' => 'Verify using a mobile app',
+    'mfa_verify_use_backup_codes' => 'Verify using a backup code',
+    'mfa_verify_backup_code' => 'Backup Code',
+    'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:',
+    'mfa_verify_backup_code_enter_here' => 'Enter backup code here',
+    'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:',
+    'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.',
 ];
\ No newline at end of file
index 67d0876c7fe7ce023c441ce7444319fc5ad1eb86..c53df6f2c1e5867265f934fd905c1043684d7c54 100644 (file)
@@ -39,6 +39,7 @@ return [
     'reset' => 'Resetten',
     'remove' => 'Verwijderen',
     'add' => 'Toevoegen',
+    'configure' => 'Configure',
     'fullscreen' => 'Volledig scherm',
     'favourite' => 'Favoriet',
     'unfavourite' => 'Verwijderen uit favoriet',
@@ -60,6 +61,7 @@ return [
     'no_activity' => 'Geen activiteit om weer te geven',
     'no_items' => 'Geen items beschikbaar',
     'back_to_top' => 'Terug naar boven',
+    'skip_to_main_content' => 'Direct naar de hoofdinhoud',
     'toggle_details' => 'Details weergeven',
     'toggle_thumbnails' => 'Thumbnails weergeven',
     'details' => 'Details',
index eecdfd589b1f3c8edd158de45a26ac2cd3807a6c..56ef9a07a8a35064af7e29c981fb6dc12fa92427 100644 (file)
@@ -36,6 +36,7 @@ return [
     'export_html' => 'Ingesloten webbestand',
     'export_pdf' => 'PDF bestand',
     'export_text' => 'Normaal tekstbestand',
+    'export_md' => 'Markdown bestand',
 
     // Permissions and restrictions
     'permissions' => 'Permissies',
@@ -98,6 +99,7 @@ return [
     'shelves_permissions' => 'Boekenplank permissies',
     'shelves_permissions_updated' => 'Boekenplank permissies opgeslagen',
     'shelves_permissions_active' => 'Boekenplank permissies actief',
+    'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',
     'shelves_copy_permissions_to_books' => 'Kopieer permissies naar boeken',
     'shelves_copy_permissions' => 'Kopieer permissies',
     'shelves_copy_permissions_explain' => 'Met deze actie worden de permissies van deze boekenplank gekopieërd naar alle boeken op de plank. Voordat deze actie wordt uitgevoerd, zorg dat de wijzigingen in de permissies van deze boekenplank zijn opgeslagen.',
index 54cee27b13706b142a2208a512cb32ba61c4f4b1..1cbc677ae0568991d9f17ae3b01adedf562605d9 100644 (file)
@@ -92,6 +92,7 @@ return [
     'recycle_bin' => 'Prullenbak',
     'recycle_bin_desc' => 'Hier kunt u items herstellen die zijn verwijderd of kiezen om ze permanent te verwijderen uit het systeem. Deze lijst is niet gefilterd, in tegenstelling tot vergelijkbare activiteitenlijsten in het systeem waar rechtenfilters worden toegepast.',
     'recycle_bin_deleted_item' => 'Verwijderde Item',
+    'recycle_bin_deleted_parent' => 'Bovenliggende',
     'recycle_bin_deleted_by' => 'Verwijderd door',
     'recycle_bin_deleted_at' => 'Verwijdert op',
     'recycle_bin_permanently_delete' => 'Permanent verwijderen',
@@ -104,6 +105,7 @@ return [
     'recycle_bin_restore_list' => 'Items te herstellen',
     'recycle_bin_restore_confirm' => 'Deze actie herstelt het verwijderde item, inclusief alle onderliggende elementen, op hun oorspronkelijke locatie. Als de oorspronkelijke locatie sindsdien is verwijderd en zich nu in de prullenbak bevindt, zal ook het bovenliggende item moeten worden hersteld.',
     'recycle_bin_restore_deleted_parent' => 'De bovenliggende map van dit item is ook verwijderd. Deze zal worden verwijderd totdat het bovenliggende item ook is hersteld.',
+    'recycle_bin_restore_parent' => 'Herstel bovenliggende',
     'recycle_bin_destroy_notification' => 'Verwijderde totaal :count items uit de prullenbak.',
     'recycle_bin_restore_notification' => 'Herstelde totaal :count items uit de prullenbak.',
 
@@ -117,6 +119,7 @@ return [
     'audit_table_user' => 'Gebruiker',
     'audit_table_event' => 'Gebeurtenis',
     'audit_table_related' => 'Gerelateerd Item of Detail',
+    'audit_table_ip' => 'IP Address',
     'audit_table_date' => 'Activiteit datum',
     'audit_date_from' => 'Datum bereik vanaf',
     'audit_date_to' => 'Datum bereik tot',
@@ -136,6 +139,7 @@ return [
     'role_details' => 'Rol Details',
     'role_name' => 'Rolnaam',
     'role_desc' => 'Korte beschrijving van de rol',
+    'role_mfa_enforced' => 'Requires Multi-Factor Authentication',
     'role_external_auth_id' => 'Externe authenticatie ID\'s',
     'role_system' => 'Systeem Permissies',
     'role_manage_users' => 'Gebruikers beheren',
@@ -145,6 +149,7 @@ return [
     'role_manage_page_templates' => 'Paginasjablonen beheren',
     'role_access_api' => 'Ga naar systeem API',
     'role_manage_settings' => 'Beheer app instellingen',
+    'role_export_content' => 'Export content',
     'role_asset' => 'Asset Permissies',
     'roles_system_warning' => 'Wees ervan bewust dat toegang tot een van de bovengenoemde drie machtigingen een gebruiker in staat kan stellen zijn eigen privileges of de privileges van anderen in het systeem te wijzigen. Wijs alleen rollen toe met deze machtigingen aan vertrouwde gebruikers.',
     'role_asset_desc' => 'Deze permissies bepalen de standaardtoegangsrechten. Permissies op boeken, hoofdstukken en pagina\'s overschrijven deze instelling.',
@@ -202,6 +207,10 @@ return [
     'users_api_tokens_create' => 'Token aanmaken',
     'users_api_tokens_expires' => 'Verloopt',
     'users_api_tokens_docs' => 'API Documentatie',
+    'users_mfa' => 'Multi-Factor Authentication',
+    'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
+    'users_mfa_x_methods' => ':count method configured|:count methods configured',
+    'users_mfa_configure' => 'Configure Methods',
 
     // API Tokens
     'user_api_token_create' => 'API-token aanmaken',
@@ -247,6 +256,7 @@ return [
         'it' => 'Italian',
         'ja' => '日本語',
         'ko' => '한국어',
+        'lt' => 'Lietuvių Kalba',
         'lv' => 'Latviešu Valoda',
         'nl' => 'Nederlands',
         'nb' => 'Norsk (Bokmål)',
index f0e99ad912fd373ad75cbc88203c5d4b82d8a452..c572ce85b4a3ce50f4312296cb5abd3c1a08d48b 100644 (file)
@@ -15,6 +15,7 @@ return [
     'alpha_dash'           => ':attribute mag alleen letters, cijfers, streepjes en liggende streepjes bevatten.',
     'alpha_num'            => ':attribute mag alleen letters en nummers bevatten.',
     'array'                => ':attribute moet een reeks zijn.',
+    'backup_codes'         => 'The provided code is not valid or has already been used.',
     'before'               => ':attribute moet een datum zijn voor :date.',
     'between'              => [
         'numeric' => ':attribute moet tussen de :min en :max zijn.',
@@ -98,6 +99,7 @@ return [
     ],
     'string'               => ':attribute moet tekst zijn.',
     'timezone'             => ':attribute moet een geldige zone zijn.',
+    'totp'                 => 'The provided code is not valid or has expired.',
     'unique'               => ':attribute is al in gebruik.',
     'url'                  => ':attribute formaat is ongeldig.',
     'uploaded'             => 'Het bestand kon niet worden geüpload. De server accepteert mogelijk geen bestanden van deze grootte.',
index 13e35e20611ede050a8f8f74251454e9b3a72a73..5ca5fd9f416d237a413427b99db04553a99a797a 100644 (file)
@@ -44,8 +44,12 @@ return [
     'bookshelf_delete_notification'    => 'Półka usunięta pomyślnie',
 
     // Favourites
-    'favourite_add_notification' => '":name" has been added to your favourites',
-    'favourite_remove_notification' => '":name" has been removed from your favourites',
+    'favourite_add_notification' => '":name" został dodany do Twoich ulubionych',
+    'favourite_remove_notification' => '":name" został usunięty z ulubionych',
+
+    // MFA
+    'mfa_setup_method_notification' => 'Metoda wieloskładnikowa została pomyślnie skonfigurowana',
+    'mfa_remove_method_notification' => 'Metoda wieloskładnikowa pomyślnie usunięta',
 
     // Other
     'commented_on'                => 'skomentował',
index 01d74b99cddfbb90fc9bcaf5b278e2c7a14605f5..d2439f2d3bbe4191ee8a6019fe6ed2401405c54d 100644 (file)
@@ -73,5 +73,40 @@ return [
     'user_invite_page_welcome' => 'Witaj w :appName!',
     'user_invite_page_text' => 'Aby zakończyć tworzenie konta musisz ustawić hasło, które będzie używane do logowania do :appName w przyszłości.',
     'user_invite_page_confirm_button' => 'Potwierdź hasło',
-    'user_invite_success' => 'Hasło zostało ustawione, teraz masz dostęp do :appName!'
+    'user_invite_success' => 'Hasło zostało ustawione, teraz masz dostęp do :appName!',
+
+    // Multi-factor Authentication
+    'mfa_setup' => 'Setup Multi-Factor Authentication',
+    'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
+    'mfa_setup_configured' => 'Already configured',
+    'mfa_setup_reconfigure' => 'Reconfigure',
+    'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?',
+    'mfa_setup_action' => 'Setup',
+    'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.',
+    'mfa_option_totp_title' => 'Mobile App',
+    'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
+    'mfa_option_backup_codes_title' => 'Backup Codes',
+    'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.',
+    'mfa_gen_confirm_and_enable' => 'Confirm and Enable',
+    'mfa_gen_backup_codes_title' => 'Backup Codes Setup',
+    'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.',
+    'mfa_gen_backup_codes_download' => 'Download Codes',
+    'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once',
+    'mfa_gen_totp_title' => 'Mobile App Setup',
+    'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
+    'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.',
+    'mfa_gen_totp_verify_setup' => 'Verify Setup',
+    'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:',
+    'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here',
+    'mfa_verify_access' => 'Verify Access',
+    'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.',
+    'mfa_verify_no_methods' => 'No Methods Configured',
+    'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.',
+    'mfa_verify_use_totp' => 'Verify using a mobile app',
+    'mfa_verify_use_backup_codes' => 'Verify using a backup code',
+    'mfa_verify_backup_code' => 'Backup Code',
+    'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:',
+    'mfa_verify_backup_code_enter_here' => 'Enter backup code here',
+    'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:',
+    'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.',
 ];
\ No newline at end of file
index 716ee7ae5fccd02a9d5d2220aa1f02bd8669972b..42a0a312b90ee71f8476c5a11ae75d5062d1496c 100644 (file)
@@ -39,11 +39,12 @@ return [
     'reset' => 'Resetuj',
     'remove' => 'Usuń',
     'add' => 'Dodaj',
+    'configure' => 'Configure',
     'fullscreen' => 'Pełny ekran',
     'favourite' => 'Favourite',
     'unfavourite' => 'Unfavourite',
-    'next' => 'Next',
-    'previous' => 'Previous',
+    'next' => 'Dalej',
+    'previous' => 'Wstecz',
 
     // Sort Options
     'sort_options' => 'Opcje sortowania',
@@ -51,7 +52,7 @@ return [
     'sort_ascending' => 'Sortuj rosnąco',
     'sort_descending' => 'Sortuj malejąco',
     'sort_name' => 'Nazwa',
-    'sort_default' => 'Default',
+    'sort_default' => 'Domyślne',
     'sort_created_at' => 'Data utworzenia',
     'sort_updated_at' => 'Data aktualizacji',
 
@@ -60,6 +61,7 @@ return [
     'no_activity' => 'Brak aktywności do wyświetlenia',
     'no_items' => 'Brak elementów do wyświetlenia',
     'back_to_top' => 'Powrót na górę',
+    'skip_to_main_content' => 'Przejdź do treści głównej',
     'toggle_details' => 'Włącz/wyłącz szczegóły',
     'toggle_thumbnails' => 'Włącz/wyłącz miniatury',
     'details' => 'Szczegóły',
index 9d172a87373d7c25cfe3306da0d4b21a577dc872..138062109103ebae8b506a01748e1ee84b3466e5 100644 (file)
@@ -36,6 +36,7 @@ return [
     'export_html' => 'Plik HTML',
     'export_pdf' => 'Plik PDF',
     'export_text' => 'Plik tekstowy',
+    'export_md' => 'Markdown File',
 
     // Permissions and restrictions
     'permissions' => 'Uprawnienia',
@@ -98,6 +99,7 @@ return [
     'shelves_permissions' => 'Uprawnienia półki',
     'shelves_permissions_updated' => 'Uprawnienia półki zostały zaktualizowane',
     'shelves_permissions_active' => 'Uprawnienia półki są aktywne',
+    'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',
     'shelves_copy_permissions_to_books' => 'Skopiuj uprawnienia do książek',
     'shelves_copy_permissions' => 'Skopiuj uprawnienia',
     'shelves_copy_permissions_explain' => 'To spowoduje zastosowanie obecnych ustawień uprawnień dla tej półki do wszystkich książek w niej zawartych. Przed aktywacją upewnij się, że wszelkie zmiany w uprawnieniach do tej półki zostały zapisane.',
index 236c787a7f767d4ab1b71ba5c3742417e4180f50..488b753c65d4c138c7b8cb1ca1e06a83d0467b84 100644 (file)
@@ -83,9 +83,9 @@ return [
     '404_page_not_found' => 'Strona nie została znaleziona',
     'sorry_page_not_found' => 'Przepraszamy, ale strona której szukasz nie została znaleziona.',
     'sorry_page_not_found_permission_warning' => 'Jeśli spodziewałeś się, że ta strona istnieje, prawdopodobnie nie masz uprawnień do jej wyświetlenia.',
-    'image_not_found' => 'Image Not Found',
-    'image_not_found_subtitle' => 'Sorry, The image file you were looking for could not be found.',
-    'image_not_found_details' => 'If you expected this image to exist it might have been deleted.',
+    'image_not_found' => 'Nie znaleziono obrazu',
+    'image_not_found_subtitle' => 'Przepraszamy, ale obraz którego szukasz nie został znaleziony.',
+    'image_not_found_details' => 'Jeśli spodziewałeś się, że ten obraz istnieje, mógł on zostać usunięty.',
     'return_home' => 'Powrót do strony głównej',
     'error_occurred' => 'Wystąpił błąd',
     'app_down' => ':appName jest aktualnie wyłączona',
index 98130dafa564ea60c70368392d7298210248d564..18121a9f24262ad02ab49b49b79dd50c464996f5 100644 (file)
@@ -16,7 +16,7 @@ return [
     'app_features_security' => 'Funkcje i bezpieczeństwo',
     'app_name' => 'Nazwa aplikacji',
     'app_name_desc' => 'Ta nazwa jest wyświetlana w nagłówku i e-mailach.',
-    'app_name_header' => 'Pokazać nazwę aplikacji w nagłówku?',
+    'app_name_header' => 'Pokaż nazwę aplikacji w nagłówku',
     'app_public_access' => 'Dostęp publiczny',
     'app_public_access_desc' => 'Włączenie tej opcji umożliwi niezalogowanym odwiedzającym dostęp do treści w Twojej instancji BookStack.',
     'app_public_access_desc_guest' => 'Dostęp dla niezalogowanych odwiedzających jest dostępny poprzez użytkownika "Guest".',
@@ -59,7 +59,7 @@ return [
     'reg_settings' => 'Ustawienia rejestracji',
     'reg_enable' => 'Włącz rejestrację',
     'reg_enable_toggle' => 'Włącz rejestrację',
-    'reg_enable_desc' => 'Kiedy rejestracja jest włączona użytkownicy mogą się rejestrować. Po rejestracji otrzymują jedną domyślną rolę użytkownika.',
+    'reg_enable_desc' => 'Po włączeniu rejestracji użytkownicy ci będą mogli się samodzielnie zarejestrować i otrzymają domyślną rolę.',
     'reg_default_role' => 'Domyślna rola użytkownika po rejestracji',
     'reg_enable_external_warning' => 'Powyższa opcja jest ignorowana, gdy zewnętrzne uwierzytelnianie LDAP lub SAML jest aktywne. Konta użytkowników dla nieistniejących użytkowników zostaną automatycznie utworzone, jeśli uwierzytelnianie za pomocą systemu zewnętrznego zakończy się sukcesem.',
     'reg_email_confirmation' => 'Potwierdzenie adresu email',
@@ -90,22 +90,24 @@ return [
 
     // Recycle Bin
     'recycle_bin' => 'Kosz',
-    '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_desc' => 'Tutaj możesz przywrócić elementy, które zostały usunięte lub usunąć je z systemu. Ta lista jest niefiltrowana w odróżnieniu od podobnych list aktywności w systemie, w którym stosowane są filtry uprawnień.',
     'recycle_bin_deleted_item' => 'Usunięta pozycja',
+    'recycle_bin_deleted_parent' => 'Parent',
     'recycle_bin_deleted_by' => 'Usunięty przez',
     'recycle_bin_deleted_at' => 'Czas usunięcia',
     'recycle_bin_permanently_delete' => 'Usuń trwale',
     'recycle_bin_restore' => 'Przywróć',
     'recycle_bin_contents_empty' => 'Kosz jest pusty',
     'recycle_bin_empty' => 'Opróżnij kosz',
-    '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_empty_confirm' => 'To na stałe zniszczy wszystkie przedmioty w koszu, w tym zawartość w każdym elemencie. Czy na pewno chcesz opróżnić kosz?',
     '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_destroy_list' => 'Elementy do usunięcia',
     'recycle_bin_restore_list' => 'Elementy do przywrócenia',
     '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.',
+    'recycle_bin_restore_parent' => 'Restore Parent',
+    'recycle_bin_destroy_notification' => 'Usunięto :count przedmiotów z kosza.',
+    'recycle_bin_restore_notification' => 'Przywrócono :count przedmiotów z kosza.',
 
     // Audit Log
     'audit' => 'Dziennik audytu',
@@ -117,6 +119,7 @@ return [
     'audit_table_user' => 'Użytkownik',
     'audit_table_event' => 'Wydarzenie',
     'audit_table_related' => 'Powiązany element lub szczegóły',
+    'audit_table_ip' => 'Adres IP',
     'audit_table_date' => 'Data Aktywności',
     'audit_date_from' => 'Zakres dat od',
     'audit_date_to' => 'Zakres dat do',
@@ -136,6 +139,7 @@ return [
     'role_details' => 'Szczegóły roli',
     'role_name' => 'Nazwa roli',
     'role_desc' => 'Krótki opis roli',
+    'role_mfa_enforced' => 'Requires Multi-Factor Authentication',
     'role_external_auth_id' => 'Zewnętrzne identyfikatory uwierzytelniania',
     'role_system' => 'Uprawnienia systemowe',
     'role_manage_users' => 'Zarządzanie użytkownikami',
@@ -145,6 +149,7 @@ return [
     'role_manage_page_templates' => 'Zarządzaj szablonami stron',
     'role_access_api' => 'Dostęp do systemowego API',
     'role_manage_settings' => 'Zarządzanie ustawieniami aplikacji',
+    'role_export_content' => 'Export content',
     'role_asset' => 'Zarządzanie zasobami',
     'roles_system_warning' => 'Pamiętaj, że dostęp do trzech powyższych uprawnień może pozwolić użytkownikowi na zmianę własnych uprawnień lub uprawnień innych osób w systemie. Przypisz tylko role z tymi uprawnieniami do zaufanych użytkowników.',
     'role_asset_desc' => 'Te ustawienia kontrolują zarządzanie zasobami systemu. Uprawnienia książek, rozdziałów i stron nadpisują te ustawienia.',
@@ -202,6 +207,10 @@ return [
     'users_api_tokens_create' => 'Utwórz token',
     'users_api_tokens_expires' => 'Wygasa',
     'users_api_tokens_docs' => 'Dokumentacja API',
+    'users_mfa' => 'Multi-Factor Authentication',
+    'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
+    'users_mfa_x_methods' => ':count method configured|:count methods configured',
+    'users_mfa_configure' => 'Configure Methods',
 
     // API Tokens
     'user_api_token_create' => 'Utwórz klucz API',
@@ -247,6 +256,7 @@ return [
         'it' => 'Italian',
         'ja' => '日本語',
         'ko' => '한국어',
+        'lt' => 'Lietuvių Kalba',
         'lv' => 'Latviešu Valoda',
         'nl' => 'Nederlands',
         'nb' => 'Norsk (Bokmål)',
index 76ff49a6a4fcb96e13fdfe629785b499494277c6..d852d46b98ccafac5389bafefb36dcffd24673fa 100644 (file)
@@ -15,6 +15,7 @@ return [
     'alpha_dash'           => ':attribute może zawierać wyłącznie litery, cyfry i myślniki.',
     'alpha_num'            => ':attribute może zawierać wyłącznie litery i cyfry.',
     'array'                => ':attribute musi być tablicą.',
+    'backup_codes'         => 'The provided code is not valid or has already been used.',
     'before'               => ':attribute musi być datą poprzedzającą :date.',
     'between'              => [
         'numeric' => ':attribute musi zawierać się w przedziale od :min do :max.',
@@ -51,7 +52,7 @@ return [
     'integer'              => ':attribute musi być liczbą całkowitą.',
     'ip'                   => ':attribute musi być prawidłowym adresem IP.',
     'ipv4'                 => ':attribute musi być prawidłowym adresem IPv4.',
-    'ipv6'                 => ':attribute musi być prawidłowym adresem  IPv6.',
+    'ipv6'                 => ':attribute musi być prawidłowym adresem IPv6.',
     'json'                 => ':attribute musi być prawidłowym ciągiem JSON.',
     'lt'                   => [
         'numeric' => ':attribute musi być mniejszy niż :value.',
@@ -98,6 +99,7 @@ return [
     ],
     'string'               => ':attribute musi być ciągiem znaków.',
     'timezone'             => ':attribute musi być prawidłową strefą czasową.',
+    'totp'                 => 'The provided code is not valid or has expired.',
     'unique'               => ':attribute zostało już zajęte.',
     'url'                  => 'Format :attribute jest nieprawidłowy.',
     'uploaded'             => 'Plik nie może zostać wysłany. Serwer nie akceptuje plików o takim rozmiarze.',
index 20194394f2d49a1e05f17278da7c476f69c80a3e..8bf2ff9fcf1ad8fb5b1d58e7ac05e93b2f4ec22c 100644 (file)
@@ -44,8 +44,12 @@ return [
     'bookshelf_delete_notification'    => 'Estante eliminada com sucesso',
 
     // Favourites
-    'favourite_add_notification' => '":name" has been added to your favourites',
-    'favourite_remove_notification' => '":name" has been removed from your favourites',
+    'favourite_add_notification' => '":name" foi adicionado aos seus favoritos',
+    'favourite_remove_notification' => '":name" foi removido dos seus favoritos',
+
+    // MFA
+    'mfa_setup_method_notification' => 'Multi-factor method successfully configured',
+    'mfa_remove_method_notification' => 'Multi-factor method successfully removed',
 
     // Other
     'commented_on'                => 'comentado a',
index 7d520a5f7d3228a77484b688898b1260fde34d92..de3d35e97e1849e1318283c80ff37272ec66fba1 100644 (file)
@@ -73,5 +73,40 @@ return [
     'user_invite_page_welcome' => 'Bem-vindo(a) a :appName!',
     'user_invite_page_text' => 'Para finalizar a sua conta e obter acesso, precisa de definir uma senha que será utilizada para efetuar login em :appName em visitas futuras.',
     'user_invite_page_confirm_button' => 'Confirmar Palavra-Passe',
-    'user_invite_success' => 'Palavra-passe definida, tem agora acesso a :appName!'
+    'user_invite_success' => 'Palavra-passe definida, tem agora acesso a :appName!',
+
+    // Multi-factor Authentication
+    'mfa_setup' => 'Setup Multi-Factor Authentication',
+    'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
+    'mfa_setup_configured' => 'Already configured',
+    'mfa_setup_reconfigure' => 'Reconfigure',
+    'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?',
+    'mfa_setup_action' => 'Setup',
+    'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.',
+    'mfa_option_totp_title' => 'Mobile App',
+    'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
+    'mfa_option_backup_codes_title' => 'Backup Codes',
+    'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.',
+    'mfa_gen_confirm_and_enable' => 'Confirm and Enable',
+    'mfa_gen_backup_codes_title' => 'Backup Codes Setup',
+    'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.',
+    'mfa_gen_backup_codes_download' => 'Download Codes',
+    'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once',
+    'mfa_gen_totp_title' => 'Mobile App Setup',
+    'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
+    'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.',
+    'mfa_gen_totp_verify_setup' => 'Verify Setup',
+    'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:',
+    'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here',
+    'mfa_verify_access' => 'Verify Access',
+    'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.',
+    'mfa_verify_no_methods' => 'No Methods Configured',
+    'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.',
+    'mfa_verify_use_totp' => 'Verify using a mobile app',
+    'mfa_verify_use_backup_codes' => 'Verify using a backup code',
+    'mfa_verify_backup_code' => 'Backup Code',
+    'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:',
+    'mfa_verify_backup_code_enter_here' => 'Enter backup code here',
+    'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:',
+    'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.',
 ];
\ No newline at end of file
index 7dd2c9c63cd551726df4db87a9b26c1035ba6b81..19a5dc24a9fb2294b77ba5a4d1c187c508016325 100644 (file)
@@ -39,11 +39,12 @@ return [
     'reset' => 'Redefinir',
     'remove' => 'Remover',
     'add' => 'Adicionar',
+    'configure' => 'Configure',
     'fullscreen' => 'Ecrã completo',
-    'favourite' => 'Favourite',
-    'unfavourite' => 'Unfavourite',
-    'next' => 'Next',
-    'previous' => 'Previous',
+    'favourite' => 'Favorito',
+    'unfavourite' => 'Retirar Favorito',
+    'next' => 'Próximo',
+    'previous' => 'Anterior',
 
     // Sort Options
     'sort_options' => 'Opções de Ordenação',
@@ -60,6 +61,7 @@ return [
     'no_activity' => 'Nenhuma atividade a mostrar',
     'no_items' => 'Nenhum item disponível',
     'back_to_top' => 'Voltar ao topo',
+    'skip_to_main_content' => 'Avançar para o conteúdo principal',
     'toggle_details' => 'Alternar Detalhes',
     'toggle_thumbnails' => 'Alternar Miniaturas',
     'details' => 'Detalhes',
index 0b258f1448c0e9442bc5e5443aa6f88f484979f9..1cd5c277fba2de338e704ed4f2083ae479126309 100644 (file)
@@ -27,8 +27,8 @@ return [
     'images' => 'Imagens',
     'my_recent_drafts' => 'Os Meus Rascunhos Recentes',
     'my_recently_viewed' => 'Visualizados Recentemente Por Mim',
-    'my_most_viewed_favourites' => 'My Most Viewed Favourites',
-    'my_favourites' => 'My Favourites',
+    'my_most_viewed_favourites' => 'Os Meus Favoritos Mais Visualizados',
+    'my_favourites' => 'Os Meus Favoritos',
     'no_pages_viewed' => 'Você não viu nenhuma página',
     'no_pages_recently_created' => 'Nenhuma página foi recentemente criada',
     'no_pages_recently_updated' => 'Nenhuma página foi recentemente atualizada',
@@ -36,6 +36,7 @@ return [
     'export_html' => 'Arquivo Web contido',
     'export_pdf' => 'Arquivo PDF',
     'export_text' => 'Arquivo Texto',
+    'export_md' => 'Ficheiro Markdown',
 
     // Permissions and restrictions
     'permissions' => 'Permissões',
@@ -98,6 +99,7 @@ return [
     'shelves_permissions' => 'Permissões da Estante',
     'shelves_permissions_updated' => 'Permissões da Estante de Livros Atualizada',
     'shelves_permissions_active' => 'Permissões da Estante de Livros Ativas',
+    'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',
     'shelves_copy_permissions_to_books' => 'Copiar Permissões para Livros',
     'shelves_copy_permissions' => 'Copiar Permissões',
     'shelves_copy_permissions_explain' => 'Isto aplicará as configurações de permissões atuais desta estante a todos os livros nela contidos. Antes de ativar, assegure-se de que quaisquer alterações nas permissões desta estante foram guardadas.',
index c60b4dd1ba55cbc5d17411bcbb1892eb52e78ac9..aa8450dbf292d449c7cb78ed3245bdc32b665d7f 100644 (file)
@@ -92,6 +92,7 @@ return [
     'recycle_bin' => 'Reciclagem',
     'recycle_bin_desc' => 'Aqui pode restaurar itens que foram eliminados ou eliminá-los permanentemente do sistema. Esta lista não é filtrada diferentemente de listas de atividades parecidas no sistema onde filtros de permissão são aplicados.',
     'recycle_bin_deleted_item' => 'Item eliminado',
+    'recycle_bin_deleted_parent' => 'Parente',
     'recycle_bin_deleted_by' => 'Eliminado por',
     'recycle_bin_deleted_at' => 'Data de Eliminação',
     'recycle_bin_permanently_delete' => 'Eliminar permanentemente',
@@ -104,6 +105,7 @@ return [
     'recycle_bin_restore_list' => 'Itens a serem Restaurados',
     'recycle_bin_restore_confirm' => 'Esta ação irá restaurar o item excluído, inclusive quaisquer elementos filhos, para o seu local original. Se a localização original tiver, entretanto, sido eliminada e estiver agora na reciclagem, o item pai também precisará de ser restaurado.',
     'recycle_bin_restore_deleted_parent' => 'O parente deste item foi também eliminado. Estes permanecerão eliminados até que o parente seja também restaurado.',
+    'recycle_bin_restore_parent' => 'Restaurar Parente',
     'recycle_bin_destroy_notification' => 'Eliminados no total :count itens da lixeira.',
     'recycle_bin_restore_notification' => 'Restaurados no total :count itens da reciclagem.',
 
@@ -117,6 +119,7 @@ return [
     'audit_table_user' => 'Utilizador',
     'audit_table_event' => 'Evento',
     'audit_table_related' => 'Item ou Detalhe Relacionado',
+    'audit_table_ip' => 'IP Address',
     'audit_table_date' => 'Data da Atividade',
     'audit_date_from' => 'Intervalo De',
     'audit_date_to' => 'Intervalo Até',
@@ -136,6 +139,7 @@ return [
     'role_details' => 'Detalhes do Cargo',
     'role_name' => 'Nome do Cargo',
     'role_desc' => 'Breve Descrição do Cargo',
+    'role_mfa_enforced' => 'Requires Multi-Factor Authentication',
     'role_external_auth_id' => 'IDs de Autenticação Externa',
     'role_system' => 'Permissões do Sistema',
     'role_manage_users' => 'Gerir utilizadores',
@@ -145,6 +149,7 @@ return [
     'role_manage_page_templates' => 'Gerir modelos de página',
     'role_access_api' => 'Aceder à API do sistema',
     'role_manage_settings' => 'Gerir as configurações da aplicação',
+    'role_export_content' => 'Export content',
     'role_asset' => 'Permissões de Ativos',
     'roles_system_warning' => 'Esteja ciente de que o acesso a qualquer uma das três permissões acima pode permitir que um utilizador altere os seus próprios privilégios ou privilégios de outros no sistema. Apenas atribua cargos com essas permissões a utilizadores de confiança.',
     'role_asset_desc' => 'Estas permissões controlam o acesso padrão para os ativos dentro do sistema. Permissões em Livros, Capítulos e Páginas serão sobrescritas por estas permissões.',
@@ -202,6 +207,10 @@ return [
     'users_api_tokens_create' => 'Criar Token',
     'users_api_tokens_expires' => 'Expira',
     'users_api_tokens_docs' => 'Documentação da API',
+    'users_mfa' => 'Multi-Factor Authentication',
+    'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
+    'users_mfa_x_methods' => ':count method configured|:count methods configured',
+    'users_mfa_configure' => 'Configure Methods',
 
     // API Tokens
     'user_api_token_create' => 'Criar Token de API',
@@ -247,6 +256,7 @@ return [
         'it' => 'Italian',
         'ja' => '日本語',
         'ko' => '한국어',
+        'lt' => 'Lietuvių Kalba',
         'lv' => 'Latviešu Valoda',
         'nl' => 'Nederlands',
         'nb' => 'Norsk (Bokmål)',
index b36ed0dab7b83a508da381c7b652cade9cad1664..30033fce8f5593214962fb43010aa508bc585ed3 100644 (file)
@@ -15,6 +15,7 @@ return [
     'alpha_dash'           => 'O campo :attribute deve conter apenas letras, números, traços e sublinhado.',
     'alpha_num'            => 'O campo :attribute deve conter apenas letras e números.',
     'array'                => 'O campo :attribute deve ser uma lista(array).',
+    'backup_codes'         => 'The provided code is not valid or has already been used.',
     'before'               => 'O campo :attribute deve ser uma data anterior à data :date.',
     'between'              => [
         'numeric' => 'O campo :attribute deve estar entre :min e :max.',
@@ -98,6 +99,7 @@ return [
     ],
     'string'               => 'O campo :attribute deve ser uma string.',
     'timezone'             => 'O campo :attribute deve conter uma timezone válida.',
+    'totp'                 => 'The provided code is not valid or has expired.',
     'unique'               => 'Já existe um campo/dado de nome :attribute.',
     'url'                  => 'O formato da URL :attribute é inválido.',
     'uploaded'             => 'O arquivo não pôde ser carregado. O servidor pode não aceitar arquivos deste tamanho.',
index ad5a34398b98fb9cf72d02c6a8cde9e7b57fc072..487a6fce6d6171dd09b5486f92c09dc82ed46ce6 100644 (file)
@@ -47,6 +47,10 @@ return [
     'favourite_add_notification' => '":name" has been added to your favourites',
     'favourite_remove_notification' => '":name" has been removed from your favourites',
 
+    // MFA
+    'mfa_setup_method_notification' => 'Multi-factor method successfully configured',
+    'mfa_remove_method_notification' => 'Multi-factor method successfully removed',
+
     // Other
     'commented_on'                => 'comentou em',
     'permissions_update'          => 'atualizou permissões',
index b2c3072c41a8fe6af821064239c6ddc1511ae895..a5f0c18bc21bc98b4eafe11741f56cb384d01dd1 100644 (file)
@@ -73,5 +73,40 @@ return [
     'user_invite_page_welcome' => 'Bem-vindo(a) a :appName!',
     'user_invite_page_text' => 'Para finalizar sua conta e obter acesso, você precisa definir uma senha que será usada para efetuar login em :appName em futuras visitas.',
     'user_invite_page_confirm_button' => 'Confirmar Senha',
-    'user_invite_success' => 'Senha definida, você agora tem acesso a :appName!'
+    'user_invite_success' => 'Senha definida, você agora tem acesso a :appName!',
+
+    // Multi-factor Authentication
+    'mfa_setup' => 'Setup Multi-Factor Authentication',
+    'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
+    'mfa_setup_configured' => 'Already configured',
+    'mfa_setup_reconfigure' => 'Reconfigure',
+    'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?',
+    'mfa_setup_action' => 'Setup',
+    'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.',
+    'mfa_option_totp_title' => 'Mobile App',
+    'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
+    'mfa_option_backup_codes_title' => 'Backup Codes',
+    'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.',
+    'mfa_gen_confirm_and_enable' => 'Confirm and Enable',
+    'mfa_gen_backup_codes_title' => 'Backup Codes Setup',
+    'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.',
+    'mfa_gen_backup_codes_download' => 'Download Codes',
+    'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once',
+    'mfa_gen_totp_title' => 'Mobile App Setup',
+    'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
+    'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.',
+    'mfa_gen_totp_verify_setup' => 'Verify Setup',
+    'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:',
+    'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here',
+    'mfa_verify_access' => 'Verify Access',
+    'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.',
+    'mfa_verify_no_methods' => 'No Methods Configured',
+    'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.',
+    'mfa_verify_use_totp' => 'Verify using a mobile app',
+    'mfa_verify_use_backup_codes' => 'Verify using a backup code',
+    'mfa_verify_backup_code' => 'Backup Code',
+    'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:',
+    'mfa_verify_backup_code_enter_here' => 'Enter backup code here',
+    'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:',
+    'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.',
 ];
\ No newline at end of file
index 1757d955ab244b9ae4cad7237f7f996e423a2d27..1435a380d6087bb8bdd623479b1fa74cfdded1e2 100644 (file)
@@ -39,11 +39,12 @@ return [
     'reset' => 'Redefinir',
     'remove' => 'Remover',
     'add' => 'Adicionar',
+    'configure' => 'Configure',
     'fullscreen' => 'Tela cheia',
-    'favourite' => 'Favourite',
-    'unfavourite' => 'Unfavourite',
-    'next' => 'Next',
-    'previous' => 'Previous',
+    'favourite' => 'Favoritos',
+    'unfavourite' => 'Remover dos Favoritos',
+    'next' => 'Seguinte',
+    'previous' => 'Anterior',
 
     // Sort Options
     'sort_options' => 'Opções de Ordenação',
@@ -60,6 +61,7 @@ return [
     'no_activity' => 'Nenhuma atividade a mostrar',
     'no_items' => 'Nenhum item disponível',
     'back_to_top' => 'Voltar ao topo',
+    'skip_to_main_content' => 'Ir para o conteúdo principal',
     'toggle_details' => 'Alternar Detalhes',
     'toggle_thumbnails' => 'Alternar Miniaturas',
     'details' => 'Detalhes',
index a920bfd202c71f110d3347871237568927f79eab..ad58879b5768f1c722853b1379975b9592cd0c2b 100644 (file)
@@ -36,6 +36,7 @@ return [
     'export_html' => 'Arquivo Web Contained',
     'export_pdf' => 'Arquivo PDF',
     'export_text' => 'Arquivo Texto',
+    'export_md' => 'Markdown File',
 
     // Permissions and restrictions
     'permissions' => 'Permissões',
@@ -98,6 +99,7 @@ return [
     'shelves_permissions' => 'Permissões da Prateleira',
     'shelves_permissions_updated' => 'Permissões da Prateleira Atualizadas',
     'shelves_permissions_active' => 'Permissões da Prateleira Ativas',
+    'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',
     'shelves_copy_permissions_to_books' => 'Copiar Permissões para Livros',
     'shelves_copy_permissions' => 'Copiar Permissões',
     'shelves_copy_permissions_explain' => 'Isto aplicará as configurações de permissões atuais desta prateleira a todos os livros contidos nela. Antes de ativar, assegure-se de que quaisquer alterações nas permissões desta prateleira tenham sido salvas.',
index f06ffa835957ba6940bdfb9ac130b62b9ba55530..c5b113da3f7d5b029c772e6f71969c9c979750ad 100644 (file)
@@ -92,6 +92,7 @@ return [
     '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_parent' => 'Parent',
     'recycle_bin_deleted_by' => 'Excluído por',
     'recycle_bin_deleted_at' => 'Momento de Exclusão',
     'recycle_bin_permanently_delete' => 'Excluir permanentemente',
@@ -104,6 +105,7 @@ return [
     'recycle_bin_restore_list' => 'Itens a serem restaurados',
     '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_restore_parent' => 'Restore Parent',
     'recycle_bin_destroy_notification' => 'Deleted :count total items from the recycle bin.',
     'recycle_bin_restore_notification' => 'Restored :count total items from the recycle bin.',
 
@@ -117,6 +119,7 @@ return [
     'audit_table_user' => 'Usuário',
     'audit_table_event' => 'Evento',
     'audit_table_related' => 'Related Item or Detail',
+    'audit_table_ip' => 'IP Address',
     'audit_table_date' => 'Data da Atividade',
     'audit_date_from' => 'Date Range From',
     'audit_date_to' => 'Date Range To',
@@ -136,6 +139,7 @@ return [
     'role_details' => 'Detalhes do Cargo',
     'role_name' => 'Nome do Cargo',
     'role_desc' => 'Breve Descrição do Cargo',
+    'role_mfa_enforced' => 'Requires Multi-Factor Authentication',
     'role_external_auth_id' => 'IDs de Autenticação Externa',
     'role_system' => 'Permissões do Sistema',
     'role_manage_users' => 'Gerenciar usuários',
@@ -145,6 +149,7 @@ return [
     'role_manage_page_templates' => 'Gerenciar modelos de página',
     'role_access_api' => 'Acessar API do sistema',
     'role_manage_settings' => 'Gerenciar configurações da aplicação',
+    'role_export_content' => 'Export content',
     'role_asset' => 'Permissões de Ativos',
     'roles_system_warning' => 'Esteja ciente de que o acesso a qualquer uma das três permissões acima pode permitir que um usuário altere seus próprios privilégios ou privilégios de outros usuários no sistema. Apenas atribua cargos com essas permissões para usuários confiáveis.',
     'role_asset_desc' => 'Essas permissões controlam o acesso padrão para os ativos dentro do sistema. Permissões em Livros, Capítulos e Páginas serão sobrescritas por essas permissões.',
@@ -202,6 +207,10 @@ return [
     'users_api_tokens_create' => 'Criar Token',
     'users_api_tokens_expires' => 'Expira',
     'users_api_tokens_docs' => 'Documentação da API',
+    'users_mfa' => 'Multi-Factor Authentication',
+    'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
+    'users_mfa_x_methods' => ':count method configured|:count methods configured',
+    'users_mfa_configure' => 'Configure Methods',
 
     // API Tokens
     'user_api_token_create' => 'Criar Token de API',
@@ -247,6 +256,7 @@ return [
         'it' => 'Italian',
         'ja' => '日本語',
         'ko' => '한국어',
+        'lt' => 'Lietuvių Kalba',
         'lv' => 'Latviešu Valoda',
         'nl' => 'Nederlands',
         'nb' => 'Norsk (Bokmål)',
index ea3779c78874efda0f86b0c0ae69b2895eec1bd1..4bf85d7cfd1bad2cb18974b1222f6658cb5de750 100644 (file)
@@ -15,6 +15,7 @@ return [
     'alpha_dash'           => 'O campo :attribute deve conter apenas letras, números, traços e underlines.',
     'alpha_num'            => 'O campo :attribute deve conter apenas letras e números.',
     'array'                => 'O campo :attribute deve ser uma array.',
+    'backup_codes'         => 'The provided code is not valid or has already been used.',
     'before'               => 'O campo :attribute deve ser uma data anterior à data :date.',
     'between'              => [
         'numeric' => 'O campo :attribute deve estar entre :min e :max.',
@@ -98,6 +99,7 @@ return [
     ],
     'string'               => 'O campo :attribute deve ser uma string.',
     'timezone'             => 'O campo :attribute deve conter uma timezone válida.',
+    'totp'                 => 'The provided code is not valid or has expired.',
     'unique'               => 'Já existe um campo/dado de nome :attribute.',
     'url'                  => 'O formato da URL :attribute é inválido.',
     'uploaded'             => 'O arquivo não pôde ser carregado. O servidor pode não aceitar arquivos deste tamanho.',
index 4f9fb07e9777317c0d8978cd605b04a2d1970625..0a43afc5a543039cf9019fdb3e0cd240cec82147 100644 (file)
@@ -45,7 +45,11 @@ return [
 
     // Favourites
     'favourite_add_notification' => '":name" добавлено в избранное',
-    'favourite_remove_notification' => ':name" удалено из избранного',
+    'favourite_remove_notification' => '":name" удалено из избранного',
+
+    // MFA
+    'mfa_setup_method_notification' => 'Двухфакторный метод авторизации успешно настроен',
+    'mfa_remove_method_notification' => 'Двухфакторный метод авторизации успешно удален',
 
     // Other
     'commented_on'                => 'прокомментировал',
index 1f0ec6b802d707e4f41c16f3120619bfce72f6a1..8410e40e4503771e688df1be1bd5474305224262 100644 (file)
@@ -73,5 +73,40 @@ return [
     'user_invite_page_welcome' => 'Добро пожаловать в :appName!',
     'user_invite_page_text' => 'Завершите настройку аккаунта, установите пароль для дальнейшего входа в :appName.',
     'user_invite_page_confirm_button' => 'Подтвердите пароль',
-    'user_invite_success' => 'Пароль установлен, теперь у вас есть доступ к :appName!'
+    'user_invite_success' => 'Пароль установлен, теперь у вас есть доступ к :appName!',
+
+    // Multi-factor Authentication
+    'mfa_setup' => 'Двухфакторная аутентификация',
+    'mfa_setup_desc' => 'Двухфакторная аутентификация повышает степень безопасности вашей учетной записи.',
+    'mfa_setup_configured' => 'Настроено',
+    'mfa_setup_reconfigure' => 'Перенастроить',
+    'mfa_setup_remove_confirmation' => 'Вы уверены, что хотите удалить этот двухфакторный метод аутентификации?',
+    'mfa_setup_action' => 'Настройка',
+    'mfa_backup_codes_usage_limit_warning' => 'У вас осталось менее 5 резервных кодов, пожалуйста, создайте и сохраните новый набор перед тем, как закончатся коды, чтобы предотвратить блокировку вашей учетной записи.',
+    'mfa_option_totp_title' => 'Мобильное приложение',
+    'mfa_option_totp_desc' => 'Для использования двухфакторной аутентификации вам понадобится мобильное приложение, поддерживающее TOTP, например Google Authenticator, Authy или Microsoft Authenticator.',
+    'mfa_option_backup_codes_title' => 'Резервные коды',
+    'mfa_option_backup_codes_desc' => 'Безопасно хранить набор одноразовых резервных кодов, которые вы можете ввести для проверки вашей личности.',
+    'mfa_gen_confirm_and_enable' => 'Подтвердить и включить',
+    'mfa_gen_backup_codes_title' => 'Настройка резервных кодов',
+    'mfa_gen_backup_codes_desc' => 'Сохраните приведенный ниже список кодов в безопасном месте. При доступе к системе вы сможете использовать один из кодов в качестве второго механизма аутентификации.',
+    'mfa_gen_backup_codes_download' => 'Скачать коды',
+    'mfa_gen_backup_codes_usage_warning' => 'Каждый код может быть использован только один раз',
+    'mfa_gen_totp_title' => 'Настройка мобильного приложения',
+    'mfa_gen_totp_desc' => 'Для использования двухфакторной аутентификации вам понадобится мобильное приложение, поддерживающее TOTP, например Google Authenticator, Authy или Microsoft Authenticator.',
+    'mfa_gen_totp_scan' => 'Отсканируйте QR-код, используя приложение для аутентификации.',
+    'mfa_gen_totp_verify_setup' => 'Проверить настройки',
+    'mfa_gen_totp_verify_setup_desc' => 'Проверьте, что все работает введя код, сгенерированный внутри вашего приложения для аутентификации, в поле ввода ниже:',
+    'mfa_gen_totp_provide_code_here' => 'Введите код, сгенерированный приложением',
+    'mfa_verify_access' => 'Подтвердите доступ',
+    'mfa_verify_access_desc' => 'Ваша учетная запись требует подтверждения личности на дополнительном уровне верификации, прежде чем вам будет предоставлен доступ. Для продолжения подтвердите вход, используя один из настроенных методов.',
+    'mfa_verify_no_methods' => 'Методы не настроены',
+    'mfa_verify_no_methods_desc' => 'Для вашей учетной записи не найдены двухфакторные методы аутентификации. Вам нужно настроить хотя бы один метод, прежде чем получить доступ.',
+    'mfa_verify_use_totp' => 'Проверить используя мобильное приложение',
+    'mfa_verify_use_backup_codes' => 'Проверить используя резервный код',
+    'mfa_verify_backup_code' => 'Резервный код',
+    'mfa_verify_backup_code_desc' => 'Введите один из оставшихся резервных кодов ниже:',
+    'mfa_verify_backup_code_enter_here' => 'Введите резервный код',
+    'mfa_verify_totp_desc' => 'Введите код, сгенерированный с помощью мобильного приложения, ниже:',
+    'mfa_setup_login_notification' => 'Двухфакторный метод настроен, пожалуйста, войдите снова, используя сконфигурированный метод.',
 ];
\ No newline at end of file
index fce83880edb5185c10441ff879533d81be8f4be8..6e2a3193121d495bde1934c54cd424abbf25904e 100644 (file)
@@ -39,6 +39,7 @@ return [
     'reset' => 'Сбросить',
     'remove' => 'Удалить',
     'add' => 'Добавить',
+    'configure' => 'Configure',
     'fullscreen' => 'На весь экран',
     'favourite' => 'Избранное',
     'unfavourite' => 'Убрать из избранного',
@@ -60,6 +61,7 @@ return [
     'no_activity' => 'Нет действий для просмотра',
     'no_items' => 'Нет доступных элементов',
     'back_to_top' => 'Наверх',
+    'skip_to_main_content' => 'Перейти к основному контенту',
     'toggle_details' => 'Подробности',
     'toggle_thumbnails' => 'Миниатюры',
     'details' => 'Детали',
index 6d666b504088ae6143c424f454d70d434b72f05e..42b06931c340cd121c06f0809a50add1eeb93eb5 100644 (file)
@@ -36,6 +36,7 @@ return [
     'export_html' => 'Веб файл',
     'export_pdf' => 'PDF файл',
     'export_text' => 'Текстовый файл',
+    'export_md' => 'Markdown File',
 
     // Permissions and restrictions
     'permissions' => 'Разрешения',
@@ -98,6 +99,7 @@ return [
     'shelves_permissions' => 'Доступы к книжной полке',
     'shelves_permissions_updated' => 'Доступы к книжной полке обновлены',
     'shelves_permissions_active' => 'Действующие разрешения книжной полки',
+    'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',
     'shelves_copy_permissions_to_books' => 'Наследовать доступы книгам',
     'shelves_copy_permissions' => 'Копировать доступы',
     'shelves_copy_permissions_explain' => 'Это применит текущие настройки доступов этой книжной полки ко всем книгам, содержащимся внутри. Перед активацией убедитесь, что все изменения в доступах этой книжной полки сохранены.',
index 676fc54b2e91323c72dd6d8bbc7a0ab24a005a71..e4bd8534094c9506432a34d772ca54332d27d35b 100755 (executable)
@@ -92,6 +92,7 @@ return [
     'recycle_bin' => 'Корзина',
     'recycle_bin_desc' => 'Здесь вы можете восстановить удаленные элементы или навсегда удалить их из системы. Этот список не отфильтрован в отличие от аналогичных списков действий в системе, где применяются фильтры.',
     'recycle_bin_deleted_item' => 'Удаленный элемент',
+    'recycle_bin_deleted_parent' => 'Parent',
     'recycle_bin_deleted_by' => 'Удалён',
     'recycle_bin_deleted_at' => 'Время удаления',
     'recycle_bin_permanently_delete' => 'Удалить навсегда',
@@ -104,6 +105,7 @@ return [
     'recycle_bin_restore_list' => 'Элементы для восстановления',
     'recycle_bin_restore_confirm' => 'Это действие восстановит удаленный элемент, включая дочерние, в исходное место. Если исходное место было удалено и теперь находится в корзине, родительский элемент также необходимо будет восстановить.',
     'recycle_bin_restore_deleted_parent' => 'Родитель этого элемента также был удален. Элементы будут удалены до тех пор, пока этот родитель не будет восстановлен.',
+    'recycle_bin_restore_parent' => 'Restore Parent',
     'recycle_bin_destroy_notification' => 'Удалено :count элементов из корзины.',
     'recycle_bin_restore_notification' => 'Восстановлено :count элементов из корзины',
 
@@ -117,6 +119,7 @@ return [
     'audit_table_user' => 'Пользователь',
     'audit_table_event' => 'Событие',
     'audit_table_related' => 'Связанный элемент',
+    'audit_table_ip' => 'IP-адрес',
     'audit_table_date' => 'Дата действия',
     'audit_date_from' => 'Диапазон даты от',
     'audit_date_to' => 'Диапазон даты до',
@@ -136,6 +139,7 @@ return [
     'role_details' => 'Детали роли',
     'role_name' => 'Название роли',
     'role_desc' => 'Краткое описание роли',
+    'role_mfa_enforced' => 'Requires Multi-Factor Authentication',
     'role_external_auth_id' => 'Внешние ID авторизации',
     'role_system' => 'Системные разрешения',
     'role_manage_users' => 'Управление пользователями',
@@ -145,6 +149,7 @@ return [
     'role_manage_page_templates' => 'Управление шаблонами страниц',
     'role_access_api' => 'Доступ к системному API',
     'role_manage_settings' => 'Управление настройками приложения',
+    'role_export_content' => 'Export content',
     'role_asset' => 'Права доступа к материалам',
     'roles_system_warning' => 'Имейте в виду, что доступ к любому из указанных выше трех разрешений может позволить пользователю изменить свои собственные привилегии или привилегии других пользователей системы. Назначать роли с этими правами можно только доверенным пользователям.',
     'role_asset_desc' => 'Эти разрешения контролируют доступ по умолчанию к параметрам внутри системы. Разрешения на книги, главы и страницы перезапишут эти разрешения.',
@@ -202,6 +207,10 @@ return [
     'users_api_tokens_create' => 'Создать токен',
     'users_api_tokens_expires' => 'Истекает',
     'users_api_tokens_docs' => 'Документация',
+    'users_mfa' => 'Двухфакторная аутентификация',
+    'users_mfa_desc' => 'Двухфакторная аутентификация повышает степень безопасности вашей учетной записи.',
+    'users_mfa_x_methods' => ':count method configured|:count methods configured',
+    'users_mfa_configure' => 'Настройка методов',
 
     // API Tokens
     'user_api_token_create' => 'Создать токен',
@@ -247,6 +256,7 @@ return [
         'it' => 'Italian',
         'ja' => '日本語',
         'ko' => '한국어',
+        'lt' => 'Lietuvių Kalba',
         'lv' => 'Latviešu Valoda',
         'nl' => 'Nederlands',
         'nb' => 'Norsk (Bokmål)',
index 8c583f7e7820eb72d748ac62ec1d94c6dfb05198..45cc96155dbbff435580c6421a3e3509163cd487 100644 (file)
@@ -15,6 +15,7 @@ return [
     'alpha_dash'           => ':attribute может содержать только буквы, цифры и тире.',
     'alpha_num'            => ':attribute должен содержать только буквы и цифры.',
     'array'                => ':attribute должен быть массивом.',
+    'backup_codes'         => 'Указанный код недействителен или уже использован.',
     'before'               => ':attribute дата должна быть до :date.',
     'between'              => [
         'numeric' => ':attribute должен быть между :min и :max.',
@@ -98,6 +99,7 @@ return [
     ],
     'string'               => ':attribute должен быть строкой.',
     'timezone'             => ':attribute должен быть корректным часовым поясом.',
+    'totp'                 => 'Указанный код недействителен или истек.',
     'unique'               => ':attribute уже есть.',
     'url'                  => 'Формат :attribute некорректен.',
     'uploaded'             => 'Не удалось загрузить файл. Сервер не может принимать файлы такого размера.',
index 15897e79107f6cfaad6223093401b1add2e4438c..9f9ded00e731a110a8913200dd4556efb9f36050 100644 (file)
@@ -8,7 +8,7 @@ return [
     // Pages
     'page_create'                 => 'vytvoril(a) stránku',
     'page_create_notification'    => 'Stránka úspešne vytvorená',
-    'page_update'                 => 'aktualizoval stránku',
+    'page_update'                 => 'aktualizoval(a) stránku',
     'page_update_notification'    => 'Stránka úspešne aktualizovaná',
     'page_delete'                 => 'odstránil(a) stránku',
     'page_delete_notification'    => 'Stránka úspešne odstránená',
@@ -44,8 +44,12 @@ return [
     'bookshelf_delete_notification'    => 'Knižnica úspešne odstránená',
 
     // Favourites
-    'favourite_add_notification' => '":name" has been added to your favourites',
-    'favourite_remove_notification' => '":name" has been removed from your favourites',
+    'favourite_add_notification' => '":name" bol pridaný medzi obľúbené',
+    'favourite_remove_notification' => '":name" bol odstránený z obľúbených',
+
+    // MFA
+    'mfa_setup_method_notification' => 'Viacúrovňový spôsob overenia úspešne nastavený',
+    'mfa_remove_method_notification' => 'Viacúrovňový spôsob overenia úspešne odstránený',
 
     // Other
     'commented_on'                => 'komentoval(a)',
index 0d96811a3671aa6bcad8ccf2e80d4ba537c0b464..f79e79cca940747ceda6628f6801755ec9985fff 100644 (file)
@@ -73,5 +73,40 @@ return [
     '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' => 'Heslo bolo nastavené, teraz máte prístup k :appName!'
+    'user_invite_success' => 'Heslo bolo nastavené, teraz máte prístup k :appName!',
+
+    // Multi-factor Authentication
+    'mfa_setup' => 'Nastaviť viacúrovňové prihlasovanie',
+    'mfa_setup_desc' => 'Pre vyššiu úroveň bezpečnosti si nastavte viacúrovňové prihlasovanie.',
+    'mfa_setup_configured' => 'Už nastavené',
+    'mfa_setup_reconfigure' => 'Znovunastavenie',
+    'mfa_setup_remove_confirmation' => 'Ste si istý, že chcete odstrániť tento spôsob viacúrovňového overenia?',
+    'mfa_setup_action' => 'Nastaveine',
+    'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.',
+    'mfa_option_totp_title' => 'Mobilná aplikácia',
+    'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
+    'mfa_option_backup_codes_title' => 'Záložné kódy',
+    'mfa_option_backup_codes_desc' => 'Bezpečne uložte jednorázové záložné kódy pre overenie vačej identity.',
+    'mfa_gen_confirm_and_enable' => 'Potvrdiť a zapnúť',
+    'mfa_gen_backup_codes_title' => 'Nastavenie záložných kódov',
+    'mfa_gen_backup_codes_desc' => 'Uložte si tieto kódy na bezpečné miesto. Jeden z kódov budete môcť použiť ako druhý faktor overenia identiy na prihlásenie sa.',
+    'mfa_gen_backup_codes_download' => 'Stiahnuť kódy',
+    'mfa_gen_backup_codes_usage_warning' => 'Každý kód môže byť použitý len jeden krát',
+    'mfa_gen_totp_title' => 'Nastavenie mobilnej aplikácie',
+    'mfa_gen_totp_desc' => 'Pre používanie viacúrovňového prihlasovania budete potrebovať mobilnú aplikáciu, ktorá podporuje TOPS ako napríklad Google Authenticator, Authy alebo Microsoft Authenticator.',
+    'mfa_gen_totp_scan' => 'Naskenujte 1R k\'d pomocou vašej mobilnej aplikácie.',
+    'mfa_gen_totp_verify_setup' => 'Overiť nastavenie',
+    'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:',
+    'mfa_gen_totp_provide_code_here' => 'Sem vložte kód vygenerovaný vašou mobilnou aplikáciou',
+    'mfa_verify_access' => 'Overiť prístup',
+    'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.',
+    'mfa_verify_no_methods' => 'Žiadny spôsob nebol nastavený',
+    'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.',
+    'mfa_verify_use_totp' => 'Overiť pomocou mobilnej aplikácie',
+    'mfa_verify_use_backup_codes' => 'Overiť pomocou záložného kódu',
+    'mfa_verify_backup_code' => 'Záložný kód',
+    'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:',
+    'mfa_verify_backup_code_enter_here' => 'Zadajte záložný kód',
+    'mfa_verify_totp_desc' => 'Zadajte kód vygenerovaný vašou mobilnou aplikáciou:',
+    'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.',
 ];
\ No newline at end of file
index 7a2051688d9ea94b12d7319ac4d06b4e3e174fa9..b9913db5942ec5a6b30c29c9f69d09a7487dd844 100644 (file)
@@ -39,11 +39,12 @@ return [
     'reset' => 'Resetovať',
     'remove' => 'Odstrániť',
     'add' => 'Pridať',
+    'configure' => 'Konfigurácia',
     'fullscreen' => 'Celá obrazovka',
-    'favourite' => 'Favourite',
-    'unfavourite' => 'Unfavourite',
-    'next' => 'Next',
-    'previous' => 'Previous',
+    'favourite' => 'Pridať do obľúbených',
+    'unfavourite' => 'Odstrániť z obľúbených',
+    'next' => 'Ďalej',
+    'previous' => 'Späť',
 
     // Sort Options
     'sort_options' => 'Možnosti triedenia',
@@ -51,7 +52,7 @@ return [
     'sort_ascending' => 'Zoradiť vzostupne',
     'sort_descending' => 'Zoradiť zostupne',
     'sort_name' => 'Meno',
-    'sort_default' => 'Default',
+    'sort_default' => 'Východzie',
     'sort_created_at' => 'Dátum vytvorenia',
     'sort_updated_at' => 'Aktualizované dňa',
 
@@ -60,6 +61,7 @@ return [
     'no_activity' => 'Žiadna aktivita na zobrazenie',
     'no_items' => 'Žiadne položky nie sú dostupné',
     'back_to_top' => 'Späť nahor',
+    'skip_to_main_content' => 'Preskočiť na hlavný obsah',
     'toggle_details' => 'Prepnúť detaily',
     'toggle_thumbnails' => 'Prepnúť náhľady',
     'details' => 'Podrobnosti',
@@ -69,7 +71,7 @@ return [
     'breadcrumb' => 'Breadcrumb',
 
     // Header
-    'header_menu_expand' => 'Expand Header Menu',
+    'header_menu_expand' => 'Rozbaliť menu v záhlaví',
     'profile_menu' => 'Menu profilu',
     'view_profile' => 'Zobraziť profil',
     'edit_profile' => 'Upraviť profil',
@@ -78,9 +80,9 @@ return [
 
     // Layout tabs
     'tab_info' => 'Informácie',
-    'tab_info_label' => 'Tab: Show Secondary Information',
+    'tab_info_label' => 'Tab: Zobraziť vedľajšie informácie',
     'tab_content' => 'Obsah',
-    'tab_content_label' => 'Tab: Show Primary Content',
+    'tab_content_label' => 'Tab: Zobraziť hlavné informácie',
 
     // Email Content
     'email_action_help' => 'Ak máte problém klinkúť na tlačidlo ":actionText", skopírujte a vložte URL uvedenú nižšie do Vášho prehliadača:',
@@ -88,6 +90,6 @@ return [
 
     // Footer Link Options
     // Not directly used but available for convenience to users.
-    'privacy_policy' => 'Privacy Policy',
-    'terms_of_service' => 'Terms of Service',
+    'privacy_policy' => 'Zásady ochrany osobných údajov',
+    'terms_of_service' => 'Podmienky používania',
 ];
index 7550255a9a004316f59bf095cd38bb5a49c60774..0d430dcf61378273fff0c35a82cda7aaa33f943d 100644 (file)
@@ -22,13 +22,13 @@ 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',
+    'meta_owned_name' => 'Vlastník :user',
     'entity_select' => 'Entita vybraná',
     'images' => 'Obrázky',
     'my_recent_drafts' => 'Moje nedávne koncepty',
     'my_recently_viewed' => 'Nedávno mnou zobrazené',
-    'my_most_viewed_favourites' => 'My Most Viewed Favourites',
-    'my_favourites' => 'My Favourites',
+    'my_most_viewed_favourites' => 'Moje najčastejšie zobrazené obľubené',
+    'my_favourites' => 'Moje obľúbené',
     'no_pages_viewed' => 'Nepozreli ste si žiadne stránky',
     'no_pages_recently_created' => 'Žiadne stránky neboli nedávno vytvorené',
     'no_pages_recently_updated' => 'Žiadne stránky neboli nedávno aktualizované',
@@ -36,13 +36,14 @@ return [
     'export_html' => 'Obsahovaný webový súbor',
     'export_pdf' => 'PDF súbor',
     'export_text' => 'Súbor s čistým textom',
+    'export_md' => 'Súbor Markdown',
 
     // Permissions and restrictions
     'permissions' => 'Oprávnenia',
     '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',
+    'permissions_owner' => 'Vlastník',
 
     // Search
     'search_results' => 'Výsledky hľadania',
@@ -62,7 +63,7 @@ return [
     'search_permissions_set' => 'Oprávnenia',
     'search_created_by_me' => 'Vytvorené mnou',
     'search_updated_by_me' => 'Aktualizované mnou',
-    'search_owned_by_me' => 'Owned by me',
+    'search_owned_by_me' => 'Patriace mne',
     'search_date_options' => 'Možnosti dátumu',
     'search_updated_before' => 'Aktualizované pred',
     'search_updated_after' => 'Aktualizované po',
@@ -98,22 +99,23 @@ return [
     'shelves_permissions' => 'Oprávnenia knižnice',
     'shelves_permissions_updated' => 'Oprávnenia knižnice aktualizované',
     'shelves_permissions_active' => 'Oprávnenia knižnice aktívne',
-    'shelves_copy_permissions_to_books' => 'Copy Permissions to Books',
-    'shelves_copy_permissions' => 'Copy Permissions',
+    'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',
+    'shelves_copy_permissions_to_books' => 'Kopírovať oprávnenia pre knihy',
+    'shelves_copy_permissions' => 'Kopírovať oprávnenia',
     '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_copy_permission_success' => 'Oprávnenia knižnice boli skopírované {0}:count kníh|{1}:count kniha|[2,3,4]:count knihy|[5,*]:count kníh',
 
     // Books
     'book' => 'Kniha',
     'books' => 'Knihy',
-    'x_books' => ':count Book|:count Books',
+    'x_books' => '{0}:count kníh|{1}:count kniha|[2,3,4]:count knihy|[5,*]:count kníh',
     'books_empty' => 'Žiadne knihy neboli vytvorené',
     'books_popular' => 'Populárne knihy',
     'books_recent' => 'Nedávne knihy',
-    'books_new' => 'New Books',
-    'books_new_action' => 'New Book',
+    'books_new' => 'Nové knihy',
+    'books_new_action' => 'Nová kniha',
     'books_popular_empty' => 'Najpopulárnejšie knihy sa objavia tu.',
-    'books_new_empty' => 'The most recently created books will appear here.',
+    'books_new_empty' => 'Najnovšie knihy sa zobrazia tu.',
     'books_create' => 'Vytvoriť novú knihu',
     'books_delete' => 'Zmazať knihu',
     'books_delete_named' => 'Zmazať knihu :bookName',
@@ -134,24 +136,24 @@ return [
     'books_navigation' => 'Navigácia knihy',
     'books_sort' => 'Zoradiť obsah knihy',
     'books_sort_named' => 'Zoradiť knihu :bookName',
-    'books_sort_name' => 'Sort by Name',
-    'books_sort_created' => 'Sort by Created Date',
-    'books_sort_updated' => 'Sort by Updated Date',
-    'books_sort_chapters_first' => 'Chapters First',
-    'books_sort_chapters_last' => 'Chapters Last',
+    'books_sort_name' => 'Zoradiť podľa mena',
+    'books_sort_created' => 'Zoradiť podľa dátumu vytvorenia',
+    'books_sort_updated' => 'Zoradiť podľa dátumu aktualizácie',
+    'books_sort_chapters_first' => 'Kapitoly ako prvé',
+    'books_sort_chapters_last' => 'Kapitoly ako posledné',
     'books_sort_show_other' => 'Zobraziť ostatné knihy',
     'books_sort_save' => 'Uložiť nové zoradenie',
 
     // Chapters
     'chapter' => 'Kapitola',
     'chapters' => 'Kapitoly',
-    'x_chapters' => ':count Chapter|:count Chapters',
+    'x_chapters' => '{0}:count Kapitol|{1}:count Kapitola|[2,3,4]:count Kapitoly|[5,*]:count Kapitol',
     'chapters_popular' => 'Populárne kapitoly',
     'chapters_new' => 'Nová kapitola',
     'chapters_create' => 'Vytvoriť novú kapitolu',
     'chapters_delete' => 'Zmazať kapitolu',
     'chapters_delete_named' => 'Zmazať kapitolu :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_explain' => 'Týmto sa odstráni kapitola s názvom \':chapterName\'. Spolu s ňou sa odstránia všetky stránky v tejto kapitole.',
     'chapters_delete_confirm' => 'Ste si istý, že chcete zmazať túto kapitolu?',
     'chapters_edit' => 'Upraviť kapitolu',
     'chapters_edit_named' => 'Upraviť kapitolu :chapterName',
@@ -163,7 +165,7 @@ return [
     'chapters_empty' => 'V tejto kapitole nie sú teraz žiadne stránky.',
     'chapters_permissions_active' => 'Oprávnenia kapitoly aktívne',
     'chapters_permissions_success' => 'Oprávnenia kapitoly aktualizované',
-    'chapters_search_this' => 'Search this chapter',
+    'chapters_search_this' => 'Hladať v kapitole',
 
     // Pages
     'page' => 'Stránka',
@@ -182,7 +184,7 @@ return [
     'pages_delete_confirm' => 'Ste si istý, že chcete zmazať túto stránku?',
     'pages_delete_draft_confirm' => 'Ste si istý, že chcete zmazať tento koncept stránky?',
     'pages_editing_named' => 'Upraviť stránku :pageName',
-    'pages_edit_draft_options' => 'Draft Options',
+    'pages_edit_draft_options' => 'Možnosti konceptu',
     'pages_edit_save_draft' => 'Uložiť koncept',
     'pages_edit_draft' => 'Upraviť koncept stránky',
     'pages_editing_draft' => 'Upravuje sa koncept',
@@ -200,25 +202,25 @@ return [
     'pages_md_preview' => 'Náhľad',
     'pages_md_insert_image' => 'Vložiť obrázok',
     'pages_md_insert_link' => 'Vložiť odkaz na entitu',
-    'pages_md_insert_drawing' => 'Insert Drawing',
+    'pages_md_insert_drawing' => 'Vložiť kresbu',
     'pages_not_in_chapter' => 'Stránka nie je v kapitole',
     'pages_move' => 'Presunúť stránku',
     'pages_move_success' => 'Stránka presunutá do ":parentName"',
-    'pages_copy' => 'Copy Page',
-    'pages_copy_desination' => 'Copy Destination',
-    'pages_copy_success' => 'Page successfully copied',
+    'pages_copy' => 'Kpoírovať stránku',
+    'pages_copy_desination' => 'Ciel kopírovania',
+    'pages_copy_success' => 'Stránka bola skopírovaná',
     'pages_permissions' => 'Oprávnenia stránky',
     'pages_permissions_success' => 'Oprávnenia stránky aktualizované',
-    'pages_revision' => 'Revision',
+    'pages_revision' => 'Revízia',
     '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_revision_restored_from' => 'Obnovené z #:id; :summary',
     'pages_revisions_created_by' => 'Vytvoril',
     'pages_revisions_date' => 'Dátum revízie',
-    'pages_revisions_number' => '#',
-    'pages_revisions_numbered' => 'Revision #:id',
-    'pages_revisions_numbered_changes' => 'Revision #:id Changes',
+    'pages_revisions_number' => 'č.',
+    'pages_revisions_numbered' => 'Revízia č. :id',
+    'pages_revisions_numbered_changes' => 'Zmeny revízie č. ',
     'pages_revisions_changelog' => 'Záznam zmien',
     'pages_revisions_changes' => 'Zmeny',
     'pages_revisions_current' => 'Aktuálna verzia',
@@ -240,21 +242,21 @@ return [
         'message' => ':start :time. Dávajte pozor aby ste si navzájom neprepísali zmeny!',
     ],
     'pages_draft_discarded' => 'Koncept ostránený, aktuálny obsah stránky bol nahraný do editora',
-    'pages_specific' => 'Specific Page',
-    'pages_is_template' => 'Page Template',
+    'pages_specific' => 'Konkrétna stránka',
+    'pages_is_template' => 'Šablóna stránky',
 
     // Editor Sidebar
     'page_tags' => 'Štítky stránok',
-    'chapter_tags' => 'Chapter Tags',
-    'book_tags' => 'Book Tags',
-    'shelf_tags' => 'Shelf Tags',
+    'chapter_tags' => 'Štítky kapitol',
+    'book_tags' => 'Štítky kníh',
+    'shelf_tags' => 'Štítky knižníc',
     'tag' => 'Štítok',
     'tags' =>  'Štítky',
-    'tag_name' =>  'Tag Name',
+    'tag_name' =>  'Názov štítku',
     'tag_value' => 'Hodnota štítku (Voliteľné)',
     'tags_explain' => "Pridajte pár štítkov pre uľahčenie kategorizácie Vášho obsahu. \n Štítku môžete priradiť hodnotu pre ešte lepšiu organizáciu.",
     'tags_add' => 'Pridať ďalší štítok',
-    'tags_remove' => 'Remove this tag',
+    'tags_remove' => 'Odstrániť tento štítok',
     'attachments' => 'Prílohy',
     'attachments_explain' => 'Nahrajte nejaké súbory alebo priložte zopár odkazov pre zobrazenie na Vašej stránke. Budú viditeľné v bočnom paneli.',
     'attachments_explain_instant_save' => 'Zmeny budú okamžite uložené.',
@@ -281,8 +283,8 @@ return [
     'attachments_file_uploaded' => 'Súbor úspešne nahraný',
     'attachments_file_updated' => 'Súbor úspešne aktualizovaný',
     'attachments_link_attached' => 'Odkaz úspešne pripojený k stránke',
-    'templates' => 'Templates',
-    'templates_set_as_template' => 'Page is a template',
+    'templates' => 'Šablóny',
+    'templates_set_as_template' => 'Táto stránka je šablóna',
     'templates_explain_set_as_template' => 'You can set this page as a template so its contents be utilized when creating other pages. Other users will be able to use this template if they have view permissions for this page.',
     'templates_replace_content' => 'Replace page content',
     'templates_append_content' => 'Append to page content',
@@ -299,18 +301,18 @@ return [
     // Comments
     'comment' => 'Komentár',
     'comments' => 'Komentáre',
-    'comment_add' => 'Add Comment',
+    'comment_add' => 'Pridať komentár',
     'comment_placeholder' => 'Tu zadajte svoje pripomienky',
-    'comment_count' => '{0} No Comments|{1} 1 Comment|[2,*] :count Comments',
+    'comment_count' => '{0} Bez komentárov|{1} 1 komentár|[2,3,4] :count komentáre|[5,*] :count komentárov',
     'comment_save' => 'Uložiť komentár',
-    'comment_saving' => 'Saving comment...',
-    'comment_deleting' => 'Deleting comment...',
-    'comment_new' => 'New Comment',
-    'comment_created' => 'commented :createDiff',
+    'comment_saving' => 'Ukladanie komentára...',
+    'comment_deleting' => 'Mazanie komentára...',
+    'comment_new' => 'Nový komentár',
+    'comment_created' => 'komentované :createDiff',
     'comment_updated' => 'Updated :updateDiff by :username',
-    'comment_deleted_success' => 'Comment deleted',
-    'comment_created_success' => 'Comment added',
-    'comment_updated_success' => 'Comment updated',
+    'comment_deleted_success' => 'Komentár odstránený',
+    'comment_created_success' => 'Komentár pridaný',
+    'comment_updated_success' => 'Komentár aktualizovaný',
     'comment_delete_confirm' => 'Ste si istý, že chcete odstrániť tento komentár?',
     'comment_in_reply_to' => 'Odpovedať na :commentId',
 
index e523110edea3a369f546aa8bba7ebf9961b76010..bb30243e8b13da41203b5731d4bc85e1bc3ceb15 100644 (file)
@@ -13,7 +13,7 @@ return [
     'email_already_confirmed' => 'Email bol už overený, skúste sa prihlásiť.',
     'email_confirmation_invalid' => 'Tento potvrdzujúci token nie je platný alebo už bol použitý, skúste sa prosím registrovať znova.',
     'email_confirmation_expired' => 'Potvrdzujúci token expiroval, bol odoslaný nový potvrdzujúci email.',
-    'email_confirmation_awaiting' => 'The email address for the account in use needs to be confirmed',
+    'email_confirmation_awaiting' => 'Potvrďte emailovú adresu pre užívateľský účet',
     'ldap_fail_anonymous' => 'LDAP access failed using anonymous bind',
     'ldap_fail_authed' => 'LDAP access failed using given dn & password details',
     'ldap_extension_not_installed' => 'LDAP PHP extension not installed',
@@ -83,7 +83,7 @@ return [
     '404_page_not_found' => 'Stránka nenájdená',
     'sorry_page_not_found' => 'Prepáčte, stránka ktorú hľadáte nebola nájdená.',
     'sorry_page_not_found_permission_warning' => 'If you expected this page to exist, you might not have permission to view it.',
-    'image_not_found' => 'Image Not Found',
+    'image_not_found' => 'Obrázok nebol nájdený',
     'image_not_found_subtitle' => 'Sorry, The image file you were looking for could not be found.',
     'image_not_found_details' => 'If you expected this image to exist it might have been deleted.',
     'return_home' => 'Vrátiť sa domov',
index eeb237981157bff33915a11940ecc935598ddd20..9ec036802a8a9f77911f249163ba3129ee7bcadc 100644 (file)
@@ -12,15 +12,15 @@ return [
     'settings_save_success' => 'Nastavenia uložené',
 
     // App Settings
-    'app_customization' => 'Customization',
-    'app_features_security' => 'Features & Security',
+    'app_customization' => 'Prispôsobenia',
+    'app_features_security' => 'Funkcie a bezpečnosť',
     'app_name' => 'Názov aplikácia',
     'app_name_desc' => 'Tento názov sa zobrazuje v hlavičke a v emailoch.',
     'app_name_header' => 'Zobraziť názov aplikácie v hlavičke?',
-    'app_public_access' => 'Public Access',
+    'app_public_access' => 'Verejný prístup',
     '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_toggle' => 'Povoliť verejný prístup',
     'app_public_viewing' => 'Povoliť verejné zobrazenie?',
     'app_secure_images' => 'Povoliť nahrávanie súborov so zvýšeným zabezpečením?',
     'app_secure_images_toggle' => 'Enable higher security image uploads',
@@ -34,20 +34,20 @@ return [
     'app_logo_desc' => 'Tento obrázok by mal mať 43px na výšku. <br>Veľké obrázky budú preškálované na menší rozmer.',
     'app_primary_color' => 'Primárna farba pre aplikáciu',
     'app_primary_color_desc' => 'Toto by mala byť hodnota v hex tvare. <br>Nechajte prázdne ak chcete použiť prednastavenú farbu.',
-    'app_homepage' => 'Application Homepage',
+    'app_homepage' => 'Domovská stránka aplikácie',
     '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_homepage_select' => 'Vybrať stránku',
+    'app_footer_links' => 'Odkazy v pätičke',
     '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' => 'Zakázať komentáre',
-    'app_disable_comments_toggle' => 'Disable comments',
+    'app_disable_comments_toggle' => 'Vypnúť komentáre',
     'app_disable_comments_desc' => 'Zakázať komentáre na všetkých stránkach aplikácie. Existujúce komentáre sa nezobrazujú.',
 
     // Color settings
-    'content_colors' => 'Content Colors',
+    'content_colors' => 'Farby obsahu',
     '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',
@@ -57,12 +57,12 @@ return [
 
     // Registration Settings
     'reg_settings' => 'Nastavenia registrácie',
-    'reg_enable' => 'Enable Registration',
-    'reg_enable_toggle' => 'Enable registration',
+    'reg_enable' => 'Povolenie registrácie',
+    'reg_enable_toggle' => 'Povoliť registrácie',
     '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_default_role' => 'Prednastavená používateľská rola po registrácii',
     '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' => 'Potvrdenie e-mailom',
     'reg_email_confirmation_toggle' => 'Require email confirmation',
     'reg_confirm_email_desc' => 'Ak je použité obmedzenie domény, potom bude vyžadované overenie emailu a hodnota nižšie bude ignorovaná.',
     'reg_confirm_restrict_domain' => 'Obmedziť registráciu na doménu',
@@ -70,28 +70,29 @@ return [
     'reg_confirm_restrict_domain_placeholder' => 'Nie sú nastavené žiadne obmedzenia',
 
     // Maintenance settings
-    'maint' => 'Maintenance',
-    'maint_image_cleanup' => 'Cleanup Images',
+    'maint' => 'Údržba',
+    'maint_image_cleanup' => 'Prečistenie obrázkov',
     '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_delete_images_only_in_revisions' => 'Also delete images that only exist in old page revisions',
-    'maint_image_cleanup_run' => 'Run Cleanup',
+    'maint_image_cleanup_run' => 'Spustiť prečistenie',
     '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!',
-    'maint_image_cleanup_nothing_found' => 'No unused images found, Nothing deleted!',
-    'maint_send_test_email' => 'Send a Test Email',
+    'maint_image_cleanup_nothing_found' => 'Žiadne nepoužit obrázky neboli nájdené. Nič sa nezmazalo!',
+    'maint_send_test_email' => 'Odoslať testovací 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_run' => 'Odoslať testovací email',
     'maint_send_test_email_success' => 'Email sent to :address',
-    'maint_send_test_email_mail_subject' => 'Test Email',
+    'maint_send_test_email_mail_subject' => 'Testovací 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',
+    'maint_recycle_bin_open' => 'Otvoriť kôš',
 
     // Recycle Bin
-    'recycle_bin' => 'Recycle Bin',
+    'recycle_bin' => 'Kôš',
     '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_item' => 'Odstránené položky',
+    'recycle_bin_deleted_parent' => 'Parent',
     'recycle_bin_deleted_by' => 'Deleted By',
     'recycle_bin_deleted_at' => 'Deletion Time',
     'recycle_bin_permanently_delete' => 'Permanently Delete',
@@ -104,6 +105,7 @@ return [
     '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_restore_parent' => 'Restore Parent',
     'recycle_bin_destroy_notification' => 'Deleted :count total items from the recycle bin.',
     'recycle_bin_restore_notification' => 'Restored :count total items from the recycle bin.',
 
@@ -114,10 +116,11 @@ 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_user' => 'Užívateľ',
+    'audit_table_event' => 'Udalosť',
     'audit_table_related' => 'Related Item or Detail',
-    'audit_table_date' => 'Activity Date',
+    'audit_table_ip' => 'IP adresa',
+    'audit_table_date' => 'Dátum aktivity',
     'audit_date_from' => 'Date Range From',
     'audit_date_to' => 'Date Range To',
 
@@ -136,6 +139,7 @@ return [
     'role_details' => 'Detaily roly',
     'role_name' => 'Názov roly',
     'role_desc' => 'Krátky popis roly',
+    'role_mfa_enforced' => 'Requires Multi-Factor Authentication',
     'role_external_auth_id' => 'External Authentication IDs',
     'role_system' => 'Systémové oprávnenia',
     'role_manage_users' => 'Spravovať používateľov',
@@ -145,6 +149,7 @@ return [
     'role_manage_page_templates' => 'Manage page templates',
     'role_access_api' => 'Access system API',
     'role_manage_settings' => 'Spravovať nastavenia aplikácie',
+    'role_export_content' => 'Export content',
     'role_asset' => 'Oprávnenia majetku',
     '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' => 'Tieto oprávnenia regulujú prednastavený prístup k zdroju v systéme. Oprávnenia pre knihy, kapitoly a stránky majú vyššiu prioritu.',
@@ -162,13 +167,13 @@ 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_latest_activity' => 'Nedávna aktivita',
+    'users_details' => 'Údaje o používateľovi',
     '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_role' => 'Používateľské roly',
     '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' => 'Heslo používateľa',
     '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',
@@ -202,6 +207,10 @@ return [
     'users_api_tokens_create' => 'Create Token',
     'users_api_tokens_expires' => 'Expires',
     'users_api_tokens_docs' => 'API Documentation',
+    'users_mfa' => 'Multi-Factor Authentication',
+    'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
+    'users_mfa_x_methods' => ':count method configured|:count methods configured',
+    'users_mfa_configure' => 'Configure Methods',
 
     // API Tokens
     'user_api_token_create' => 'Create API Token',
@@ -247,6 +256,7 @@ return [
         'it' => 'Italian',
         'ja' => '日本語',
         'ko' => '한국어',
+        'lt' => 'Lietuvių Kalba',
         'lv' => 'Latviešu Valoda',
         'nl' => 'Nederlands',
         'nb' => 'Norsk (Bokmål)',
index 545313415afbf963738584626e2827ade96a8d09..1f1a8841f382e19d7fb81711b6e42b0de1809172 100644 (file)
@@ -15,6 +15,7 @@ return [
     'alpha_dash'           => ':attribute môže obsahovať iba písmená, čísla a pomlčky.',
     'alpha_num'            => ':attribute môže obsahovať iba písmená a čísla.',
     'array'                => ':attribute musí byť pole.',
+    'backup_codes'         => 'The provided code is not valid or has already been used.',
     'before'               => ':attribute musí byť dátum pred :date.',
     'between'              => [
         'numeric' => ':attribute musí byť medzi :min a :max.',
@@ -98,6 +99,7 @@ return [
     ],
     'string'               => ':attribute musí byť reťazec.',
     'timezone'             => ':attribute musí byť plantá časová zóna.',
+    'totp'                 => 'The provided code is not valid or has expired.',
     'unique'               => ':attribute je už použité.',
     'url'                  => ':attribute formát je neplatný.',
     'uploaded'             => 'The file could not be uploaded. The server may not accept files of this size.',
index 91ec575e8e902ecc271b985f00a7faec9d9805d9..e1926c8ba78b91fd516df4b683a260a58c38c04f 100644 (file)
@@ -47,6 +47,10 @@ return [
     'favourite_add_notification' => '":name" has been added to your favourites',
     'favourite_remove_notification' => '":name" has been removed from your favourites',
 
+    // MFA
+    'mfa_setup_method_notification' => 'Multi-factor method successfully configured',
+    'mfa_remove_method_notification' => 'Multi-factor method successfully removed',
+
     // Other
     'commented_on'                => 'komentar na',
     'permissions_update'          => 'pravice so posodobljene',
index df6fb4227dc665ce4edbc3866217f8b38a432d37..b6c41666ae2d8e53f53b72e1f11402b8a96d8235 100644 (file)
@@ -73,5 +73,40 @@ return [
     'user_invite_page_welcome' => 'Dobrodošli na :appName!',
     'user_invite_page_text' => 'Za zaključiti in pridobiti dostop si morate nastaviti geslo, ki bo uporabljeno za prijavo v :appName.',
     'user_invite_page_confirm_button' => 'Potrdi geslo',
-    'user_invite_success' => 'Geslo nastavljeno, sedaj imaš dostop do :appName!'
+    'user_invite_success' => 'Geslo nastavljeno, sedaj imaš dostop do :appName!',
+
+    // Multi-factor Authentication
+    'mfa_setup' => 'Setup Multi-Factor Authentication',
+    'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
+    'mfa_setup_configured' => 'Already configured',
+    'mfa_setup_reconfigure' => 'Reconfigure',
+    'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?',
+    'mfa_setup_action' => 'Setup',
+    'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.',
+    'mfa_option_totp_title' => 'Mobile App',
+    'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
+    'mfa_option_backup_codes_title' => 'Backup Codes',
+    'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.',
+    'mfa_gen_confirm_and_enable' => 'Confirm and Enable',
+    'mfa_gen_backup_codes_title' => 'Backup Codes Setup',
+    'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.',
+    'mfa_gen_backup_codes_download' => 'Download Codes',
+    'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once',
+    'mfa_gen_totp_title' => 'Mobile App Setup',
+    'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
+    'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.',
+    'mfa_gen_totp_verify_setup' => 'Verify Setup',
+    'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:',
+    'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here',
+    'mfa_verify_access' => 'Verify Access',
+    'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.',
+    'mfa_verify_no_methods' => 'No Methods Configured',
+    'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.',
+    'mfa_verify_use_totp' => 'Verify using a mobile app',
+    'mfa_verify_use_backup_codes' => 'Verify using a backup code',
+    'mfa_verify_backup_code' => 'Backup Code',
+    'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:',
+    'mfa_verify_backup_code_enter_here' => 'Enter backup code here',
+    'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:',
+    'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.',
 ];
\ No newline at end of file
index a34d91b3389164a375dd8a5dce530931516bbc24..9f478e75dd8bf13ac264066da573876e581578be 100644 (file)
@@ -39,6 +39,7 @@ return [
     'reset' => 'Ponastavi',
     'remove' => 'Odstrani',
     'add' => 'Dodaj',
+    'configure' => 'Configure',
     'fullscreen' => 'Celozaslonski način',
     'favourite' => 'Favourite',
     'unfavourite' => 'Unfavourite',
@@ -60,6 +61,7 @@ return [
     'no_activity' => 'Ni aktivnosti za prikaz',
     'no_items' => 'Na voljo ni nobenega elementa',
     'back_to_top' => 'Nazaj na vrh',
+    'skip_to_main_content' => 'Skip to main content',
     'toggle_details' => 'Preklopi podrobnosti',
     'toggle_thumbnails' => 'Preklopi sličice',
     'details' => 'Podrobnosti',
index 699b7c4700a131a6208b71b5add197325bc7ac3e..55f05e231b3ea837d736040a2b8280bbce317142 100644 (file)
@@ -36,6 +36,7 @@ return [
     'export_html' => 'Vsebuje spletno datoteko',
     'export_pdf' => 'PDF datoteka (.pdf)',
     'export_text' => 'Navadna besedilna datoteka',
+    'export_md' => 'Markdown File',
 
     // Permissions and restrictions
     'permissions' => 'Dovoljenja',
@@ -98,6 +99,7 @@ return [
     'shelves_permissions' => 'Dovoljenja knjižnih polic',
     'shelves_permissions_updated' => 'Posodobljena dovoljenja knjižnih polic',
     'shelves_permissions_active' => 'Aktivna dovoljenja knjižnih polic',
+    'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',
     'shelves_copy_permissions_to_books' => 'Kopiraj dovoljenja na knjige',
     '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.',
index dc03828681fd7ee63cc6d0eb662ac0f835208e72..cadba7bce937bd7c041e33607ac5484330a38c27 100644 (file)
@@ -92,6 +92,7 @@ return [
     '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_parent' => 'Parent',
     'recycle_bin_deleted_by' => 'Izbrisal uporabnik',
     'recycle_bin_deleted_at' => 'Čas izbrisa',
     'recycle_bin_permanently_delete' => 'Trajno izbrišem?',
@@ -104,6 +105,7 @@ return [
     '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_restore_parent' => 'Restore Parent',
     'recycle_bin_destroy_notification' => 'Izbrisano :count skupno število elementov iz koša.',
     'recycle_bin_restore_notification' => 'Obnovljeno :count skupno število elementov iz koša.',
 
@@ -117,6 +119,7 @@ return [
     'audit_table_user' => 'Uporabnik',
     'audit_table_event' => 'Dogodek',
     'audit_table_related' => 'Povezani predmet ali podrobnost',
+    'audit_table_ip' => 'IP Address',
     'audit_table_date' => 'Datum zadnje dejavnosti',
     'audit_date_from' => 'Časovno obdobje od',
     'audit_date_to' => 'Časovno obdobje do',
@@ -136,6 +139,7 @@ return [
     'role_details' => 'Podrobnosti vloge',
     'role_name' => 'Naziv vloge',
     'role_desc' => 'Kratki opis vloge',
+    'role_mfa_enforced' => 'Requires Multi-Factor Authentication',
     'role_external_auth_id' => 'Zunanje dokazilo ID',
     'role_system' => 'Sistemska dovoljenja',
     'role_manage_users' => 'Upravljanje uporabnikov',
@@ -145,6 +149,7 @@ return [
     'role_manage_page_templates' => 'Uredi predloge',
     'role_access_api' => 'API za dostop do sistema',
     'role_manage_settings' => 'Nastavitve za upravljanje',
+    'role_export_content' => 'Export content',
     'role_asset' => 'Sistemska dovoljenja',
     '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.',
@@ -202,6 +207,10 @@ return [
     'users_api_tokens_create' => 'Ustvari žeton',
     'users_api_tokens_expires' => 'Poteče',
     'users_api_tokens_docs' => 'API dokumentacija',
+    'users_mfa' => 'Multi-Factor Authentication',
+    'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
+    'users_mfa_x_methods' => ':count method configured|:count methods configured',
+    'users_mfa_configure' => 'Configure Methods',
 
     // API Tokens
     'user_api_token_create' => 'Ustvari žeton',
@@ -248,6 +257,7 @@ return [
         'it' => 'Italian',
         'ja' => '日本語',
         'ko' => '한국어',
+        'lt' => 'Lietuvių Kalba',
         'lv' => 'Latviešu Valoda',
         'nl' => 'Nederlands',
         'nb' => 'Norsk (Bokmål)',
index 9b1a5ff463877dc18d5a0b554baee3427c2630b0..5b08463ca9e0667075aa527cbe4f8f73e17906d5 100644 (file)
@@ -15,6 +15,7 @@ return [
     'alpha_dash'           => ':attribute lahko vsebuje samo ?rke, ?tevilke in ?rtice.',
     'alpha_num'            => ':attribute lahko vsebuje samo črke in številke.',
     'array'                => ':attribute mora biti niz.',
+    'backup_codes'         => 'The provided code is not valid or has already been used.',
     'before'               => ':attribute mora biti datum pred :date.',
     'between'              => [
         'numeric' => ':attribute mora biti med :min in :max.',
@@ -98,6 +99,7 @@ return [
     ],
     'string'               => ':attribute mora biti niz.',
     'timezone'             => ':attribute mora biti veljavna cona.',
+    'totp'                 => 'The provided code is not valid or has expired.',
     'unique'               => ':attribute je že zaseden.',
     'url'                  => ':attribute oblika ni veljavna.',
     'uploaded'             => 'Datoteke ni bilo mogoče naložiti. Strežnik morda ne sprejema datotek te velikosti.',
index 30c157a66d1dd53d244ca1f6d435f06ab3972cfe..a1315dcff15fae4f50a809275a8ec41069570bdf 100644 (file)
@@ -47,6 +47,10 @@ return [
     'favourite_add_notification' => '":name" har lagts till i dina favoriter',
     'favourite_remove_notification' => '":name" har tagits bort från dina favoriter',
 
+    // MFA
+    'mfa_setup_method_notification' => 'Multi-factor method successfully configured',
+    'mfa_remove_method_notification' => 'Multi-factor method successfully removed',
+
     // Other
     'commented_on'                => 'kommenterade',
     'permissions_update'          => 'uppdaterade behörigheter',
index 8669055351722eb127ac674a2a57978f66777ac5..1d1a81c7440ab1126aed82128ec4f35a9aef11cc 100644 (file)
@@ -73,5 +73,40 @@ return [
     'user_invite_page_welcome' => 'Välkommen till :appName!',
     'user_invite_page_text' => 'För att slutföra ditt konto och få åtkomst måste du ange ett lösenord som kommer att användas för att logga in på :appName vid framtida besök.',
     'user_invite_page_confirm_button' => 'Bekräfta lösenord',
-    'user_invite_success' => 'Lösenord satt, du har nu tillgång till :appName!'
+    'user_invite_success' => 'Lösenord satt, du har nu tillgång till :appName!',
+
+    // Multi-factor Authentication
+    'mfa_setup' => 'Setup Multi-Factor Authentication',
+    'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
+    'mfa_setup_configured' => 'Already configured',
+    'mfa_setup_reconfigure' => 'Reconfigure',
+    'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?',
+    'mfa_setup_action' => 'Setup',
+    'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.',
+    'mfa_option_totp_title' => 'Mobile App',
+    'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
+    'mfa_option_backup_codes_title' => 'Backup Codes',
+    'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.',
+    'mfa_gen_confirm_and_enable' => 'Confirm and Enable',
+    'mfa_gen_backup_codes_title' => 'Backup Codes Setup',
+    'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.',
+    'mfa_gen_backup_codes_download' => 'Download Codes',
+    'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once',
+    'mfa_gen_totp_title' => 'Mobile App Setup',
+    'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
+    'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.',
+    'mfa_gen_totp_verify_setup' => 'Verify Setup',
+    'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:',
+    'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here',
+    'mfa_verify_access' => 'Verify Access',
+    'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.',
+    'mfa_verify_no_methods' => 'No Methods Configured',
+    'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.',
+    'mfa_verify_use_totp' => 'Verify using a mobile app',
+    'mfa_verify_use_backup_codes' => 'Verify using a backup code',
+    'mfa_verify_backup_code' => 'Backup Code',
+    'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:',
+    'mfa_verify_backup_code_enter_here' => 'Enter backup code here',
+    'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:',
+    'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.',
 ];
\ No newline at end of file
index 1482eeaab4a1a2d554d89d76b6bb8a319ed145b5..177a8abef1a4f44db652f6927f4aee54cbea2567 100644 (file)
@@ -39,6 +39,7 @@ return [
     'reset' => 'Återställ',
     'remove' => 'Radera',
     'add' => 'Lägg till',
+    'configure' => 'Configure',
     'fullscreen' => 'Helskärm',
     'favourite' => 'Favorit',
     'unfavourite' => 'Ta bort favorit',
@@ -60,6 +61,7 @@ return [
     'no_activity' => 'Ingen aktivitet att visa',
     'no_items' => 'Inga tillgängliga föremål',
     'back_to_top' => 'Tillbaka till toppen',
+    'skip_to_main_content' => 'Skip to main content',
     'toggle_details' => 'Växla detaljer',
     'toggle_thumbnails' => 'Växla miniatyrer',
     'details' => 'Information',
index ce0dfddf2dc8150656f35b294d42326303c6d0f4..30142658397bbac63f9ee6cf9f7aa00f4dcb7693 100644 (file)
@@ -36,6 +36,7 @@ return [
     'export_html' => 'Webb-fil',
     'export_pdf' => 'PDF-fil',
     'export_text' => 'Textfil',
+    'export_md' => 'Markdown File',
 
     // Permissions and restrictions
     'permissions' => 'Rättigheter',
@@ -98,6 +99,7 @@ return [
     'shelves_permissions' => 'Bokhyllerättigheter',
     'shelves_permissions_updated' => 'Bokhyllerättigheterna har ändrats',
     'shelves_permissions_active' => 'Bokhyllerättigheterna är aktiva',
+    'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',
     'shelves_copy_permissions_to_books' => 'Kopiera rättigheter till böcker',
     'shelves_copy_permissions' => 'Kopiera rättigheter',
     'shelves_copy_permissions_explain' => 'Detta kommer kopiera hyllans rättigheter till alla böcker på den. Se till att du har sparat alla ändringar innan du går vidare.',
index d5044efbd36b350b0f0d81c0d3057f136930c151..1aa51ee38d8df0019ad90fb4c3dadb6c9207e5f5 100644 (file)
@@ -92,6 +92,7 @@ return [
     '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_parent' => 'Parent',
     'recycle_bin_deleted_by' => 'Borttagen av',
     'recycle_bin_deleted_at' => 'Tid för borttagning',
     'recycle_bin_permanently_delete' => 'Radera permanent',
@@ -104,6 +105,7 @@ return [
     '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_restore_parent' => 'Restore Parent',
     'recycle_bin_destroy_notification' => 'Raderade :count totala objekt från papperskorgen.',
     'recycle_bin_restore_notification' => 'Återställt :count totala objekt från papperskorgen.',
 
@@ -117,6 +119,7 @@ return [
     'audit_table_user' => 'Användare',
     'audit_table_event' => 'Händelse',
     'audit_table_related' => 'Relaterat objekt eller detalj',
+    'audit_table_ip' => 'IP Address',
     'audit_table_date' => 'Datum för senaste aktiviteten',
     'audit_date_from' => 'Datumintervall från',
     'audit_date_to' => 'Datumintervall till',
@@ -136,6 +139,7 @@ return [
     'role_details' => 'Om rollen',
     'role_name' => 'Rollens namn',
     'role_desc' => 'Kort beskrivning av rollen',
+    'role_mfa_enforced' => 'Requires Multi-Factor Authentication',
     'role_external_auth_id' => 'Externa autentiserings-ID:n',
     'role_system' => 'Systemrättigheter',
     'role_manage_users' => 'Hanter användare',
@@ -145,6 +149,7 @@ return [
     'role_manage_page_templates' => 'Hantera mallar',
     'role_access_api' => 'Åtkomst till systemets API',
     'role_manage_settings' => 'Hantera appinställningar',
+    'role_export_content' => 'Export content',
     'role_asset' => 'Tillgång till innehåll',
     '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.',
@@ -202,6 +207,10 @@ return [
     'users_api_tokens_create' => 'Skapa token',
     'users_api_tokens_expires' => 'Förfaller',
     'users_api_tokens_docs' => 'API-dokumentation',
+    'users_mfa' => 'Multi-Factor Authentication',
+    'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
+    'users_mfa_x_methods' => ':count method configured|:count methods configured',
+    'users_mfa_configure' => 'Configure Methods',
 
     // API Tokens
     'user_api_token_create' => 'Skapa API-nyckel',
@@ -247,6 +256,7 @@ return [
         'it' => 'Italian',
         'ja' => '日本語',
         'ko' => '한국어',
+        'lt' => 'Lietuvių Kalba',
         'lv' => 'Latviešu Valoda',
         'nl' => 'Nederlands',
         'nb' => 'Norsk (Bokmål)',
index da39796bc388e6ad11b8ea6a273e3ada6c2c7f75..0c9cc3164157faed9ed03dea078edf53496c171f 100644 (file)
@@ -15,6 +15,7 @@ return [
     'alpha_dash'           => ':attribute får bara innehålla bokstäver, siffror och bindestreck.',
     'alpha_num'            => ':attribute får bara innehålla bokstäver och siffror.',
     'array'                => ':attribute måste vara en array.',
+    'backup_codes'         => 'The provided code is not valid or has already been used.',
     'before'               => ':attribute måste vara före :date.',
     'between'              => [
         'numeric' => ':attribute måste vara mellan :min och :max.',
@@ -98,6 +99,7 @@ return [
     ],
     'string'               => ':attribute måste vara en sträng.',
     'timezone'             => ':attribute måste vara en giltig tidszon.',
+    'totp'                 => 'The provided code is not valid or has expired.',
     'unique'               => ':attribute är upptaget',
     'url'                  => 'Formatet på :attribute är ogiltigt.',
     'uploaded'             => 'Filen kunde inte laddas upp. Servern kanske inte tillåter filer med denna storlek.',
index 9bcd1891f14a925a4dd8726cfd230968dbbedf22..9a6e60cd40b7559f7c102a96909e920891eff756 100644 (file)
@@ -47,6 +47,10 @@ return [
     'favourite_add_notification' => '":name" favorilerinize eklendi',
     'favourite_remove_notification' => '":name" favorilerinizden çıkarıldı',
 
+    // MFA
+    'mfa_setup_method_notification' => 'Multi-factor method successfully configured',
+    'mfa_remove_method_notification' => 'Multi-factor method successfully removed',
+
     // Other
     'commented_on'                => 'yorum yaptı',
     'permissions_update'          => 'güncellenmiş izinler',
index 6a0e2c1b5ea6bce020997932adb93ebb4699f395..0ce90d4d08013d32c594536c4b50f211b9218b8c 100644 (file)
@@ -73,5 +73,40 @@ return [
     'user_invite_page_welcome' => ':appName uygulamasına hoş geldiniz!',
     'user_invite_page_text' => 'Hesap kurulumunuzu tamamlamak ve gelecekteki :appName ziyaretlerinizde hesabınıza erişim sağlayabilmeniz için bir şifre belirlemeniz gerekiyor.',
     'user_invite_page_confirm_button' => 'Şifreyi Onayla',
-    'user_invite_success' => 'Şifreniz ayarlandı, artık :appName uygulamasına giriş yapabilirsiniz!'
+    'user_invite_success' => 'Şifreniz ayarlandı, artık :appName uygulamasına giriş yapabilirsiniz!',
+
+    // Multi-factor Authentication
+    'mfa_setup' => 'Setup Multi-Factor Authentication',
+    'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
+    'mfa_setup_configured' => 'Already configured',
+    'mfa_setup_reconfigure' => 'Reconfigure',
+    'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?',
+    'mfa_setup_action' => 'Setup',
+    'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.',
+    'mfa_option_totp_title' => 'Mobile App',
+    'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
+    'mfa_option_backup_codes_title' => 'Backup Codes',
+    'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.',
+    'mfa_gen_confirm_and_enable' => 'Confirm and Enable',
+    'mfa_gen_backup_codes_title' => 'Backup Codes Setup',
+    'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.',
+    'mfa_gen_backup_codes_download' => 'Download Codes',
+    'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once',
+    'mfa_gen_totp_title' => 'Mobile App Setup',
+    'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
+    'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.',
+    'mfa_gen_totp_verify_setup' => 'Verify Setup',
+    'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:',
+    'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here',
+    'mfa_verify_access' => 'Verify Access',
+    'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.',
+    'mfa_verify_no_methods' => 'No Methods Configured',
+    'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.',
+    'mfa_verify_use_totp' => 'Verify using a mobile app',
+    'mfa_verify_use_backup_codes' => 'Verify using a backup code',
+    'mfa_verify_backup_code' => 'Backup Code',
+    'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:',
+    'mfa_verify_backup_code_enter_here' => 'Enter backup code here',
+    'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:',
+    'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.',
 ];
\ No newline at end of file
index 50ce70e619d199a980db2c6ca706e59994d3d0b5..1f19a62f406d4093d6b6bc94c078e1ee03d84863 100644 (file)
@@ -39,6 +39,7 @@ return [
     'reset' => 'Sıfırla',
     'remove' => 'Kaldır',
     'add' => 'Ekle',
+    'configure' => 'Configure',
     'fullscreen' => 'Tam Ekran',
     'favourite' => 'Favourite',
     'unfavourite' => 'Unfavourite',
@@ -60,6 +61,7 @@ return [
     'no_activity' => 'Gösterilecek eylem bulunamadı',
     'no_items' => 'Herhangi bir öge bulunamadı',
     'back_to_top' => 'Başa dön',
+    'skip_to_main_content' => 'Skip to main content',
     'toggle_details' => 'Detayları Göster/Gizle',
     'toggle_thumbnails' => 'Ön İzleme Görsellerini Göster/Gizle',
     'details' => 'Detaylar',
index e48277a4bbdfc4d9bbc58691a84964fc4e1b2bbf..770918dd096c7901c7e67e0142ca215ad3bcde28 100644 (file)
@@ -36,6 +36,7 @@ return [
     'export_html' => 'Web Dosyası',
     'export_pdf' => 'PDF Dosyası',
     'export_text' => 'Düz Metin Dosyası',
+    'export_md' => 'Markdown File',
 
     // Permissions and restrictions
     'permissions' => 'İzinler',
@@ -98,6 +99,7 @@ return [
     'shelves_permissions' => 'Kitaplık İzinleri',
     'shelves_permissions_updated' => 'Kitaplık İzinleri Güncellendi',
     'shelves_permissions_active' => 'Kitaplık İzinleri Aktif',
+    'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',
     'shelves_copy_permissions_to_books' => 'İzinleri Kitaplara Kopyala',
     'shelves_copy_permissions' => 'İzinleri Kopyala',
     'shelves_copy_permissions_explain' => 'Bu işlem sonucunda kitaplığınızın izinleri, içerdiği kitaplara da aynen uygulanır. Aktifleştirmeden önce bu kitaplığa ait izinleri kaydettiğinizden emin olun.',
index 4b015c04244331866b7e1f0370feea0e1649e51d..aca4a062891bd01dc51074583f43af85a55ca27b 100755 (executable)
@@ -92,6 +92,7 @@ return [
     '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_parent' => 'Parent',
     'recycle_bin_deleted_by' => 'Tarafından silindi',
     'recycle_bin_deleted_at' => 'Silinme Zamanı',
     'recycle_bin_permanently_delete' => 'Kalıcı Olarak Sil',
@@ -104,6 +105,7 @@ return [
     '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_restore_parent' => 'Restore Parent',
     'recycle_bin_destroy_notification' => 'Deleted :count total items from the recycle bin.',
     'recycle_bin_restore_notification' => 'Restored :count total items from the recycle bin.',
 
@@ -117,6 +119,7 @@ return [
     'audit_table_user' => 'Kullanıcı',
     'audit_table_event' => 'Etkinlik',
     'audit_table_related' => 'İlgili Öğe veya Detay',
+    'audit_table_ip' => 'IP Address',
     'audit_table_date' => 'Aktivite Tarihi',
     'audit_date_from' => 'Tarih Aralığından',
     'audit_date_to' => 'Tarih Aralığına',
@@ -136,6 +139,7 @@ return [
     'role_details' => 'Rol Detayları',
     'role_name' => 'Rol Adı',
     'role_desc' => 'Rolün Kısa Tanımı',
+    'role_mfa_enforced' => 'Requires Multi-Factor Authentication',
     'role_external_auth_id' => 'Harici Doğrulama Kimlikleri',
     'role_system' => 'Sistem Yetkileri',
     'role_manage_users' => 'Kullanıcıları yönet',
@@ -145,6 +149,7 @@ return [
     'role_manage_page_templates' => 'Sayfa şablonlarını yönet',
     'role_access_api' => 'Sistem programlama arayüzüne (API) eriş',
     'role_manage_settings' => 'Uygulama ayarlarını yönet',
+    'role_export_content' => 'Export content',
     'role_asset' => 'Varlık Yetkileri',
     'roles_system_warning' => 'Yukarıdaki üç izinden herhangi birine erişimin, kullanıcının kendi ayrıcalıklarını veya sistemdeki diğerlerinin ayrıcalıklarını değiştirmesine izin verebileceğini unutmayın. Yalnızca bu izinlere sahip rolleri güvenilir kullanıcılara atayın.',
     'role_asset_desc' => 'Bu izinler, sistem içindeki varlıklara varsayılan erişim izinlerini ayarlar. Kitaplar, bölümler ve sayfalar üzerindeki izinler, buradaki izinleri geçersiz kılar.',
@@ -202,6 +207,10 @@ return [
     'users_api_tokens_create' => 'Anahtar Oluştur',
     'users_api_tokens_expires' => 'Bitiş süresi',
     'users_api_tokens_docs' => 'API Dokümantasyonu',
+    'users_mfa' => 'Multi-Factor Authentication',
+    'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
+    'users_mfa_x_methods' => ':count method configured|:count methods configured',
+    'users_mfa_configure' => 'Configure Methods',
 
     // API Tokens
     'user_api_token_create' => 'API Anahtarı Oluştur',
@@ -247,6 +256,7 @@ return [
         'it' => 'Italian',
         'ja' => '日本語',
         'ko' => '한국어',
+        'lt' => 'Lietuvių Kalba',
         'lv' => 'Latviešu Valoda',
         'nl' => 'Nederlands',
         'nb' => 'Norsk (Bokmål)',
index 48bbef92b20b316a7ed926979ddc62290d9f5f80..9cd8093d483cf5af980cb9c93093653b5648326d 100644 (file)
@@ -15,6 +15,7 @@ return [
     'alpha_dash'           => ':attribute sadece harf, rakam ve tirelerden oluşabilir.',
     'alpha_num'            => ':attribute sadece harflerden ve rakamlardan oluşabilir.',
     'array'                => ':attribute bir dizi olmalıdır.',
+    'backup_codes'         => 'The provided code is not valid or has already been used.',
     'before'               => ':attribute tarihi, :date tarihinden önceki bir tarih olmalıdır.',
     'between'              => [
         'numeric' => ':attribute değeri, :min ve :max değerleri arasında olmalıdır.',
@@ -98,6 +99,7 @@ return [
     ],
     'string'               => ':attribute, string olmalıdır.',
     'timezone'             => ':attribute, geçerli bir bölge olmalıdır.',
+    'totp'                 => 'The provided code is not valid or has expired.',
     'unique'               => ':attribute zaten alınmış.',
     'url'                  => ':attribute formatı geçersiz.',
     'uploaded'             => 'Dosya yüklemesi başarısız oldu. Sunucu, bu boyuttaki dosyaları kabul etmiyor olabilir.',
index 918eb1a41f6968a28d02f80093fb072eba739f00..88130b90fdf2be780cb5f1508a9beb2eccafb598 100644 (file)
@@ -47,6 +47,10 @@ return [
     'favourite_add_notification' => '":name" has been added to your favourites',
     'favourite_remove_notification' => '":name" has been removed from your favourites',
 
+    // MFA
+    'mfa_setup_method_notification' => 'Multi-factor method successfully configured',
+    'mfa_remove_method_notification' => 'Multi-factor method successfully removed',
+
     // Other
     'commented_on'                => 'прокоментував',
     'permissions_update'          => 'оновив дозволи',
index e11848a2092125e02ab979bc7e4de2fec4c47dc3..52625b60fba1150d76e716bc26347842ec1a6eec 100644 (file)
@@ -73,5 +73,40 @@ return [
     'user_invite_page_welcome' => 'Ласкаво просимо до :appName!',
     'user_invite_page_text' => 'Для завершення процесу створення облікового запису та отримання доступу вам потрібно задати пароль, який буде використовуватися для входу в :appName в майбутньому.',
     'user_invite_page_confirm_button' => 'Підтвердити пароль',
-    'user_invite_success' => 'Встановлено пароль, тепер у вас є доступ до :appName!'
+    'user_invite_success' => 'Встановлено пароль, тепер у вас є доступ до :appName!',
+
+    // Multi-factor Authentication
+    'mfa_setup' => 'Setup Multi-Factor Authentication',
+    'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
+    'mfa_setup_configured' => 'Already configured',
+    'mfa_setup_reconfigure' => 'Reconfigure',
+    'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?',
+    'mfa_setup_action' => 'Setup',
+    'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.',
+    'mfa_option_totp_title' => 'Mobile App',
+    'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
+    'mfa_option_backup_codes_title' => 'Backup Codes',
+    'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.',
+    'mfa_gen_confirm_and_enable' => 'Confirm and Enable',
+    'mfa_gen_backup_codes_title' => 'Backup Codes Setup',
+    'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.',
+    'mfa_gen_backup_codes_download' => 'Download Codes',
+    'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once',
+    'mfa_gen_totp_title' => 'Mobile App Setup',
+    'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
+    'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.',
+    'mfa_gen_totp_verify_setup' => 'Verify Setup',
+    'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:',
+    'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here',
+    'mfa_verify_access' => 'Verify Access',
+    'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.',
+    'mfa_verify_no_methods' => 'No Methods Configured',
+    'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.',
+    'mfa_verify_use_totp' => 'Verify using a mobile app',
+    'mfa_verify_use_backup_codes' => 'Verify using a backup code',
+    'mfa_verify_backup_code' => 'Backup Code',
+    'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:',
+    'mfa_verify_backup_code_enter_here' => 'Enter backup code here',
+    'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:',
+    'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.',
 ];
\ No newline at end of file
index 62342a723aa882aded4d5ec598e5ea7a479146f0..734c566e55f68e3075367b134fa52f1c47e50965 100644 (file)
@@ -39,6 +39,7 @@ return [
     'reset' => 'Скинути',
     'remove' => 'Видалити',
     'add' => 'Додати',
+    'configure' => 'Configure',
     'fullscreen' => 'На весь екран',
     'favourite' => 'Favourite',
     'unfavourite' => 'Unfavourite',
@@ -60,6 +61,7 @@ return [
     'no_activity' => 'Немає активності для показу',
     'no_items' => 'Немає доступних елементів',
     'back_to_top' => 'Повернутися до початку',
+    'skip_to_main_content' => 'Skip to main content',
     'toggle_details' => 'Подробиці',
     'toggle_thumbnails' => 'Мініатюри',
     'details' => 'Деталі',
index f41d97bb00fb724b426caf93b93440141d13545b..427acc5b180be1a2ed8c1a4214544e6185142dfd 100644 (file)
@@ -36,6 +36,7 @@ return [
     'export_html' => 'Вбудований веб-файл',
     'export_pdf' => 'PDF файл',
     'export_text' => 'Текстовий файл',
+    'export_md' => 'Markdown File',
 
     // Permissions and restrictions
     'permissions' => 'Дозволи',
@@ -98,6 +99,7 @@ return [
     'shelves_permissions' => 'Дозволи на книжкову полицю',
     'shelves_permissions_updated' => 'Дозволи на книжкову полицю оновлено',
     'shelves_permissions_active' => 'Діючі дозволи на книжкову полицю',
+    'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',
     'shelves_copy_permissions_to_books' => 'Копіювати дозволи на книги',
     'shelves_copy_permissions' => 'Копіювати дозволи',
     'shelves_copy_permissions_explain' => 'Це застосовує поточні налаштування дозволів цієї книжкової полиці до всіх книг, що містяться всередині. Перш ніж активувати, переконайтесь що будь-які зміни дозволів цієї книжкової полиці були збережені.',
index fdcd67034f4f9a69316a678cfb7c34470697f443..2c96d4a2b5119f98fe746eca8cd5645e51a0b5b9 100644 (file)
@@ -92,6 +92,7 @@ return [
     'recycle_bin' => 'Кошик',
     'recycle_bin_desc' => 'Тут ви можете відновити видалені елементи, або назавжди видалити їх із системи. Цей список нефільтрований, на відміну від подібних списків активності в системі, де застосовуються фільтри дозволів.',
     'recycle_bin_deleted_item' => 'Виадлений елемент',
+    'recycle_bin_deleted_parent' => 'Parent',
     'recycle_bin_deleted_by' => 'Ким видалено',
     'recycle_bin_deleted_at' => 'Час видалення',
     'recycle_bin_permanently_delete' => 'Видалити остаточно',
@@ -104,6 +105,7 @@ return [
     'recycle_bin_restore_list' => 'Елементи для відновлення',
     'recycle_bin_restore_confirm' => 'Ця дія відновить видалений елемент у початкове місце, включаючи всі дочірні елементи. Якщо вихідне розташування відтоді було видалено, і знаходиться у кошику, батьківський елемент також потрібно буде відновити.',
     'recycle_bin_restore_deleted_parent' => 'Батьківський елемент цього об\'єкта також був видалений. Вони залишатимуться видаленими, доки батьківський елемент також не буде відновлений.',
+    'recycle_bin_restore_parent' => 'Restore Parent',
     'recycle_bin_destroy_notification' => 'Видалено :count елементів із кошика.',
     'recycle_bin_restore_notification' => 'Відновлено :count елементів із кошика.',
 
@@ -117,6 +119,7 @@ return [
     'audit_table_user' => 'Користувач',
     'audit_table_event' => 'Подія',
     'audit_table_related' => 'Пов’язаний елемент',
+    'audit_table_ip' => 'IP Address',
     'audit_table_date' => 'Дата активності',
     'audit_date_from' => 'Діапазон дат від',
     'audit_date_to' => 'Діапазон дат до',
@@ -136,6 +139,7 @@ return [
     'role_details' => 'Деталі ролі',
     'role_name' => 'Назва ролі',
     'role_desc' => 'Короткий опис ролі',
+    'role_mfa_enforced' => 'Requires Multi-Factor Authentication',
     'role_external_auth_id' => 'Зовнішні ID автентифікації',
     'role_system' => 'Системні дозволи',
     'role_manage_users' => 'Керування користувачами',
@@ -145,6 +149,7 @@ return [
     'role_manage_page_templates' => 'Управління шаблонами сторінок',
     'role_access_api' => 'Доступ до системного API',
     'role_manage_settings' => 'Керування налаштуваннями програми',
+    'role_export_content' => 'Export content',
     'role_asset' => 'Дозволи',
     'roles_system_warning' => 'Майте на увазі, що доступ до будь-якого з вищезазначених трьох дозволів може дозволити користувачеві змінювати власні привілеї або привілеї інших в системі. Ролі з цими дозволами призначайте лише довіреним користувачам.',
     'role_asset_desc' => 'Ці дозволи контролюють стандартні доступи всередині системи. Права на книги, розділи та сторінки перевизначать ці дозволи.',
@@ -202,6 +207,10 @@ return [
     'users_api_tokens_create' => 'Створити токен',
     'users_api_tokens_expires' => 'Закінчується',
     'users_api_tokens_docs' => 'Документація API',
+    'users_mfa' => 'Multi-Factor Authentication',
+    'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
+    'users_mfa_x_methods' => ':count method configured|:count methods configured',
+    'users_mfa_configure' => 'Configure Methods',
 
     // API Tokens
     'user_api_token_create' => 'Створити токен API',
@@ -247,6 +256,7 @@ return [
         'it' => 'Italian',
         'ja' => '日本語',
         'ko' => '한국어',
+        'lt' => 'Lietuvių Kalba',
         'lv' => 'Latviešu Valoda',
         'nl' => 'Nederlands',
         'nb' => 'Norsk (Bokmål)',
index 77df1ed4ea627eea4b088b5aaad4b8e67d0af38a..a2c1b9890c2d10d3596907da86550cfb3ffb4bde 100644 (file)
@@ -15,6 +15,7 @@ return [
     'alpha_dash'           => 'Поле :attribute має містити лише літери, цифри, дефіси та підкреслення.',
     'alpha_num'            => 'Поле :attribute має містити лише літери та цифри.',
     'array'                => 'Поле :attribute має бути масивом.',
+    'backup_codes'         => 'The provided code is not valid or has already been used.',
     'before'               => 'Поле :attribute має містити дату не пізніше :date.',
     'between'              => [
         'numeric' => 'Поле :attribute має бути між :min та :max.',
@@ -98,6 +99,7 @@ return [
     ],
     'string'               => 'Поле :attribute повинне містити текст.',
     'timezone'             => 'Поле :attribute повинне містити коректну часову зону.',
+    'totp'                 => 'The provided code is not valid or has expired.',
     'unique'               => 'Вказане значення поля :attribute вже існує.',
     'url'                  => 'Формат поля :attribute неправильний.',
     'uploaded'             => 'Не вдалося завантажити файл. Сервер може не приймати файли такого розміру.',
index 10d24a16b4b9638635d24b2a3b3714c6e3342ea1..255ce38aa10937d1e2414089bd7d41d58f209ff6 100644 (file)
@@ -44,8 +44,12 @@ return [
     'bookshelf_delete_notification'    => 'Giá sách đã được xóa thành công',
 
     // Favourites
-    'favourite_add_notification' => '":name" has been added to your favourites',
-    'favourite_remove_notification' => '":name" has been removed from your favourites',
+    'favourite_add_notification' => '":name" đã được thêm vào danh sách yêu thích của bạn',
+    'favourite_remove_notification' => '":name" đã được gỡ khỏi danh sách yêu thích của bạn',
+
+    // MFA
+    'mfa_setup_method_notification' => 'Cấu hình xác thực nhiều bước thành công',
+    'mfa_remove_method_notification' => 'Đã gỡ xác thực nhiều bước',
 
     // Other
     'commented_on'                => 'đã bình luận về',
index 5ba2db390f6280cc791f345003109c33968a8313..e95d26ac6129bfda9c689c09b07d08e85ed1f5ad 100644 (file)
@@ -73,5 +73,40 @@ return [
     'user_invite_page_welcome' => 'Chào mừng đến với :appName!',
     'user_invite_page_text' => 'Để hoàn tất tài khoản và lấy quyền truy cập bạn cần đặt mật khẩu để sử dụng cho các lần đăng nhập sắp tới tại :appName.',
     'user_invite_page_confirm_button' => 'Xác nhận Mật khẩu',
-    'user_invite_success' => 'Mật khẩu đã được thiết lập, bạn có quyền truy cập đến :appName!'
+    'user_invite_success' => 'Mật khẩu đã được thiết lập, bạn có quyền truy cập đến :appName!',
+
+    // Multi-factor Authentication
+    'mfa_setup' => 'Cài đặt xác thực nhiều bước',
+    'mfa_setup_desc' => 'Cài đặt xác thực nhiều bước như một lớp bảo mật khác cho tài khoản của bạn.',
+    'mfa_setup_configured' => 'Đã cài đặt',
+    'mfa_setup_reconfigure' => 'Cài đặt lại',
+    'mfa_setup_remove_confirmation' => 'Bạn có chắc muốn gỡ bỏ phương thức xác thực nhiều bước này?',
+    'mfa_setup_action' => 'Cài đặt',
+    'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.',
+    'mfa_option_totp_title' => 'Ứng dụng di động',
+    'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
+    'mfa_option_backup_codes_title' => 'Mã dự phòng',
+    'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.',
+    'mfa_gen_confirm_and_enable' => 'Xác nhận và Mở',
+    'mfa_gen_backup_codes_title' => 'Cài đặt Mã dự phòng',
+    'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.',
+    'mfa_gen_backup_codes_download' => 'Tải mã',
+    'mfa_gen_backup_codes_usage_warning' => 'Mỗi mã chỉ có thể sử dụng một lần',
+    'mfa_gen_totp_title' => 'Cài đặt ứng dụng di động',
+    'mfa_gen_totp_desc' => 'Để sử dụng xác thực nhiều bước, bạn cần một ứng dụng di động hỗ trợ TOTP ví dụ như Google Authenticator, Authy hoặc Microsoft Authenticator.',
+    'mfa_gen_totp_scan' => 'Quét mã QR dưới đây bằng ứng dụng xác thực mà bạn muốn để bắt đầu.',
+    'mfa_gen_totp_verify_setup' => 'Xác nhận cài đặt',
+    'mfa_gen_totp_verify_setup_desc' => 'Xác nhận rằng tất cả hoạt động bằng cách nhập vào một mã, được tạo ra bởi ứng dụng xác thực của bạn vào ô dưới đây:',
+    'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here',
+    'mfa_verify_access' => 'Verify Access',
+    'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.',
+    'mfa_verify_no_methods' => 'No Methods Configured',
+    'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.',
+    'mfa_verify_use_totp' => 'Verify using a mobile app',
+    'mfa_verify_use_backup_codes' => 'Verify using a backup code',
+    'mfa_verify_backup_code' => 'Mã dự phòng',
+    'mfa_verify_backup_code_desc' => 'Nhập một trong các mã dự phòng còn lại của bạn vào ô phía dưới:',
+    'mfa_verify_backup_code_enter_here' => 'Nhập mã xác thực của bạn tại đây',
+    'mfa_verify_totp_desc' => 'Nhập mã do ứng dụng di động của bạn tạo ra vào dưới đây:',
+    'mfa_setup_login_notification' => 'Đã cài đặt xác thực nhiều bước, bạn vui lòng đăng nhập lại sử dụng phương thức đã cài đặt.',
 ];
\ No newline at end of file
index 6e779da242d855f40bcac9b219b7d898ddb98f8f..f118d34c3a9e6cb04bda0e0b421976b23d97362d 100644 (file)
@@ -39,11 +39,12 @@ return [
     'reset' => 'Thiết lập lại',
     'remove' => 'Xóa bỏ',
     'add' => 'Thêm',
+    'configure' => 'Cấu hình',
     'fullscreen' => 'Toàn màn hình',
-    'favourite' => 'Favourite',
-    'unfavourite' => 'Unfavourite',
-    'next' => 'Next',
-    'previous' => 'Previous',
+    'favourite' => 'Yêu thích',
+    'unfavourite' => 'Bỏ yêu thích',
+    'next' => 'Tiếp theo',
+    'previous' => 'Trước đó',
 
     // Sort Options
     'sort_options' => 'Tùy Chọn Sắp Xếp',
@@ -51,7 +52,7 @@ return [
     'sort_ascending' => 'Sắp xếp tăng dần',
     'sort_descending' => 'Sắp xếp giảm dần',
     'sort_name' => 'Tên',
-    'sort_default' => 'Default',
+    'sort_default' => 'Mặc định',
     'sort_created_at' => 'Ngày Tạo',
     'sort_updated_at' => 'Ngày cập nhật',
 
@@ -60,6 +61,7 @@ return [
     'no_activity' => 'Không có hoạt động nào',
     'no_items' => 'Không có mục nào khả dụng',
     'back_to_top' => 'Lên đầu trang',
+    'skip_to_main_content' => 'Nhảy đến nội dung chính',
     'toggle_details' => 'Bật/tắt chi tiết',
     'toggle_thumbnails' => 'Bật/tắt ảnh ảnh nhỏ',
     'details' => 'Chi tiết',
@@ -88,6 +90,6 @@ return [
 
     // Footer Link Options
     // Not directly used but available for convenience to users.
-    'privacy_policy' => 'Privacy Policy',
-    'terms_of_service' => 'Terms of Service',
+    'privacy_policy' => 'Chính Sách Quyền Riêng Tư',
+    'terms_of_service' => 'Điều khoản Dịch vụ',
 ];
index cfea0956e97e0e26f629c67009ae6630552aa6b9..0cbfd27a43b1e83a1bc02bb588da5c23e5810e96 100644 (file)
@@ -22,13 +22,13 @@ 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',
+    'meta_owned_name' => 'Được sở hữu bởi :user',
     'entity_select' => 'Chọn thực thể',
     'images' => 'Ảnh',
     'my_recent_drafts' => 'Bản nháp gần đây của tôi',
     'my_recently_viewed' => 'Xem gần đây',
     'my_most_viewed_favourites' => 'My Most Viewed Favourites',
-    'my_favourites' => 'My Favourites',
+    'my_favourites' => 'Danh sách yêu thích của tôi',
     'no_pages_viewed' => 'Bạn chưa xem bất cứ trang nào',
     'no_pages_recently_created' => 'Không có trang nào được tạo gần đây',
     'no_pages_recently_updated' => 'Không có trang nào được cập nhật gần đây',
@@ -36,13 +36,14 @@ return [
     'export_html' => 'Đang chứa tệp tin Web',
     'export_pdf' => 'Tệp PDF',
     'export_text' => 'Tệp văn bản thuần túy',
+    'export_md' => '\bTệp Markdown',
 
     // Permissions and restrictions
     'permissions' => 'Quyền',
     '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',
+    'permissions_owner' => 'Chủ sở hữu',
 
     // Search
     'search_results' => 'Kết quả Tìm kiếm',
@@ -62,7 +63,7 @@ return [
     'search_permissions_set' => 'Phân quyền',
     'search_created_by_me' => 'Được tạo bởi tôi',
     'search_updated_by_me' => 'Được cập nhật bởi tôi',
-    'search_owned_by_me' => 'Owned by me',
+    'search_owned_by_me' => 'Của tôi',
     'search_date_options' => 'Tùy chọn ngày',
     'search_updated_before' => 'Đã được cập nhật trước đó',
     'search_updated_after' => 'Đã được cập nhật sau',
@@ -98,6 +99,7 @@ return [
     'shelves_permissions' => 'Các quyền đối với kệ sách',
     'shelves_permissions_updated' => 'Các quyền với kệ sách đã được cập nhật',
     'shelves_permissions_active' => 'Đang bật các quyền hạn từ Kệ sách',
+    'shelves_permissions_cascade_warning' => 'Các quyền trên giá sách sẽ không được tự động gán cho các sách trên đó. Vì một quyển sách có thể tồn tại trên nhiều giá sách. Các quyền có thể được sao chép xuống các quyển sách sử dụng tuỳ chọn dưới đây.',
     'shelves_copy_permissions_to_books' => 'Sao chép các quyền cho sách',
     'shelves_copy_permissions' => 'Sao chép các quyền',
     'shelves_copy_permissions_explain' => 'Điều này sẽ áp dụng các cài đặt quyền của giá sách hiện tại với tất cả các cuốn sách bên trong. Trước khi kích hoạt, đảm bảo bất cứ thay đổi liên quan đến quyền của giá sách này đã được lưu.',
@@ -151,7 +153,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' => 'This will delete the chapter with the name \':chapterName\'. All pages that exist within this chapter will also be deleted.',
+    'chapters_delete_explain' => 'Hành động này sẽ xoá chương \':chapterName\'. Tất cả các trang trong chương này cũng sẽ bị xoá.',
     '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',
@@ -213,7 +215,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_revision_restored_from' => 'Khôi phục từ #:id; :summary',
     'pages_revisions_created_by' => 'Tạo bởi',
     'pages_revisions_date' => 'Ngày của Phiên bản',
     'pages_revisions_number' => '#',
index 0c4a6fa8f5dbd2fc396e8916ff74101251a531f2..cfd2b974638912795a50464b65e2bd4fd3adb43f 100644 (file)
@@ -83,9 +83,9 @@ return [
     '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' => '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.',
-    'image_not_found' => 'Image Not Found',
-    'image_not_found_subtitle' => 'Sorry, The image file you were looking for could not be found.',
-    'image_not_found_details' => 'If you expected this image to exist it might have been deleted.',
+    'image_not_found' => 'Không tìm thấy Ảnh',
+    'image_not_found_subtitle' => 'Rất tiếc, không thể tìm thấy Ảnh bạn đang tìm kiếm.',
+    'image_not_found_details' => 'Nếu bạn hi vọng ảnh này tồn tại, rất có thể nó đã bị xóa.',
     '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 a105e82c7eac111e106f3322f1842f0d7a080fc7..7dbed9018bba197ddff5e32121b1c1daf752523a 100644 (file)
@@ -92,6 +92,7 @@ return [
     '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' => 'Mục Đã Xóa',
+    'recycle_bin_deleted_parent' => 'Parent',
     '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',
@@ -104,6 +105,7 @@ return [
     '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_restore_parent' => 'Restore Parent',
     'recycle_bin_destroy_notification' => 'Deleted :count total items from the recycle bin.',
     'recycle_bin_restore_notification' => 'Restored :count total items from the recycle bin.',
 
@@ -117,6 +119,7 @@ return [
     'audit_table_user' => 'Người dùng',
     'audit_table_event' => 'Sự kiện',
     'audit_table_related' => 'Related Item or Detail',
+    'audit_table_ip' => 'IP Address',
     'audit_table_date' => 'Ngày hoạt động',
     'audit_date_from' => 'Ngày từ khoảng',
     'audit_date_to' => 'Ngày đến khoảng',
@@ -136,6 +139,7 @@ return [
     'role_details' => 'Thông tin chi tiết Quyền',
     'role_name' => 'Tên quyền',
     'role_desc' => 'Thông tin vắn tắt của Quyền',
+    'role_mfa_enforced' => 'Requires Multi-Factor Authentication',
     'role_external_auth_id' => 'Mã của xác thực ngoài',
     'role_system' => 'Quyền Hệ thống',
     'role_manage_users' => 'Quản lý người dùng',
@@ -145,6 +149,7 @@ return [
     'role_manage_page_templates' => 'Quản lý các mẫu trang',
     'role_access_api' => 'Truy cập đến API hệ thống',
     'role_manage_settings' => 'Quản lý cài đặt của ứng dụng',
+    'role_export_content' => 'Export content',
     'role_asset' => 'Quyền tài sản (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.',
     'role_asset_desc' => 'Các quyền này điều khiển truy cập mặc định tới tài sản (asset) nằm trong hệ thống. Quyền tại Sách, Chường và Trang se ghi đè các quyền này.',
@@ -202,6 +207,10 @@ return [
     'users_api_tokens_create' => 'Tạo Token',
     'users_api_tokens_expires' => 'Hết hạn',
     'users_api_tokens_docs' => 'Tài liệu API',
+    'users_mfa' => 'Multi-Factor Authentication',
+    'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
+    'users_mfa_x_methods' => ':count method configured|:count methods configured',
+    'users_mfa_configure' => 'Configure Methods',
 
     // API Tokens
     'user_api_token_create' => 'Tạo Token API',
@@ -247,6 +256,7 @@ return [
         'it' => 'Italian',
         'ja' => '日本語',
         'ko' => '한국어',
+        'lt' => 'Lietuvių Kalba',
         'lv' => 'Latviešu Valoda',
         'nl' => 'Nederlands',
         'nb' => 'Norsk (Bokmål)',
index bcfb178fb5e2edb9d4bbde9c69e4cbb878534089..7e237cf9e7c0f6dbcb4b1e0887f8fdb4fbe15be6 100644 (file)
@@ -15,6 +15,7 @@ return [
     'alpha_dash'           => ':attribute chỉ được chứa chữ cái, chữ số, gạch nối và gạch dưới.',
     'alpha_num'            => ':attribute chỉ được chứa chữ cái hoặc chữ số.',
     'array'                => ':attribute phải là một mảng.',
+    'backup_codes'         => 'The provided code is not valid or has already been used.',
     'before'               => ':attribute phải là một ngày trước :date.',
     'between'              => [
         'numeric' => ':attribute phải nằm trong khoảng :min đến :max.',
@@ -98,6 +99,7 @@ return [
     ],
     'string'               => ':attribute phải là một chuỗi.',
     'timezone'             => ':attribute phải là một khu vực hợp lệ.',
+    'totp'                 => 'The provided code is not valid or has expired.',
     'unique'               => ':attribute đã có người sử dụng.',
     'url'                  => 'Định dạng của :attribute không hợp lệ.',
     'uploaded'             => 'Tệp tin đã không được tải lên. Máy chủ không chấp nhận các tệp tin với dung lượng lớn như tệp tin trên.',
index cc453abf67a4ee42cfa256b47d6f6bd784dbb74a..65e7c3583b2fb7ac1ce40c36421014e77cd561c2 100644 (file)
@@ -47,6 +47,10 @@ return [
     'favourite_add_notification' => '":name" 已添加到你的收藏',
     'favourite_remove_notification' => '":name" 已从你的收藏中删除',
 
+    // MFA
+    'mfa_setup_method_notification' => '多重身份认证设置成功',
+    'mfa_remove_method_notification' => '多重身份认证已成功移除',
+
     // Other
     'commented_on'                => '评论',
     'permissions_update'          => '权限已更新',
index b883e0b6c4535cfe9a5d495780f0f7b1077e5f6c..a31ee20e75b53c9f214ecc7a5ebe3c48961b5b33 100644 (file)
@@ -26,7 +26,7 @@ return [
     'remember_me' => '记住我',
     'ldap_email_hint' => '请输入用于此帐户的电子邮件。',
     'create_account' => '创建账户',
-    'already_have_account' => '您已经有账号?',
+    'already_have_account' => '已经有账号了?',
     'dont_have_account' => '您还没有账号吗?',
     'social_login' => 'SNS登录',
     'social_registration' => '使用社交网站账号注册',
@@ -73,5 +73,40 @@ return [
     'user_invite_page_welcome' => '欢迎来到 :appName!',
     'user_invite_page_text' => '要完成您的帐户并获得访问权限,您需要设置一个密码,该密码将在以后访问时用于登录 :appName。',
     'user_invite_page_confirm_button' => '确认密码',
-    'user_invite_success' => '已设置密码,您现在可以访问 :appName!'
+    'user_invite_success' => '已设置密码,您现在可以访问 :appName!',
+
+    // Multi-factor Authentication
+    'mfa_setup' => '设置多重身份认证',
+    'mfa_setup_desc' => '设置多重身份认证能增加您账户的安全性。',
+    'mfa_setup_configured' => '已经设置过了',
+    'mfa_setup_reconfigure' => '重新配置',
+    'mfa_setup_remove_confirmation' => '您确定想要移除多重身份认证吗?',
+    'mfa_setup_action' => '设置',
+    'mfa_backup_codes_usage_limit_warning' => '您剩余的备用认证码少于 5 个,请在用完认证码之前生成并保存新的认证码,以防止您的帐户被锁定。',
+    'mfa_option_totp_title' => '移动设备 App',
+    'mfa_option_totp_desc' => '要使用多重身份认证功能,您需要一个支持 TOTP(基于时间的一次性密码算法) 的移动设备 App,如谷歌身份验证器(Google Authenticator)、Authy 或微软身份验证器(Microsoft Authenticator)。',
+    'mfa_option_backup_codes_title' => '备用认证码',
+    'mfa_option_backup_codes_desc' => '请安全地保存这些一次性使用的备用认证码,您可以输入这些认证码来验证您的身份。',
+    'mfa_gen_confirm_and_enable' => '确认并启用',
+    'mfa_gen_backup_codes_title' => '备用认证码设置',
+    'mfa_gen_backup_codes_desc' => '将下面的认证码存放在一个安全的地方。访问系统时,您可以使用其中的一个验证码进行二次认证。',
+    'mfa_gen_backup_codes_download' => '下载认证码',
+    'mfa_gen_backup_codes_usage_warning' => '每个认证码只能使用一次',
+    'mfa_gen_totp_title' => '移动设备 App',
+    'mfa_gen_totp_desc' => '要使用多重身份认证功能,您需要一个支持 TOTP(基于时间的一次性密码算法) 的移动设备 App,如谷歌身份验证器(Google Authenticator)、Authy 或微软身份验证器(Microsoft Authenticator)。',
+    'mfa_gen_totp_scan' => '要开始操作,请使用你的身份验证 App 扫描下面的二维码。',
+    'mfa_gen_totp_verify_setup' => '验证设置',
+    'mfa_gen_totp_verify_setup_desc' => '请在下面的框中输入您在身份验证 App 中生成的认证码来验证一切是否正常:',
+    'mfa_gen_totp_provide_code_here' => '在此输入您的 App 生成的认证码',
+    'mfa_verify_access' => '认证访问',
+    'mfa_verify_access_desc' => '您的账户要求您在访问前通过额外的验证确认您的身份。使用您设置的认证方法认证以继续。',
+    'mfa_verify_no_methods' => '没有设置认证方法',
+    'mfa_verify_no_methods_desc' => '您的账户没有设置多重身份认证。您需要至少设置一种才能访问。',
+    'mfa_verify_use_totp' => '使用移动设备 App 进行认证',
+    'mfa_verify_use_backup_codes' => '使用备用认证码进行认证',
+    'mfa_verify_backup_code' => '备用认证码',
+    'mfa_verify_backup_code_desc' => '在下面输入你的其中一个备用认证码:',
+    'mfa_verify_backup_code_enter_here' => '在这里输入备用认证码',
+    'mfa_verify_totp_desc' => '在下面输入您的移动 App 生成的认证码:',
+    'mfa_setup_login_notification' => '多重身份认证已设置,请使用新配置的方法重新登录。',
 ];
\ No newline at end of file
index c8a0eef075eb36404767698c2eb2f449f21c12cc..6c2fa668bed9aab35c9064a9cce303c3c4325932 100644 (file)
@@ -39,9 +39,10 @@ return [
     'reset' => '重置',
     'remove' => '删除',
     'add' => '添加',
+    'configure' => '配置',
     'fullscreen' => '全屏',
     'favourite' => '收藏',
-    'unfavourite' => '不喜欢',
+    'unfavourite' => '取消收藏',
     'next' => '下一页',
     'previous' => '上一页',
 
@@ -60,6 +61,7 @@ return [
     'no_activity' => '没有活动要显示',
     'no_items' => '没有可用的项目',
     'back_to_top' => '回到顶部',
+    'skip_to_main_content' => '跳转到主要内容',
     'toggle_details' => '显示/隐藏详细信息',
     'toggle_thumbnails' => '显示/隐藏缩略图',
     'details' => '详细信息',
index 494ac717f48fbe1138b450f72adf182dc53d82ea..277bcfb32a6b9da249d59cf57145c87446dfcbe8 100644 (file)
@@ -36,6 +36,7 @@ return [
     'export_html' => '网页文件',
     'export_pdf' => 'PDF文件',
     'export_text' => '纯文本文件',
+    'export_md' => 'Markdown 文件',
 
     // Permissions and restrictions
     'permissions' => '权限',
@@ -98,6 +99,7 @@ return [
     'shelves_permissions' => '书架权限',
     'shelves_permissions_updated' => '书架权限已更新',
     'shelves_permissions_active' => '书架权限激活',
+    'shelves_permissions_cascade_warning' => '书架上的权限不会自动应用到书架里的书。这是因为书可以在多个书架上存在。使用下面的选项可以将权限复制到书架里的书上。',
     'shelves_copy_permissions_to_books' => '将权限复制到图书',
     'shelves_copy_permissions' => '复制权限',
     'shelves_copy_permissions_explain' => '这会将此书架的当前权限设置应用于其中包含的所有图书。 在激活之前,请确保已保存对此书架权限的任何更改。',
@@ -106,7 +108,7 @@ return [
     // Books
     'book' => '图书',
     'books' => '图书',
-    'x_books' => ':count本书',
+    'x_books' => ':count 本书',
     'books_empty' => '不存在已创建的书',
     'books_popular' => '热门图书',
     'books_recent' => '最近的书',
index b6152425f7569e7900d3815086a9bc7ee87b7e78..910b9614d0f11c040eddca20c7d6c011456d3039 100755 (executable)
@@ -15,7 +15,7 @@ return [
     'app_customization' => '定制',
     'app_features_security' => '功能与安全',
     'app_name' => '站点名称',
-    'app_name_desc' => '此名称将在网页头部和Email中显示。',
+    'app_name_desc' => '此名称将在网页头部和系统发送的电子邮件中显示。',
     'app_name_header' => '在网页头部显示站点名称?',
     'app_public_access' => '访问权限',
     'app_public_access_desc' => '启用此选项将允许未登录的用户访问站点内容。',
@@ -35,7 +35,7 @@ return [
     'app_primary_color' => '站点主色',
     'app_primary_color_desc' => '这应该是一个十六进制值。<br>保留为空以重置为默认颜色。',
     'app_homepage' => '站点主页',
-    'app_homepage_desc' => '选择要在主页上显示的页面来替换默认的视图,选定页面的访问权限将被忽略。',
+    'app_homepage_desc' => '选择要在主页上显示的页面来替换默认的页面,选定页面的访问权限将被忽略。',
     'app_homepage_select' => '选择一个页面',
     'app_footer_links' => '页脚链接',
     'app_footer_links_desc' => '添加在网站页脚中显示的链接。这些链接将显示在大多数页面的底部,也包括不需要登录的页面。您可以使用标签"trans::<key>"来使用系统定义的翻译。例如:使用"trans::common.privacy_policy"将显示为“隐私政策”,而"trans::common.terms_of_service"将显示为“服务条款”。',
@@ -64,15 +64,15 @@ return [
     'reg_enable_external_warning' => '当启用外部LDAP或者SAML认证时,上面的选项会被忽略。当使用外部系统认证认证成功时,将自动创建非现有会员的用户账户。',
     'reg_email_confirmation' => '邮件确认',
     'reg_email_confirmation_toggle' => '需要电子邮件确认',
-    'reg_confirm_email_desc' => '如果使用域名限制,则需要Email验证,并且该值将被忽略。',
+    'reg_confirm_email_desc' => '如果使用域名限制,则需要电子邮件验证,并且该值将被忽略。',
     'reg_confirm_restrict_domain' => '域名限制',
-    'reg_confirm_restrict_domain_desc' => '输入您想要限制注册的Email域名列表,用逗号隔开。在被允许与应用程序交互之前,用户将被发送一封Email来确认他们的地址。<br>注意用户在注册成功后可以修改他们的Email地址。',
+    'reg_confirm_restrict_domain_desc' => '输入您想要限制注册的电子邮件域名列表(即只允许使用这些电子邮件域名注册),多个域名用英文逗号隔开。在允许用户与应用程序交互之前,系统将向用户发送一封电子邮件以确认其电子邮件地址。<br>请注意,用户在注册成功后仍然可以更改他们的电子邮件地址。',
     'reg_confirm_restrict_domain_placeholder' => '尚未设置限制',
 
     // Maintenance settings
     'maint' => '维护',
     'maint_image_cleanup' => '清理图像',
-    'maint_image_cleanup_desc' => "扫描页面和修订内容以检查哪些图像是正在使用的以及哪些图像是多余的。确保在运行前创建完整的数据库和映像备份。",
+    'maint_image_cleanup_desc' => "扫描页面和修订内容以检查哪些图片是正在使用的以及哪些图片是多余的。确保在运行前完整备份数据库和图片。",
     'maint_delete_images_only_in_revisions' => '同时删除只存在于旧的页面修订中的图片',
     'maint_image_cleanup_run' => '运行清理',
     'maint_image_cleanup_warning' => '发现了 :count 张可能未使用的图像。您确定要删除这些图像吗?',
@@ -92,6 +92,7 @@ return [
     'recycle_bin' => '回收站',
     'recycle_bin_desc' => '在这里,您可以还原已删除的项目,或选择将其从系统中永久删除。与系统中过滤过的类似的活动记录不同,这个表会显示所有操作。',
     'recycle_bin_deleted_item' => '被删除的项目',
+    'recycle_bin_deleted_parent' => '上级',
     'recycle_bin_deleted_by' => '删除者',
     'recycle_bin_deleted_at' => '删除时间',
     'recycle_bin_permanently_delete' => '永久删除',
@@ -104,6 +105,7 @@ return [
     'recycle_bin_restore_list' => '要恢复的项目',
     'recycle_bin_restore_confirm' => '此操作会将已删除的项目及其所有子元素恢复到原始位置。如果项目的原始位置已被删除,并且现在位于回收站中,则要恢复项目的上级项目也需要恢复。',
     'recycle_bin_restore_deleted_parent' => '该项目的上级项目也已被删除。这些项目将保持被删除状态,直到上级项目被恢复。',
+    'recycle_bin_restore_parent' => '还原上级',
     'recycle_bin_destroy_notification' => '从回收站中删除了 :count 个项目。',
     'recycle_bin_restore_notification' => '从回收站中恢复了 :count 个项目。',
 
@@ -117,6 +119,7 @@ return [
     'audit_table_user' => '用户',
     'audit_table_event' => '事件',
     'audit_table_related' => '相关项目或详细信息',
+    'audit_table_ip' => 'IP地址',
     'audit_table_date' => '活动日期',
     'audit_date_from' => '日期范围从',
     'audit_date_to' => '日期范围至',
@@ -136,6 +139,7 @@ return [
     'role_details' => '角色详细信息',
     'role_name' => '角色名',
     'role_desc' => '角色简述',
+    'role_mfa_enforced' => '需要多重身份认证',
     'role_external_auth_id' => '外部身份认证ID',
     'role_system' => '系统权限',
     'role_manage_users' => '管理用户',
@@ -145,6 +149,7 @@ return [
     'role_manage_page_templates' => '管理页面模板',
     'role_access_api' => '访问系统 API',
     'role_manage_settings' => '管理App设置',
+    'role_export_content' => '导出内容',
     'role_asset' => '资源许可',
     'roles_system_warning' => '请注意,具有上述三个权限中的任何一个都可以允许用户更改自己的特权或系统中其他人的特权。 只将具有这些权限的角色分配给受信任的用户。',
     'role_asset_desc' => '对系统内资源的默认访问许可将由这些权限控制。单独设置在书籍,章节和页面上的权限将覆盖这里的权限设定。',
@@ -202,6 +207,10 @@ return [
     'users_api_tokens_create' => '创建令牌',
     'users_api_tokens_expires' => '过期',
     'users_api_tokens_docs' => 'API文档',
+    'users_mfa' => '多重身份认证',
+    'users_mfa_desc' => '设置多重身份认证能增加您账户的安全性。',
+    'users_mfa_x_methods' => ':count 方法已配置|:count 方法已配置',
+    'users_mfa_configure' => '配置方法',
 
     // API Tokens
     'user_api_token_create' => '创建 API 令牌',
@@ -247,6 +256,7 @@ return [
         'it' => 'Italian',
         'ja' => '日本語',
         'ko' => '한국어',
+        'lt' => 'Lietuvių Kalba',
         'lv' => 'Latviešu Valoda',
         'nl' => 'Nederlands',
         'nb' => '挪威语 (Bokmål)',
index 72b0d594e75dd852b8002bcdb91b2c8034db5c3d..3398a1142c0a2ea63eb08c174653f2a3b3d30bb8 100644 (file)
@@ -15,6 +15,7 @@ return [
     'alpha_dash'           => ':attribute 只能包含字母、数字和短横线。',
     'alpha_num'            => ':attribute 只能包含字母和数字。',
     'array'                => ':attribute 必须是一个数组。',
+    'backup_codes'         => '您输入的认证码无效或已被使用。',
     'before'               => ':attribute 必须是在 :date 前的日期。',
     'between'              => [
         'numeric' => ':attribute 必须在:min到:max之间。',
@@ -98,6 +99,7 @@ return [
     ],
     'string'               => ':attribute 必须是字符串。',
     'timezone'             => ':attribute 必须是有效的区域。',
+    'totp'                 => '您输入的认证码无效或已过期。',
     'unique'               => ':attribute 已经被使用。',
     'url'                  => ':attribute 格式无效。',
     'uploaded'             => '无法上传文件。 服务器可能不接受此大小的文件。',
index 6d07a176a6b605bf457772baeea2a2a6fcfb532f..0fe3ecd849b41d97073be558f9f0c53a81601b5a 100644 (file)
@@ -44,8 +44,12 @@ return [
     'bookshelf_delete_notification'    => '書架已刪除成功',
 
     // Favourites
-    'favourite_add_notification' => '":name" has been added to your favourites',
-    'favourite_remove_notification' => '":name" has been removed from your favourites',
+    'favourite_add_notification' => '":name" 已加入到你的最愛',
+    'favourite_remove_notification' => '":name" 已從你的最愛移除',
+
+    // MFA
+    'mfa_setup_method_notification' => 'Multi-factor method successfully configured',
+    'mfa_remove_method_notification' => 'Multi-factor method successfully removed',
 
     // Other
     'commented_on'                => '評論',
index e4f3c79782b6da909c5a0b5e039cfa901c319ead..89fc6f55ac0f1ca7c62722df08451f16a1e55701 100644 (file)
@@ -73,5 +73,40 @@ return [
     'user_invite_page_welcome' => '歡迎使用 :appName!',
     'user_invite_page_text' => '要完成設定您的帳號並取得存取權,您必須設定密碼,此密碼將用於登入 :appName。',
     'user_invite_page_confirm_button' => '確認密碼',
-    'user_invite_success' => '密碼已設定,您現在可以存取 :appName 了!'
+    'user_invite_success' => '密碼已設定,您現在可以存取 :appName 了!',
+
+    // Multi-factor Authentication
+    'mfa_setup' => 'Setup Multi-Factor Authentication',
+    'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
+    'mfa_setup_configured' => 'Already configured',
+    'mfa_setup_reconfigure' => 'Reconfigure',
+    'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?',
+    'mfa_setup_action' => 'Setup',
+    'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.',
+    'mfa_option_totp_title' => 'Mobile App',
+    'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
+    'mfa_option_backup_codes_title' => 'Backup Codes',
+    'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.',
+    'mfa_gen_confirm_and_enable' => 'Confirm and Enable',
+    'mfa_gen_backup_codes_title' => 'Backup Codes Setup',
+    'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.',
+    'mfa_gen_backup_codes_download' => 'Download Codes',
+    'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once',
+    'mfa_gen_totp_title' => 'Mobile App Setup',
+    'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
+    'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.',
+    'mfa_gen_totp_verify_setup' => 'Verify Setup',
+    'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:',
+    'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here',
+    'mfa_verify_access' => 'Verify Access',
+    'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.',
+    'mfa_verify_no_methods' => 'No Methods Configured',
+    'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.',
+    'mfa_verify_use_totp' => 'Verify using a mobile app',
+    'mfa_verify_use_backup_codes' => 'Verify using a backup code',
+    'mfa_verify_backup_code' => 'Backup Code',
+    'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:',
+    'mfa_verify_backup_code_enter_here' => 'Enter backup code here',
+    'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:',
+    'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.',
 ];
\ No newline at end of file
index 6d1d15f3e308aa58f3b4ac04432035db531df45f..b358111fda7b034694c0529775d50035da266f3b 100644 (file)
@@ -39,11 +39,12 @@ return [
     'reset' => '重設',
     'remove' => '移除',
     'add' => '新增',
+    'configure' => 'Configure',
     'fullscreen' => '全螢幕',
-    'favourite' => 'Favourite',
-    'unfavourite' => 'Unfavourite',
-    'next' => 'Next',
-    'previous' => 'Previous',
+    'favourite' => '最愛',
+    'unfavourite' => '取消最愛',
+    'next' => '下一頁',
+    'previous' => '上一頁',
 
     // Sort Options
     'sort_options' => '排序選項',
@@ -51,7 +52,7 @@ return [
     'sort_ascending' => '遞增排序',
     'sort_descending' => '遞減排序',
     'sort_name' => '名稱',
-    'sort_default' => 'Default',
+    'sort_default' => '預設',
     'sort_created_at' => '建立日期',
     'sort_updated_at' => '更新日期',
 
@@ -60,6 +61,7 @@ return [
     'no_activity' => '無活動可顯示',
     'no_items' => '無可用項目',
     'back_to_top' => '回到頂端',
+    'skip_to_main_content' => '跳到主內容',
     'toggle_details' => '顯示/隱藏詳細資訊',
     'toggle_thumbnails' => '顯示/隱藏縮圖',
     'details' => '詳細資訊',
@@ -69,7 +71,7 @@ return [
     'breadcrumb' => '頁面路徑',
 
     // Header
-    'header_menu_expand' => 'Expand Header Menu',
+    'header_menu_expand' => '展開選單',
     'profile_menu' => '個人資料選單',
     'view_profile' => '檢視個人資料',
     'edit_profile' => '編輯個人資料',
@@ -78,9 +80,9 @@ return [
 
     // Layout tabs
     'tab_info' => '資訊',
-    'tab_info_label' => 'Tab: Show Secondary Information',
+    'tab_info_label' => '顯示次要訊息',
     'tab_content' => '內容',
-    'tab_content_label' => 'Tab: Show Primary Content',
+    'tab_content_label' => '顯示主要內容',
 
     // Email Content
     'email_action_help' => '如果您無法點擊 ":actionText" 按鈕,請將下方的網址複製並貼上到您的網路瀏覽器中:',
index 32e3792be5858d7d16b4f3ceea86d2d76a7f169a..2b98bddb88d882a884afba745bcf84b20b969203 100644 (file)
@@ -27,8 +27,8 @@ return [
     'images' => '圖片',
     'my_recent_drafts' => '我最近的草稿',
     'my_recently_viewed' => '我最近檢視',
-    'my_most_viewed_favourites' => 'My Most Viewed Favourites',
-    'my_favourites' => 'My Favourites',
+    'my_most_viewed_favourites' => '我瀏覽最多次的最愛',
+    'my_favourites' => '我的最愛',
     'no_pages_viewed' => '您尚未看過任何頁面',
     'no_pages_recently_created' => '最近未建立任何頁面',
     'no_pages_recently_updated' => '最近沒有頁面被更新',
@@ -36,6 +36,7 @@ return [
     'export_html' => '網頁檔案',
     'export_pdf' => 'PDF 檔案',
     'export_text' => '純文字檔案',
+    'export_md' => 'Markdown 檔案',
 
     // Permissions and restrictions
     'permissions' => '權限',
@@ -62,7 +63,7 @@ return [
     'search_permissions_set' => '權限設定',
     'search_created_by_me' => '我建立的',
     'search_updated_by_me' => '我更新的',
-    'search_owned_by_me' => 'Owned by me',
+    'search_owned_by_me' => '我所擁有的',
     'search_date_options' => '日期選項',
     'search_updated_before' => '在此之前更新',
     'search_updated_after' => '在此之後更新',
@@ -98,6 +99,7 @@ return [
     'shelves_permissions' => '書架權限',
     'shelves_permissions_updated' => '書架權限已更新',
     'shelves_permissions_active' => '書架權限已啟用',
+    'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',
     'shelves_copy_permissions_to_books' => '將權限複製到書本',
     'shelves_copy_permissions' => '複製權限',
     'shelves_copy_permissions_explain' => '這會將此書架目前的權限設定套用到所有包含的書本上。在啟用前,請確認您已儲存任何對此書架權限的變更。',
index 0435fc8ad4c9b8f269bbf4e879913865ca48861d..0d898552fe8b302a9c90f03ddcd7ff56376d0ccc 100644 (file)
@@ -83,9 +83,9 @@ return [
     '404_page_not_found' => '找不到頁面',
     'sorry_page_not_found' => '抱歉,找不到您在尋找的頁面。',
     'sorry_page_not_found_permission_warning' => '如果您確認這個頁面存在,則代表可能沒有查看它的權限。',
-    'image_not_found' => 'Image Not Found',
-    'image_not_found_subtitle' => 'Sorry, The image file you were looking for could not be found.',
-    'image_not_found_details' => 'If you expected this image to exist it might have been deleted.',
+    'image_not_found' => '找不到圖片',
+    'image_not_found_subtitle' => '對不起,無法找到您所看的圖片',
+    'image_not_found_details' => '原本的圖片可能已經被刪除',
     'return_home' => '回到首頁',
     'error_occurred' => '發生錯誤',
     'app_down' => ':appName 離線中',
index 77c1d8b83688c15a5f67ce1871a3288310e1a627..aa0a8799341a2db0dfeb319a0e1fc2c21f9f03ec 100644 (file)
@@ -92,6 +92,7 @@ return [
     'recycle_bin' => '資源回收桶',
     'recycle_bin_desc' => '在這裡,您可以還原已刪除的項目,或是選擇將其從系統中永久移除。與系統中套用了權限過濾條件類似的活動列表不同的是,此列表並未過濾。',
     'recycle_bin_deleted_item' => '已刪除項目',
+    'recycle_bin_deleted_parent' => '上層',
     'recycle_bin_deleted_by' => '刪除由',
     'recycle_bin_deleted_at' => '刪除時間',
     'recycle_bin_permanently_delete' => '永久刪除',
@@ -104,6 +105,7 @@ return [
     'recycle_bin_restore_list' => '要被還原的項目',
     'recycle_bin_restore_confirm' => '此動作將會還原已被刪除的項目(包含任何下層元素)到其原始位置。如果原始位置已被刪除,且目前位於垃圾桶裡,那麼上層項目也需要被還原。',
     'recycle_bin_restore_deleted_parent' => '此項目的上層項目也已被刪除。因此將會保持被刪除的狀態,直到上層項目也被還原。',
+    'recycle_bin_restore_parent' => '還原上層',
     'recycle_bin_destroy_notification' => '已從回收桶刪除共 :count 個項目。',
     'recycle_bin_restore_notification' => '已從回收桶還原共 :count 個項目。',
 
@@ -117,6 +119,7 @@ return [
     'audit_table_user' => '使用者',
     'audit_table_event' => '活動',
     'audit_table_related' => '相關的項目或詳細資訊',
+    'audit_table_ip' => 'IP Address',
     'audit_table_date' => '活動日期',
     'audit_date_from' => '日期範圍,從',
     'audit_date_to' => '日期範圍,到',
@@ -136,6 +139,7 @@ return [
     'role_details' => '角色詳細資訊',
     'role_name' => '角色名稱',
     'role_desc' => '角色簡短說明',
+    'role_mfa_enforced' => 'Requires Multi-Factor Authentication',
     'role_external_auth_id' => '外部身份驗證 ID',
     'role_system' => '系統權限',
     'role_manage_users' => '管理使用者',
@@ -145,6 +149,7 @@ return [
     'role_manage_page_templates' => '管理頁面範本',
     'role_access_api' => '存取系統 API',
     'role_manage_settings' => '管理應用程式設定',
+    'role_export_content' => 'Export content',
     'role_asset' => '資源權限',
     'roles_system_warning' => '請注意,有上述三項權限中的任一項的使用者都可以更改自己或系統中其他人的權限。有這些權限的角色只應分配給受信任的使用者。',
     'role_asset_desc' => '對系統內資源的預設權限將由這裡的權限控制。若有單獨設定在書本、章節和頁面上的權限,將會覆寫這裡的權限設定。',
@@ -202,6 +207,10 @@ return [
     'users_api_tokens_create' => '建立權杖',
     'users_api_tokens_expires' => '過期',
     'users_api_tokens_docs' => 'API 文件',
+    'users_mfa' => 'Multi-Factor Authentication',
+    'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
+    'users_mfa_x_methods' => ':count method configured|:count methods configured',
+    'users_mfa_configure' => 'Configure Methods',
 
     // API Tokens
     'user_api_token_create' => '建立 API 權杖',
@@ -247,6 +256,7 @@ return [
         'it' => 'Italian',
         'ja' => '日本語',
         'ko' => '한국어',
+        'lt' => 'Lietuvių Kalba',
         'lv' => 'Latviešu Valoda',
         'nl' => 'Nederlands',
         'nb' => 'Norsk (Bokmål)',
index 691ebb619652058994c356803665d5ba4370fcf2..e93c182ee4fe600f75b4d57e35401557c157036f 100644 (file)
@@ -15,6 +15,7 @@ return [
     'alpha_dash'           => ':attribute 只能包含字母、數字、破折號與底線。',
     'alpha_num'            => ':attribute 只能包含字母和數字。',
     'array'                => ':attribute 必須是陣列。',
+    'backup_codes'         => 'The provided code is not valid or has already been used.',
     'before'               => ':attribute 必須是在 :date 前的日期。',
     'between'              => [
         'numeric' => ':attribute 必須在 :min 到 :max 之間。',
@@ -98,6 +99,7 @@ return [
     ],
     'string'               => ':attribute 必須是字元串。',
     'timezone'             => ':attribute 必須是有效的區域。',
+    'totp'                 => 'The provided code is not valid or has expired.',
     'unique'               => ':attribute 已經被使用。',
     'url'                  => ':attribute 格式無效。',
     'uploaded'             => '無法上傳文件, 服務器可能不接受此大小的文件。',
index 953d1d06054d23730b9c104a0a833980ce4f6e9b..665b1213be8ba5a4401b3449c801f0c627d5c207 100644 (file)
   }
 }
 
+.input-fill-width {
+  width: 100% !important;
+}
+
 .fake-input {
   @extend .input-base;
   overflow: auto;
 .markdown-editor-display {
   background-color: #fff;
   body {
+    display: block;
     background-color: #fff;
     padding-inline-start: 16px;
     padding-inline-end: 16px;
index 516d7d612e590d7a6ec516a689cdd102ff6606c7..362bab7d39195672c1511bc73832e0e6a4eb1ae0 100644 (file)
@@ -145,6 +145,7 @@ body.flexbox {
 .flex {
   min-height: 0;
   flex: 1;
+  max-width: 100%;
   &.fit-content {
     flex-basis: auto;
     flex-grow: 0;
@@ -181,6 +182,10 @@ body.flexbox {
   display: inline-block !important;
 }
 
+.relative {
+  position: relative;
+}
+
 .hidden {
   display: none !important;
 }
index 7a0987c66c1a1f1c87d71d210e32eb698345270a..cbe3cd4be02b25158f261a293c9015274f25c8a6 100644 (file)
@@ -280,13 +280,9 @@ ul, ol {
   }
 }
 ul {
-  padding-left: $-m * 1.3;
-  padding-right: $-m * 1.3;
   list-style: disc;
   ul {
     list-style: circle;
-    margin-top: 0;
-    margin-bottom: 0;
   }
   label {
     margin: 0;
@@ -295,23 +291,33 @@ ul {
 
 ol {
   list-style: decimal;
-  padding-left: $-m * 2;
-  padding-right: $-m * 2;
+}
+
+ol, ul {
+  padding-left: $-m * 2.0;
+  padding-right: $-m * 2.0;
+}
+
+li > ol, li > ul {
+  margin-top: 0;
+  margin-bottom: 0;
+  margin-block-end: 0;
+  margin-block-start: 0;
+  padding-block-end: 0;
+  padding-block-start: 0;
+  padding-left: $-m * 1.2;
+  padding-right: $-m * 1.2;
 }
 
 li.checkbox-item, li.task-list-item {
   list-style: none;
-  margin-left: - ($-m * 1.3);
+  margin-left: -($-m * 1.2);
   input[type="checkbox"] {
     margin-right: $-xs;
   }
-}
-
-li > ol, li > ul {
-  margin-block-end: 0px;
-  margin-block-start: 0px;
-  padding-block-end: 0px;
-  padding-block-start: 0px;
+  li.checkbox-item, li.task-list-item {
+    margin-left: $-xs;
+  }
 }
 
 /*
index 56f7135c36bca4776474eaa839bb6d5aa80e81fc..5bec265e82b9d4704839655f18a3f9db365c76fd 100644 (file)
@@ -1,4 +1,4 @@
-@extends('simple-layout')
+@extends('layouts.simple')
 
 @section('body')
 
             <div style="overflow: auto;">
 
                 <section code-highlighter class="card content-wrap auto-height">
-                    <h1 class="list-heading text-capitals mb-l">Getting Started</h1>
-
-                    <h5 id="authentication" class="text-mono mb-m">Authentication</h5>
-                    <p>
-                        To access the API a user has to have the <em>"Access System API"</em> permission enabled on one of their assigned roles.
-                        Permissions to content accessed via the API is limited by the roles & permissions assigned to the user that's used to access the API.
-                    </p>
-                    <p>Authentication to use the API is primarily done using API Tokens. Once the <em>"Access System API"</em> permission has been assigned to a user, a "API Tokens" section should be visible when editing their user profile. Choose "Create Token" and enter an appropriate name and expiry date, relevant for your API usage then press "Save". A "Token ID" and "Token Secret" will be immediately displayed. These values should be used as a header in API HTTP requests in the following format:</p>
-                    <pre><code class="language-css">Authorization: Token &lt;token_id&gt;:&lt;token_secret&gt;</code></pre>
-                    <p>Here's an example of an authorized cURL request to list books in the system:</p>
-                    <pre><code class="language-shell">curl --request GET \
-  --url https://example.com/api/books \
-  --header 'Authorization: Token C6mdvEQTGnebsmVn3sFNeeuelGEBjyQp:NOvD3VlzuSVuBPNaf1xWHmy7nIRlaj22'</code></pre>
-                    <p>If already logged into the system within the browser, via a user account with permission to access the API, the system will also accept an existing session meaning you can browse API endpoints directly in the browser or use the browser devtools to play with the API.</p>
-
-                    <hr>
-
-                    <h5 id="request-format" class="text-mono mb-m">Request Format</h5>
-                    <p>The API is primarily design to be interfaced using JSON so the majority of API endpoints, that accept data, will read JSON request data although <code>application/x-www-form-urlencoded</code> request data is also accepted. Endpoints that receive file data will need data sent in a <code>multipart/form-data</code> format although this will be highlighted in the documentation for such endpoints.</p>
-                    <p>For endpoints in this documentation that accept data, a "Body Parameters" table will be available showing the parameters that will accepted in the request. Any rules for the values of such parameters, such as the data-type or if they're required, will be shown alongside the parameter name.</p>
-
-                    <hr>
-
-                    <h5 id="listing-endpoints" class="text-mono mb-m">Listing Endpoints</h5>
-                    <p>Some endpoints will return a list of data models. These endpoints will return an array of the model data under a <code>data</code> property along with a numeric <code>total</code> property to indicate the total number of records found for the query within the system. Here's an example of a listing response:</p>
-                    <pre><code class="language-json">{
-  "data": [
-    {
-      "id": 1,
-      "name": "BookStack User Guide",
-      "slug": "bookstack-user-guide",
-      "description": "This is a general guide on using BookStack on a day-to-day basis.",
-      "created_at": "2019-05-05 21:48:46",
-      "updated_at": "2019-12-11 20:57:31",
-      "created_by": 1,
-      "updated_by": 1,
-      "image_id": 3
-    }
-  ],
-  "total": 16
-}</code></pre>
-                    <p>
-                        There are a number of standard URL parameters that can be supplied to manipulate and page through the results returned from a listing endpoint:
-                    </p>
-                    <table class="table">
-                        <tr>
-                            <th>Parameter</th>
-                            <th>Details</th>
-                            <th width="30%">Examples</th>
-                        </tr>
-                        <tr>
-                            <td>count</td>
-                            <td>
-                                Specify how many records will be returned in the response. <br>
-                                (Default: {{ config('api.default_item_count') }}, Max: {{ config('api.max_item_count') }})
-                            </td>
-                            <td>Limit the count to 50<br><code>?count=50</code></td>
-                        </tr>
-                        <tr>
-                            <td>offset</td>
-                            <td>
-                                Specify how many records to skip over in the response. <br>
-                                (Default: 0)
-                            </td>
-                            <td>Skip over the first 100 records<br><code>?offset=100</code></td>
-                        </tr>
-                        <tr>
-                            <td>sort</td>
-                            <td>
-                                Specify what field is used to sort the data and the direction of the sort (Ascending or Descending).<br>
-                                Value is the name of a field, A <code>+</code> or <code>-</code> prefix dictates ordering. <br>
-                                Direction defaults to ascending. <br>
-                                Can use most fields shown in the response.
-                            </td>
-                            <td>
-                                Sort by name ascending<br><code>?sort=+name</code> <br> <br>
-                                Sort by "Created At" date descending<br><code>?sort=-created_at</code>
-                            </td>
-                        </tr>
-                        <tr>
-                            <td>filter[&lt;field&gt;]</td>
-                            <td>
-                                Specify a filter to be applied to the query. Can use most fields shown in the response. <br>
-                                By default a filter will apply a "where equals" query but the below operations are available using the format filter[&lt;field&gt;:&lt;operation&gt;] <br>
-                                <table>
-                                    <tr>
-                                        <td>eq</td>
-                                        <td>Where <code>&lt;field&gt;</code> equals the filter value.</td>
-                                    </tr>
-                                    <tr>
-                                        <td>ne</td>
-                                        <td>Where <code>&lt;field&gt;</code> does not equal the filter value.</td>
-                                    </tr>
-                                    <tr>
-                                        <td>gt</td>
-                                        <td>Where <code>&lt;field&gt;</code> is greater than the filter value.</td>
-                                    </tr>
-                                    <tr>
-                                        <td>lt</td>
-                                        <td>Where <code>&lt;field&gt;</code> is less than the filter value.</td>
-                                    </tr>
-                                    <tr>
-                                        <td>gte</td>
-                                        <td>Where <code>&lt;field&gt;</code> is greater than or equal to the filter value.</td>
-                                    </tr>
-                                    <tr>
-                                        <td>lte</td>
-                                        <td>Where <code>&lt;field&gt;</code> is less than or equal to the filter value.</td>
-                                    </tr>
-                                    <tr>
-                                        <td>like</td>
-                                        <td>
-                                            Where <code>&lt;field&gt;</code> is "like" the filter value. <br>
-                                            <code>%</code> symbols can be used as wildcards.
-                                        </td>
-                                    </tr>
-                                </table>
-                            </td>
-                            <td>
-                                Filter where id is 5: <br><code>?filter[id]=5</code><br><br>
-                                Filter where id is not 5: <br><code>?filter[id:ne]=5</code><br><br>
-                                Filter where name contains "cat": <br><code>?filter[name:like]=%cat%</code><br><br>
-                                Filter where created after 2020-01-01: <br><code>?filter[created_at:gt]=2020-01-01</code>
-                            </td>
-                        </tr>
-                    </table>
-
-                    <hr>
-
-                    <h5 id="error-handling" class="text-mono mb-m">Error Handling</h5>
-                    <p>
-                        Successful responses will return a 200 or 204 HTTP response code. Errors will return a 4xx or a 5xx HTTP response code depending on the type of error. Errors follow a standard format as shown below. The message provided may be translated depending on the configured language of the system in addition to the API users' language preference. The code provided in the JSON response will match the HTTP response code.
-                    </p>
-
-                    <pre><code class="language-json">{
-       "error": {
-               "code": 401,
-               "message": "No authorization token found on the request"
-       }
-}
-</code></pre>
-
+                    @include('api-docs.parts.getting-started')
                 </section>
 
                 @foreach($docs as $model => $endpoints)
                         <h1 class="list-heading text-capitals">{{ $model }}</h1>
 
                         @foreach($endpoints as $endpoint)
-                            <h6 class="text-uppercase text-muted float right">{{ $endpoint['controller_method_kebab'] }}</h6>
-                            <h5 id="{{ $endpoint['name'] }}" class="text-mono mb-m">
-                                <span class="api-method" data-method="{{ $endpoint['method'] }}">{{ $endpoint['method'] }}</span>
-                                @if($endpoint['controller_method_kebab'] === 'list')
-                                    <a style="color: inherit;" target="_blank" rel="noopener" href="{{ url($endpoint['uri']) }}">{{ url($endpoint['uri']) }}</a>
-                                @else
-                                    {{ url($endpoint['uri']) }}
-                                @endif
-                            </h5>
-                            <p class="mb-m">{{ $endpoint['description'] ?? '' }}</p>
-                            @if($endpoint['body_params'] ?? false)
-                                <details class="mb-m">
-                                    <summary class="text-muted">Body Parameters</summary>
-                                    <table class="table">
-                                        <tr>
-                                            <th>Param Name</th>
-                                            <th>Value Rules</th>
-                                        </tr>
-                                        @foreach($endpoint['body_params'] as $paramName => $rules)
-                                        <tr>
-                                            <td>{{ $paramName }}</td>
-                                            <td>
-                                                @foreach($rules as $rule)
-                                                    <code class="mr-xs">{{ $rule }}</code>
-                                                @endforeach
-                                            </td>
-                                        </tr>
-                                        @endforeach
-                                    </table>
-                                </details>
-                            @endif
-                            @if($endpoint['example_request'] ?? false)
-                                <details details-highlighter class="mb-m">
-                                    <summary class="text-muted">Example Request</summary>
-                                    <pre><code class="language-json">{{ $endpoint['example_request'] }}</code></pre>
-                                </details>
-                            @endif
-                            @if($endpoint['example_response'] ?? false)
-                                <details details-highlighter class="mb-m">
-                                    <summary class="text-muted">Example Response</summary>
-                                    <pre><code class="language-json">{{ $endpoint['example_response'] }}</code></pre>
-                                </details>
-                            @endif
-                            @if(!$loop->last)
-                            <hr>
-                            @endif
+                            @include('api-docs.parts.endpoint', ['endpoint' => $endpoint, 'loop' => $loop])
                         @endforeach
                     </section>
                 @endforeach
diff --git a/resources/views/api-docs/parts/endpoint.blade.php b/resources/views/api-docs/parts/endpoint.blade.php
new file mode 100644 (file)
index 0000000..c1bce80
--- /dev/null
@@ -0,0 +1,52 @@
+<h6 class="text-uppercase text-muted float right">{{ $endpoint['controller_method_kebab'] }}</h6>
+
+<h5 id="{{ $endpoint['name'] }}" class="text-mono mb-m">
+    <span class="api-method" data-method="{{ $endpoint['method'] }}">{{ $endpoint['method'] }}</span>
+    @if($endpoint['controller_method_kebab'] === 'list')
+        <a style="color: inherit;" target="_blank" rel="noopener" href="{{ url($endpoint['uri']) }}">{{ url($endpoint['uri']) }}</a>
+    @else
+        {{ url($endpoint['uri']) }}
+    @endif
+</h5>
+
+<p class="mb-m">{{ $endpoint['description'] ?? '' }}</p>
+
+@if($endpoint['body_params'] ?? false)
+    <details class="mb-m">
+        <summary class="text-muted">Body Parameters</summary>
+        <table class="table">
+            <tr>
+                <th>Param Name</th>
+                <th>Value Rules</th>
+            </tr>
+            @foreach($endpoint['body_params'] as $paramName => $rules)
+                <tr>
+                    <td>{{ $paramName }}</td>
+                    <td>
+                        @foreach($rules as $rule)
+                            <code class="mr-xs">{{ $rule }}</code>
+                        @endforeach
+                    </td>
+                </tr>
+            @endforeach
+        </table>
+    </details>
+@endif
+
+@if($endpoint['example_request'] ?? false)
+    <details details-highlighter class="mb-m">
+        <summary class="text-muted">Example Request</summary>
+        <pre><code class="language-json">{{ $endpoint['example_request'] }}</code></pre>
+    </details>
+@endif
+
+@if($endpoint['example_response'] ?? false)
+    <details details-highlighter class="mb-m">
+        <summary class="text-muted">Example Response</summary>
+        <pre><code class="language-json">{{ $endpoint['example_response'] }}</code></pre>
+    </details>
+@endif
+
+@if(!$loop->last)
+    <hr>
+@endif
\ No newline at end of file
diff --git a/resources/views/api-docs/parts/getting-started.blade.php b/resources/views/api-docs/parts/getting-started.blade.php
new file mode 100644 (file)
index 0000000..ba0f85f
--- /dev/null
@@ -0,0 +1,141 @@
+<h1 class="list-heading text-capitals mb-l">Getting Started</h1>
+
+<h5 id="authentication" class="text-mono mb-m">Authentication</h5>
+<p>
+    To access the API a user has to have the <em>"Access System API"</em> permission enabled on one of their assigned roles.
+    Permissions to content accessed via the API is limited by the roles & permissions assigned to the user that's used to access the API.
+</p>
+<p>Authentication to use the API is primarily done using API Tokens. Once the <em>"Access System API"</em> permission has been assigned to a user, a "API Tokens" section should be visible when editing their user profile. Choose "Create Token" and enter an appropriate name and expiry date, relevant for your API usage then press "Save". A "Token ID" and "Token Secret" will be immediately displayed. These values should be used as a header in API HTTP requests in the following format:</p>
+<pre><code class="language-css">Authorization: Token &lt;token_id&gt;:&lt;token_secret&gt;</code></pre>
+<p>Here's an example of an authorized cURL request to list books in the system:</p>
+<pre><code class="language-shell">curl --request GET \
+  --url https://example.com/api/books \
+  --header 'Authorization: Token C6mdvEQTGnebsmVn3sFNeeuelGEBjyQp:NOvD3VlzuSVuBPNaf1xWHmy7nIRlaj22'</code></pre>
+<p>If already logged into the system within the browser, via a user account with permission to access the API, the system will also accept an existing session meaning you can browse API endpoints directly in the browser or use the browser devtools to play with the API.</p>
+
+<hr>
+
+<h5 id="request-format" class="text-mono mb-m">Request Format</h5>
+<p>The API is primarily design to be interfaced using JSON so the majority of API endpoints, that accept data, will read JSON request data although <code>application/x-www-form-urlencoded</code> request data is also accepted. Endpoints that receive file data will need data sent in a <code>multipart/form-data</code> format although this will be highlighted in the documentation for such endpoints.</p>
+<p>For endpoints in this documentation that accept data, a "Body Parameters" table will be available showing the parameters that will accepted in the request. Any rules for the values of such parameters, such as the data-type or if they're required, will be shown alongside the parameter name.</p>
+
+<hr>
+
+<h5 id="listing-endpoints" class="text-mono mb-m">Listing Endpoints</h5>
+<p>Some endpoints will return a list of data models. These endpoints will return an array of the model data under a <code>data</code> property along with a numeric <code>total</code> property to indicate the total number of records found for the query within the system. Here's an example of a listing response:</p>
+<pre><code class="language-json">{
+  "data": [
+    {
+      "id": 1,
+      "name": "BookStack User Guide",
+      "slug": "bookstack-user-guide",
+      "description": "This is a general guide on using BookStack on a day-to-day basis.",
+      "created_at": "2019-05-05 21:48:46",
+      "updated_at": "2019-12-11 20:57:31",
+      "created_by": 1,
+      "updated_by": 1,
+      "image_id": 3
+    }
+  ],
+  "total": 16
+}</code></pre>
+<p>
+    There are a number of standard URL parameters that can be supplied to manipulate and page through the results returned from a listing endpoint:
+</p>
+<table class="table">
+    <tr>
+        <th>Parameter</th>
+        <th>Details</th>
+        <th width="30%">Examples</th>
+    </tr>
+    <tr>
+        <td>count</td>
+        <td>
+            Specify how many records will be returned in the response. <br>
+            (Default: {{ config('api.default_item_count') }}, Max: {{ config('api.max_item_count') }})
+        </td>
+        <td>Limit the count to 50<br><code>?count=50</code></td>
+    </tr>
+    <tr>
+        <td>offset</td>
+        <td>
+            Specify how many records to skip over in the response. <br>
+            (Default: 0)
+        </td>
+        <td>Skip over the first 100 records<br><code>?offset=100</code></td>
+    </tr>
+    <tr>
+        <td>sort</td>
+        <td>
+            Specify what field is used to sort the data and the direction of the sort (Ascending or Descending).<br>
+            Value is the name of a field, A <code>+</code> or <code>-</code> prefix dictates ordering. <br>
+            Direction defaults to ascending. <br>
+            Can use most fields shown in the response.
+        </td>
+        <td>
+            Sort by name ascending<br><code>?sort=+name</code> <br> <br>
+            Sort by "Created At" date descending<br><code>?sort=-created_at</code>
+        </td>
+    </tr>
+    <tr>
+        <td>filter[&lt;field&gt;]</td>
+        <td>
+            Specify a filter to be applied to the query. Can use most fields shown in the response. <br>
+            By default a filter will apply a "where equals" query but the below operations are available using the format filter[&lt;field&gt;:&lt;operation&gt;] <br>
+            <table>
+                <tr>
+                    <td>eq</td>
+                    <td>Where <code>&lt;field&gt;</code> equals the filter value.</td>
+                </tr>
+                <tr>
+                    <td>ne</td>
+                    <td>Where <code>&lt;field&gt;</code> does not equal the filter value.</td>
+                </tr>
+                <tr>
+                    <td>gt</td>
+                    <td>Where <code>&lt;field&gt;</code> is greater than the filter value.</td>
+                </tr>
+                <tr>
+                    <td>lt</td>
+                    <td>Where <code>&lt;field&gt;</code> is less than the filter value.</td>
+                </tr>
+                <tr>
+                    <td>gte</td>
+                    <td>Where <code>&lt;field&gt;</code> is greater than or equal to the filter value.</td>
+                </tr>
+                <tr>
+                    <td>lte</td>
+                    <td>Where <code>&lt;field&gt;</code> is less than or equal to the filter value.</td>
+                </tr>
+                <tr>
+                    <td>like</td>
+                    <td>
+                        Where <code>&lt;field&gt;</code> is "like" the filter value. <br>
+                        <code>%</code> symbols can be used as wildcards.
+                    </td>
+                </tr>
+            </table>
+        </td>
+        <td>
+            Filter where id is 5: <br><code>?filter[id]=5</code><br><br>
+            Filter where id is not 5: <br><code>?filter[id:ne]=5</code><br><br>
+            Filter where name contains "cat": <br><code>?filter[name:like]=%cat%</code><br><br>
+            Filter where created after 2020-01-01: <br><code>?filter[created_at:gt]=2020-01-01</code>
+        </td>
+    </tr>
+</table>
+
+<hr>
+
+<h5 id="error-handling" class="text-mono mb-m">Error Handling</h5>
+<p>
+    Successful responses will return a 200 or 204 HTTP response code. Errors will return a 4xx or a 5xx HTTP response code depending on the type of error. Errors follow a standard format as shown below. The message provided may be translated depending on the configured language of the system in addition to the API users' language preference. The code provided in the JSON response will match the HTTP response code.
+</p>
+
+<pre><code class="language-json">{
+       "error": {
+               "code": 401,
+               "message": "No authorization token found on the request"
+       }
+}
+</code></pre>
\ No newline at end of file
index ee86dc24006510ee905aadc16aef601aeb2af259..15837448ac0be451ee46c1d2600ee63e41fa4e89 100644 (file)
@@ -22,7 +22,7 @@
             <button refs="tabs@toggleLink" type="button" class="tab-item {{ $attachment->external ? 'selected' : '' }}">{{ trans('entities.attachments_set_link') }}</button>
         </div>
         <div refs="tabs@contentFile" class="mb-m {{ $attachment->external ? 'hidden' : '' }}">
-            @include('components.dropzone', [
+            @include('form.dropzone', [
                 'placeholder' => trans('entities.attachments_edit_drop_upload'),
                 'url' =>  url('/attachments/upload/' . $attachment->id),
                 'successMessage' => trans('entities.attachments_file_updated'),
index 4628f7495650def200565f8babea00a06b96b464..024cb583c522e8147fca04626f1a67ba09e4e31a 100644 (file)
@@ -18,7 +18,7 @@
                     @include('attachments.manager-list', ['attachments' => $page->attachments->all()])
                 </div>
                 <div refs="tabs@contentUpload" class="hidden">
-                    @include('components.dropzone', [
+                    @include('form.dropzone', [
                         'placeholder' => trans('entities.attachments_dropzone'),
                         'url' =>  url('/attachments/upload?uploaded_to=' . $page->id),
                         'successMessage' => trans('entities.attachments_file_uploaded'),
index fbe62f21e9fabdbe036f0d635dde3913d04291e4..c29ed57067bd1f1b1c47f5ddc1bc9de4a3b980d7 100644 (file)
@@ -1,4 +1,4 @@
-@extends('simple-layout')
+@extends('layouts.simple')
 
 @section('content')
 
index 4212c1964c8c0c478aa8c7f3c51da1607f13fe94..de99bb3f29feeb55babaa3f13020c474978f4a1f 100644 (file)
@@ -1,4 +1,4 @@
-@extends('simple-layout')
+@extends('layouts.simple')
 
 @section('content')
 
@@ -9,7 +9,7 @@
         <div class="card content-wrap auto-height">
             <h1 class="list-heading">{{ Str::title(trans('auth.log_in')) }}</h1>
 
-            @include('auth.forms.login.' . $authMethod)
+            @include('auth.parts.login-form-' . $authMethod)
 
             @if(count($socialDrivers) > 0)
                 <hr class="my-l">
similarity index 95%
rename from resources/views/auth/forms/login/standard.blade.php
rename to resources/views/auth/parts/login-form-standard.blade.php
index 87603e2cb6dc8fd88a9c41bbb9b3ad0d5d35b992..71989dc2f23195353096e8162f2db7b26992931f 100644 (file)
@@ -18,7 +18,7 @@
 
     <div class="grid half collapse-xs gap-xl v-center">
         <div class="text-left ml-xxs">
-            @include('components.custom-checkbox', [
+            @include('form.custom-checkbox', [
                 'name' => 'remember',
                 'checked' => false,
                 'value' => 'on',
index 8273ed2356bf931f7fbe142fd6e4ad1e4841baec..d42b9e3c66d68a09bd19121363aca6900cb9f9e0 100644 (file)
@@ -1,4 +1,4 @@
-@extends('simple-layout')
+@extends('layouts.simple')
 
 @section('content')
     <div class="container very-small mt-xl">
index 930544cde40e870c788c1793afeb70661bc010a2..de332f9e177b36ef16b17022b38bc0415d5eabf2 100644 (file)
@@ -1,4 +1,4 @@
-@extends('simple-layout')
+@extends('layouts.simple')
 
 @section('content')
 
index 8fbf8abbbd7beae0c6d70af0a87d3183c4a57492..0de877a53f49509799b9f2695791cd29b7e80c90 100644 (file)
@@ -1,4 +1,4 @@
-@extends('simple-layout')
+@extends('layouts.simple')
 
 @section('content')
 
index d3483c6e610f5a3d370a8255b053c58d8b665b02..91ec0b621f12f2190edd676c31e3ff1310db7dc2 100644 (file)
@@ -1,4 +1,4 @@
-@extends('simple-layout')
+@extends('layouts.simple')
 
 @section('content')
     <div class="container very-small">
index 85473685b96207e108d07d3093a2c23fa4f43bcd..2f780b8a31f21eaa097966fdc7b1cb4655b6baef 100644 (file)
@@ -1,4 +1,4 @@
-@extends('simple-layout')
+@extends('layouts.simple')
 
 @section('content')
 
@@ -17,8 +17,8 @@
                 {!! csrf_field() !!}
                 <div class="form-group">
                     <label for="email">{{ trans('auth.email') }}</label>
-                    @if(auth()->check())
-                        @include('form.text', ['name' => 'email', 'model' => auth()->user()])
+                    @if($user)
+                        @include('form.text', ['name' => 'email', 'model' => $user])
                     @else
                         @include('form.text', ['name' => 'email'])
                     @endif
diff --git a/resources/views/books/_breadcrumbs.blade.php b/resources/views/books/_breadcrumbs.blade.php
deleted file mode 100644 (file)
index e4ecc36..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-<div class="breadcrumbs">
-    <a href="{{$book->getUrl()}}" class="text-book text-button">@icon('book'){{ $book->getShortName() }}</a>
-</div>
\ No newline at end of file
index db3e90e51913a8d6870417a1687984107a050ec7..eead4191c345260382c36fc664c2bfc91eaaf5f1 100644 (file)
@@ -1,10 +1,10 @@
-@extends('simple-layout')
+@extends('layouts.simple')
 
 @section('body')
     <div class="container small">
         <div class="my-s">
             @if (isset($bookshelf))
-                @include('partials.breadcrumbs', ['crumbs' => [
+                @include('entities.breadcrumbs', ['crumbs' => [
                     $bookshelf,
                     $bookshelf->getUrl('/create-book') => [
                         'text' => trans('entities.books_create'),
@@ -12,7 +12,7 @@
                     ]
                 ]])
             @else
-                @include('partials.breadcrumbs', ['crumbs' => [
+                @include('entities.breadcrumbs', ['crumbs' => [
                     '/books' => [
                         'text' => trans('entities.books'),
                         'icon' => 'book'
@@ -28,7 +28,7 @@
         <main class="content-wrap card">
             <h1 class="list-heading">{{ trans('entities.books_create') }}</h1>
             <form action="{{ isset($bookshelf) ? $bookshelf->getUrl('/create-book') : url('/books') }}" method="POST" enctype="multipart/form-data">
-                @include('books.form', ['returnLocation' => isset($bookshelf) ? $bookshelf->getUrl() : url('/books')])
+                @include('books.parts.form', ['returnLocation' => isset($bookshelf) ? $bookshelf->getUrl() : url('/books')])
             </form>
         </main>
     </div>
index be3f742cba5d13f5bc45c01864d3122ba4043283..b0f3590c17cd73b27f0f4f8f48c291747109ef8e 100644 (file)
@@ -1,11 +1,11 @@
-@extends('simple-layout')
+@extends('layouts.simple')
 
 @section('body')
 
     <div class="container small">
 
         <div class="my-s">
-            @include('partials.breadcrumbs', ['crumbs' => [
+            @include('entities.breadcrumbs', ['crumbs' => [
                 $book,
                 $book->getUrl('/delete') => [
                     'text' => trans('entities.books_delete'),
index ac11b58e201df206b3a8cc4b58d8422a16606877..4039771213d49e57d9d6e9924c00b67ebee25c8e 100644 (file)
@@ -1,11 +1,11 @@
-@extends('simple-layout')
+@extends('layouts.simple')
 
 @section('body')
 
     <div class="container small">
 
         <div class="my-s">
-            @include('partials.breadcrumbs', ['crumbs' => [
+            @include('entities.breadcrumbs', ['crumbs' => [
                 $book,
                 $book->getUrl('/edit') => [
                     'text' => trans('entities.books_edit'),
@@ -18,7 +18,7 @@
             <h1 class="list-heading">{{ trans('entities.books_edit') }}</h1>
             <form action="{{ $book->getUrl() }}" method="POST" enctype="multipart/form-data">
                 <input type="hidden" name="_method" value="PUT">
-                @include('books.form', ['model' => $book, 'returnLocation' => $book->getUrl()])
+                @include('books.parts.form', ['model' => $book, 'returnLocation' => $book->getUrl()])
             </form>
         </main>
     </div>
index 9cd5618a53f3be19fe609fefdeadb3e6e1fed4f0..0b6b4a58c19108e6bcf341ea46b8c37df633610c 100644 (file)
@@ -1,4 +1,4 @@
-@extends('export-layout')
+@extends('layouts.export')
 
 @section('title', $book->name)
 
index 81fb66cfcd18148a796cd478ed53b43110a4817c..5f1bd374372b0d9ad3dfce808363b8976cecb703 100644 (file)
@@ -1,21 +1,21 @@
-@extends('tri-layout')
+@extends('layouts.tri')
 
 @section('body')
-    @include('books.list', ['books' => $books, 'view' => $view])
+    @include('books.parts.list', ['books' => $books, 'view' => $view])
 @stop
 
 @section('left')
     @if($recents)
         <div id="recents" class="mb-xl">
             <h5>{{ trans('entities.recently_viewed') }}</h5>
-            @include('partials.entity-list', ['entities' => $recents, 'style' => 'compact'])
+            @include('entities.list', ['entities' => $recents, 'style' => 'compact'])
         </div>
     @endif
 
     <div id="popular" class="mb-xl">
         <h5>{{ trans('entities.books_popular') }}</h5>
         @if(count($popular) > 0)
-            @include('partials.entity-list', ['entities' => $popular, 'style' => 'compact'])
+            @include('entities.list', ['entities' => $popular, 'style' => 'compact'])
         @else
             <div class="body text-muted">{{ trans('entities.books_popular_empty') }}</div>
         @endif
@@ -24,7 +24,7 @@
     <div id="new" class="mb-xl">
         <h5>{{ trans('entities.books_new') }}</h5>
         @if(count($popular) > 0)
-            @include('partials.entity-list', ['entities' => $new, 'style' => 'compact'])
+            @include('entities.list', ['entities' => $new, 'style' => 'compact'])
         @else
             <div class="body text-muted">{{ trans('entities.books_new_empty') }}</div>
         @endif
@@ -43,7 +43,7 @@
                 </a>
             @endif
 
-            @include('partials.view-toggle', ['view' => $view, 'type' => 'books'])
+            @include('entities.view-toggle', ['view' => $view, 'type' => 'books'])
         </div>
     </div>
 
similarity index 92%
rename from resources/views/books/form.blade.php
rename to resources/views/books/parts/form.blade.php
index 9fdcaea61be259070a57dcc0d76b1800a280e4bc..bb87089b272ac3a30e62f5846c7611c67ca8bb4a 100644 (file)
@@ -17,7 +17,7 @@
     <div class="collapse-content" collapsible-content>
         <p class="small">{{ trans('common.cover_image_description') }}</p>
 
-        @include('components.image-picker', [
+        @include('form.image-picker', [
             'defaultImage' => url('/book_default_cover.png'),
             'currentImage' => (isset($model) && $model->cover) ? $model->getBookCover() : url('/book_default_cover.png') ,
             'name' => 'image',
@@ -31,7 +31,7 @@
         <label for="tag-manager">{{ trans('entities.book_tags') }}</label>
     </button>
     <div class="collapse-content" collapsible-content>
-        @include('components.tag-manager', ['entity' => $book ?? null])
+        @include('entities.tag-manager', ['entity' => $book ?? null])
     </div>
 </div>
 
similarity index 84%
rename from resources/views/books/list.blade.php
rename to resources/views/books/parts/list.blade.php
index 52cd935d1182a7babb4b2926e56f8586ecf08cf8..30b0766135ccff81c87fb2d3fa7c1a5ba9d2de4a 100644 (file)
@@ -3,7 +3,7 @@
         <h1 class="list-heading">{{ trans('entities.books') }}</h1>
         <div class="text-m-right my-m">
 
-            @include('partials.sort', ['options' => [
+            @include('entities.sort', ['options' => [
                 'name' => trans('common.sort_name'),
                 'created_at' => trans('common.sort_created_at'),
                 'updated_at' => trans('common.sort_updated_at'),
         @if($view === 'list')
             <div class="entity-list">
                 @foreach($books as $book)
-                    @include('books.list-item', ['book' => $book])
+                    @include('books.parts.list-item', ['book' => $book])
                 @endforeach
             </div>
         @else
              <div class="grid third">
                 @foreach($books as $key => $book)
-                    @include('partials.entity-grid-item', ['entity' => $book])
+                    @include('entities.grid-item', ['entity' => $book])
                 @endforeach
              </div>
         @endif
index b387ed6c7c94b082800b1a72979c61668308a2f7..d72042d42f5efe3d1ec4d4abfb97f0aa7f74e3d9 100644 (file)
@@ -1,11 +1,11 @@
-@extends('simple-layout')
+@extends('layouts.simple')
 
 @section('body')
 
     <div class="container">
 
         <div class="my-s">
-            @include('partials.breadcrumbs', ['crumbs' => [
+            @include('entities.breadcrumbs', ['crumbs' => [
                 $book,
                 $book->getUrl('/permissions') => [
                     'text' => trans('entities.books_permissions'),
index 5879cf6a22277283180e6b3782564618bf0640b5..25a6f69fad9ffec241720eac4b4b39d407903798 100644 (file)
@@ -1,4 +1,4 @@
-@extends('tri-layout')
+@extends('layouts.tri')
 
 @section('container-attrs')
     component="entity-search"
@@ -16,7 +16,7 @@
 @section('body')
 
     <div class="mb-s">
-        @include('partials.breadcrumbs', ['crumbs' => [
+        @include('entities.breadcrumbs', ['crumbs' => [
             $book,
         ]])
     </div>
@@ -29,9 +29,9 @@
                 <div class="entity-list book-contents">
                     @foreach($bookChildren as $childElement)
                         @if($childElement->isA('chapter'))
-                            @include('chapters.list-item', ['chapter' => $childElement])
+                            @include('chapters.parts.list-item', ['chapter' => $childElement])
                         @else
-                            @include('pages.list-item', ['page' => $childElement])
+                            @include('pages.parts.list-item', ['page' => $childElement])
                         @endif
                     @endforeach
                 </div>
@@ -59,7 +59,7 @@
             @endif
         </div>
 
-        @include('partials.entity-search-results')
+        @include('entities.search-results')
     </main>
 
 @stop
@@ -68,7 +68,7 @@
     <div class="mb-xl">
         <h5>{{ trans('common.details') }}</h5>
         <div class="text-small text-muted blended-links">
-            @include('partials.entity-meta', ['entity' => $book])
+            @include('entities.meta', ['entity' => $book])
             @if($book->restricted)
                 <div class="active-restriction">
                     @if(userCan('restrictions-manage', $book))
             <hr class="primary-background">
 
             @if(signedInUser())
-                @include('partials.entity-favourite-action', ['entity' => $book])
+                @include('entities.favourite-action', ['entity' => $book])
+            @endif
+            @if(userCan('content-export'))
+                @include('entities.export-menu', ['entity' => $book])
             @endif
-            @include('partials.entity-export-menu', ['entity' => $book])
         </div>
     </div>
 
 
 @section('left')
 
-    @include('partials.entity-search-form', ['label' => trans('entities.books_search_this')])
+    @include('entities.search-form', ['label' => trans('entities.books_search_this')])
 
     @if($book->tags->count() > 0)
         <div class="mb-xl">
-            @include('components.tag-list', ['entity' => $book])
+            @include('entities.tag-list', ['entity' => $book])
         </div>
     @endif
 
     @if(count($bookParentShelves) > 0)
         <div class="actions mb-xl">
             <h5>{{ trans('entities.shelves_long') }}</h5>
-            @include('partials.entity-list', ['entities' => $bookParentShelves, 'style' => 'compact'])
+            @include('entities.list', ['entities' => $bookParentShelves, 'style' => 'compact'])
         </div>
     @endif
 
     @if(count($activity) > 0)
         <div class="mb-xl">
             <h5>{{ trans('entities.recent_activity') }}</h5>
-            @include('partials.activity-list', ['activity' => $activity])
+            @include('common.activity-list', ['activity' => $activity])
         </div>
     @endif
 @stop
index 642b88c873a1c442451494903a5db83db76c5c33..a24bd8959203c9b5f24b263aec0374d8fced88c2 100644 (file)
@@ -1,11 +1,11 @@
-@extends('simple-layout')
+@extends('layouts.simple')
 
 @section('body')
 
     <div class="container">
 
         <div class="my-s">
-            @include('partials.breadcrumbs', ['crumbs' => [
+            @include('entities.breadcrumbs', ['crumbs' => [
                 $book,
                 $book->getUrl('/sort') => [
                     'text' => trans('entities.books_sort'),
@@ -19,7 +19,7 @@
                 <div book-sort class="card content-wrap">
                     <h1 class="list-heading mb-l">{{ trans('entities.books_sort') }}</h1>
                     <div book-sort-boxes>
-                        @include('books.sort-box', ['book' => $book, 'bookChildren' => $bookChildren])
+                        @include('books.parts.sort-box', ['book' => $book, 'bookChildren' => $bookChildren])
                     </div>
 
                     <form action="{{ $book->getUrl('/sort') }}" method="POST">
@@ -38,7 +38,7 @@
                 <main class="card content-wrap">
                     <h2 class="list-heading mb-m">{{ trans('entities.books_sort_show_other') }}</h2>
 
-                    @include('components.entity-selector', ['name' => 'books_list', 'selectorSize' => 'compact', 'entityTypes' => 'book', 'entityPermission' => 'update', 'showAdd' => true])
+                    @include('entities.selector', ['name' => 'books_list', 'selectorSize' => 'compact', 'entityTypes' => 'book', 'entityPermission' => 'update', 'showAdd' => true])
 
                 </main>
             </div>
index c9787e6348991073a5c3e265097e0926b8179338..1216f4d277cb2afa222dfc5f82606cca763d9231 100644 (file)
@@ -1,10 +1,10 @@
-@extends('simple-layout')
+@extends('layouts.simple')
 
 @section('body')
     <div class="container small">
 
         <div class="my-s">
-            @include('partials.breadcrumbs', ['crumbs' => [
+            @include('entities.breadcrumbs', ['crumbs' => [
                 $book,
                 $book->getUrl('create-chapter') => [
                     'text' => trans('entities.chapters_create'),
@@ -16,7 +16,7 @@
         <main class="content-wrap card">
             <h1 class="list-heading">{{ trans('entities.chapters_create') }}</h1>
             <form action="{{ $book->getUrl('/create-chapter') }}" method="POST">
-                @include('chapters.form')
+                @include('chapters.parts.form')
             </form>
         </main>
 
index 60f8c99339022535b48019d4232b3a764c831574..e0e774ddf7ce14f7319c3344c73a7c25628a6c42 100644 (file)
@@ -1,11 +1,11 @@
-@extends('simple-layout')
+@extends('layouts.simple')
 
 @section('body')
 
     <div class="container small">
 
         <div class="my-s">
-            @include('partials.breadcrumbs', ['crumbs' => [
+            @include('entities.breadcrumbs', ['crumbs' => [
                 $chapter->book,
                 $chapter,
                 $chapter->getUrl('/delete') => [
index d8bb056f632ce8d172da67ebb8b2e50ad7c092cb..65c48c18d77aba368a7af0872ad273d672e764a2 100644 (file)
@@ -1,11 +1,11 @@
-@extends('simple-layout')
+@extends('layouts.simple')
 
 @section('body')
 
     <div class="container small">
 
         <div class="my-s">
-            @include('partials.breadcrumbs', ['crumbs' => [
+            @include('entities.breadcrumbs', ['crumbs' => [
                 $book,
                 $chapter,
                 $chapter->getUrl('/edit') => [
@@ -19,7 +19,7 @@
             <h1 class="list-heading">{{ trans('entities.chapters_edit') }}</h1>
             <form action="{{  $chapter->getUrl() }}" method="POST">
                 <input type="hidden" name="_method" value="PUT">
-                @include('chapters.form', ['model' => $chapter])
+                @include('chapters.parts.form', ['model' => $chapter])
             </form>
         </main>
 
index 18f056f27f175570b8546be9322bc858afecbd19..61286ab170a9d4fd471f2cb22f9e9aeb8e7e2a73 100644 (file)
@@ -1,4 +1,4 @@
-@extends('export-layout')
+@extends('layouts.export')
 
 @section('title', $chapter->name)
 
index 8663dca5050e53fb0dbbc9ab6b369d720bbab59a..96d4021ee4cf247c4e2232e9fc31084a9bbfafe2 100644 (file)
@@ -1,11 +1,11 @@
-@extends('simple-layout')
+@extends('layouts.simple')
 
 @section('body')
 
     <div class="container small">
 
         <div class="my-s">
-            @include('partials.breadcrumbs', ['crumbs' => [
+            @include('entities.breadcrumbs', ['crumbs' => [
                 $chapter->book,
                 $chapter,
                 $chapter->getUrl('/move') => [
@@ -23,7 +23,7 @@
                 {!! csrf_field() !!}
                 <input type="hidden" name="_method" value="PUT">
 
-                @include('components.entity-selector', ['name' => 'entity_selection', 'selectorSize' => 'large', 'entityTypes' => 'book', 'entityPermission' => 'chapter-create'])
+                @include('entities.selector', ['name' => 'entity_selection', 'selectorSize' => 'large', 'entityTypes' => 'book', 'entityPermission' => 'chapter-create'])
 
                 <div class="form-group text-right">
                     <a href="{{ $chapter->getUrl() }}" class="button outline">{{ trans('common.cancel') }}</a>
similarity index 81%
rename from resources/views/chapters/child-menu.blade.php
rename to resources/views/chapters/parts/child-menu.blade.php
index a1358e1db4e0398cc8d339a79213df190a2ab3f5..a00f0f7e1ae341c3a6638fe091c153118e7c9baf 100644 (file)
@@ -6,7 +6,7 @@
     <ul class="sub-menu inset-list @if($isOpen) open @endif" @if($isOpen) style="display: block;" @endif role="menu">
         @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' : '' ])
+                @include('entities.list-item-basic', ['entity' => $childPage, 'classes' => $current->matches($childPage)? 'selected' : '' ])
             </li>
         @endforeach
     </ul>
similarity index 92%
rename from resources/views/chapters/form.blade.php
rename to resources/views/chapters/parts/form.blade.php
index 0de665744474c40a20c918741ccf7bfd71070062..3908d0693fb7a3ec635be5d609dfb61f579c3d7c 100644 (file)
@@ -16,7 +16,7 @@
         <label for="tags">{{ trans('entities.chapter_tags') }}</label>
     </button>
     <div class="collapse-content" collapsible-content>
-        @include('components.tag-manager', ['entity' => $chapter ?? null])
+        @include('entities.tag-manager', ['entity' => $chapter ?? null])
     </div>
 </div>
 
similarity index 92%
rename from resources/views/chapters/list-item.blade.php
rename to resources/views/chapters/parts/list-item.blade.php
index 9186983332eaae842d8af07aff34abbfcfd6f994..285e3489353cca255481e32241245f856527aae0 100644 (file)
@@ -18,7 +18,7 @@
                     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->visible_pages])
+                    @include('entities.list', ['entities' => $chapter->visible_pages])
                 </div>
             </div>
         </div>
index 48c954dc96871b0f23154772ec2ba6e79a600d80..6b4e219384a746e22841b27c60fbff24a9c48e37 100644 (file)
@@ -1,11 +1,11 @@
-@extends('simple-layout')
+@extends('layouts.simple')
 
 @section('body')
 
     <div class="container">
 
         <div class="my-s">
-            @include('partials.breadcrumbs', ['crumbs' => [
+            @include('entities.breadcrumbs', ['crumbs' => [
                 $chapter->book,
                 $chapter,
                 $chapter->getUrl('/permissions') => [
index e8e0f6374fe24ccdd33f97300d54919bb8861969..1646d4f18d0e1d33ab904eb86671dd855bbc98e9 100644 (file)
@@ -1,4 +1,4 @@
-@extends('tri-layout')
+@extends('layouts.tri')
 
 @section('container-attrs')
     component="entity-search"
@@ -13,7 +13,7 @@
 @section('body')
 
     <div class="mb-m print-hidden">
-        @include('partials.breadcrumbs', ['crumbs' => [
+        @include('entities.breadcrumbs', ['crumbs' => [
             $chapter->book,
             $chapter,
         ]])
@@ -26,7 +26,7 @@
             @if(count($pages) > 0)
                 <div class="entity-list book-contents">
                     @foreach($pages as $page)
-                        @include('pages.list-item', ['page' => $page])
+                        @include('pages.parts.list-item', ['page' => $page])
                     @endforeach
                 </div>
             @else
             @endif
         </div>
 
-        @include('partials.entity-search-results')
+        @include('entities.search-results')
     </main>
 
-    @include('partials.entity-sibling-navigation', ['next' => $next, 'previous' => $previous])
+    @include('entities.sibling-navigation', ['next' => $next, 'previous' => $previous])
 
 @stop
 
@@ -65,7 +65,7 @@
     <div class="mb-xl">
         <h5>{{ trans('common.details') }}</h5>
         <div class="blended-links text-small text-muted">
-            @include('partials.entity-meta', ['entity' => $chapter])
+            @include('entities.meta', ['entity' => $chapter])
 
             @if($book->restricted)
                 <div class="active-restriction">
             <hr class="primary-background"/>
 
             @if(signedInUser())
-                @include('partials.entity-favourite-action', ['entity' => $chapter])
+                @include('entities.favourite-action', ['entity' => $chapter])
+            @endif
+            @if(userCan('content-export'))
+                @include('entities.export-menu', ['entity' => $chapter])
             @endif
-            @include('partials.entity-export-menu', ['entity' => $chapter])
         </div>
     </div>
 @stop
 
 @section('left')
 
-    @include('partials.entity-search-form', ['label' => trans('entities.chapters_search_this')])
+    @include('entities.search-form', ['label' => trans('entities.chapters_search_this')])
 
     @if($chapter->tags->count() > 0)
         <div class="mb-xl">
-            @include('components.tag-list', ['entity' => $chapter])
+            @include('entities.tag-list', ['entity' => $chapter])
         </div>
     @endif
 
-    @include('partials.book-tree', ['book' => $book, 'sidebarTree' => $sidebarTree])
+    @include('entities.book-tree', ['book' => $book, 'sidebarTree' => $sidebarTree])
 @stop
 
 
index 322477ebdc4e98783c8a666043b048ae36fda804..9f4a12357156cb41b65f08f61955c9d636e7d763 100644 (file)
@@ -48,7 +48,7 @@
 
     <div comment-content class="content px-s pb-s">
         <div class="form-group loading" style="display: none;">
-            @include('partials.loading-icon', ['text' => trans('entities.comment_deleting')])
+            @include('common.loading-icon', ['text' => trans('entities.comment_deleting')])
         </div>
         {!! $comment->html  !!}
     </div>
@@ -64,7 +64,7 @@
                     <button type="submit" class="button">{{ trans('entities.comment_save') }}</button>
                 </div>
                 <div class="form-group loading" style="display: none;">
-                    @include('partials.loading-icon', ['text' => trans('entities.comment_saving')])
+                    @include('common.loading-icon', ['text' => trans('entities.comment_saving')])
                 </div>
             </form>
         </div>
index 12216b95b9bdb1f5ef33bfc3403094773b5aa73b..a5a84b004dc8e20a2fbf514487f5bfa13b548e3d 100644 (file)
@@ -24,7 +24,7 @@
                 <button type="submit" class="button">{{ trans('entities.comment_save') }}</button>
             </div>
             <div class="form-group loading" style="display: none;">
-                @include('partials.loading-icon', ['text' => trans('entities.comment_saving')])
+                @include('common.loading-icon', ['text' => trans('entities.comment_saving')])
             </div>
         </form>
     </div>
similarity index 76%
rename from resources/views/partials/activity-list.blade.php
rename to resources/views/common/activity-list.blade.php
index 397a69d38cb8f3b471cad9acfaaa958faa0ec0ea..90272b21c6921ab8672bfa788f52b2aa09cd0641 100644 (file)
@@ -3,7 +3,7 @@
     <div class="activity-list">
         @foreach($activity as $activityItem)
             <div class="activity-list-item">
-                @include('partials.activity-item', ['activity' => $activityItem])
+                @include('common.activity-item', ['activity' => $activityItem])
             </div>
         @endforeach
     </div>
similarity index 55%
rename from resources/views/partials/custom-head.blade.php
rename to resources/views/common/custom-head.blade.php
index fa5ba0cc456333776de144372098e68fc7f3fe65..6f88bd43f7a9cd77d8d1aec9a85c665fadb27e7c 100644 (file)
@@ -1,5 +1,7 @@
+@inject('headContent', 'BookStack\Theming\CustomHtmlHeadContentProvider')
+
 @if(setting('app-custom-head') && \Route::currentRouteName() !== 'settings')
 <!-- Custom user content -->
-{!! setting('app-custom-head') !!}
+{!! $headContent->forWeb() !!}
 <!-- End custom user content -->
 @endif
\ No newline at end of file
index af9490a410f897a62891230268130591f8e253c3..d2606d816c956eb4deb666bb970d0c4f0889c980 100644 (file)
@@ -1,4 +1,4 @@
-@extends('simple-layout')
+@extends('layouts.simple')
 
 @section('body')
     <div class="container small pt-xl">
@@ -6,7 +6,7 @@
             <h1 class="list-heading">{{ $title }}</h1>
 
             <div class="book-contents">
-                @include('partials.entity-list', ['entities' => $entities, 'style' => 'detailed'])
+                @include('entities.list', ['entities' => $entities, 'style' => 'detailed'])
             </div>
 
             <div class="text-center">
index 5790f20655fe29852cc1d8b62c71ed5ff729fedf..5413d239ec9b3a790a8e7ba2463c65d673324558 100644 (file)
@@ -1,4 +1,4 @@
-@extends('simple-layout')
+@extends('layouts.simple')
 
 @section('body')
     <div class="container small pt-xl">
@@ -6,7 +6,7 @@
             <h1 class="list-heading">{{ $title }}</h1>
 
             <div class="book-contents">
-                @include('partials.entity-list', ['entities' => $entities, 'style' => 'detailed'])
+                @include('entities.list', ['entities' => $entities, 'style' => 'detailed'])
             </div>
 
             <div class="text-right">
diff --git a/resources/views/common/export-custom-head.blade.php b/resources/views/common/export-custom-head.blade.php
new file mode 100644 (file)
index 0000000..2452d6b
--- /dev/null
@@ -0,0 +1,7 @@
+@inject('headContent', 'BookStack\Theming\CustomHtmlHeadContentProvider')
+
+@if(setting('app-custom-head'))
+<!-- Custom user content -->
+{!! $headContent->forExport() !!}
+<!-- End custom user content -->
+@endif
\ No newline at end of file
index 274a09996125f21303d4d38a58b91f4214fe65f1..2311ce3e019e9f529ef2fbde6569a518db4f11c6 100644 (file)
@@ -79,7 +79,7 @@
                             </li>
                             <li><hr></li>
                             <li>
-                                @include('partials.dark-mode-toggle')
+                                @include('common.dark-mode-toggle')
                             </li>
                         </ul>
                     </div>
similarity index 76%
rename from resources/views/partials/book-tree.blade.php
rename to resources/views/entities/book-tree.blade.php
index 15b5832897d01756c7fc59b22a89805bdf7a8bbe..ce016143a30bbc645f2784db5e62b89fcac2f6d0 100644 (file)
@@ -7,19 +7,19 @@
     <ul class="sidebar-page-list mt-xs menu entity-list">
         @if (userCan('view', $book))
             <li class="list-item-book book">
-                @include('partials.entity-list-item-basic', ['entity' => $book, 'classes' => ($current->matches($book)? 'selected' : '')])
+                @include('entities.list-item-basic', ['entity' => $book, 'classes' => ($current->matches($book)? 'selected' : '')])
             </li>
         @endif
 
         @foreach($sidebarTree as $bookChild)
             <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' : ''])
+                @include('entities.list-item-basic', ['entity' => $bookChild, 'classes' => $current->matches($bookChild)? 'selected' : ''])
 
                 @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">
-                            @include('chapters.child-menu', [
+                            @include('chapters.parts.child-menu', [
                                 'chapter' => $bookChild,
                                 'current' => $current,
                                 'isOpen'  => $bookChild->matchesOrContains($current)
similarity index 95%
rename from resources/views/partials/breadcrumb-listing.blade.php
rename to resources/views/entities/breadcrumb-listing.blade.php
index 2a559aa7d5f7f28b39b253698da83c4ab3664051..929f56ed3c62e53b29e6ffde41d84186be90ac0e 100644 (file)
@@ -16,7 +16,7 @@
                    type="text">
         </div>
         <div refs="dropdown-search@loading">
-            @include('partials.loading-icon')
+            @include('common.loading-icon')
         </div>
         <div refs="dropdown-search@listContainer" class="dropdown-search-list px-m"></div>
     </div>
similarity index 96%
rename from resources/views/partials/breadcrumbs.blade.php
rename to resources/views/entities/breadcrumbs.blade.php
index 065aa842026e91ca481dae18a5a606a5bcfe341d..d078d987322a255726ef3e4052e12bf6a0b32770 100644 (file)
@@ -40,7 +40,7 @@
             </a>
         @elseif($isEntity && userCan('view', $crumb))
             @if($breadcrumbCount > 0)
-                @include('partials.breadcrumb-listing', ['entity' => $crumb])
+                @include('entities.breadcrumb-listing', ['entity' => $crumb])
             @endif
             <a href="{{ $crumb->getUrl() }}" class="text-{{$crumb->getType()}} icon-list-item outline-hover">
                 <span>@icon($crumb->getType())</span>
similarity index 76%
rename from resources/views/partials/entity-list-basic.blade.php
rename to resources/views/entities/list-basic.blade.php
index dc5c3f33385c2e29e873169540ebf87d3f34179c..84b91048418e1b5704ffd9c3c1b8668f0265c20f 100644 (file)
@@ -1,7 +1,7 @@
 <div class="entity-list {{ $style ?? '' }}">
     @if(count($entities) > 0)
         @foreach($entities as $index => $entity)
-            @include('partials.entity-list-item-basic', ['entity' => $entity])
+            @include('entities.list-item-basic', ['entity' => $entity])
         @endforeach
     @else
         <p class="text-muted empty-text">
similarity index 79%
rename from resources/views/partials/entity-list-item.blade.php
rename to resources/views/entities/list-item.blade.php
index d605953c77fd38541ff18e976ea81433e3329b52..8b5eb20b04f81e7d5e8e89b6736b63838c1a49be 100644 (file)
@@ -1,4 +1,4 @@
-@component('partials.entity-list-item-basic', ['entity' => $entity])
+@component('entities.list-item-basic', ['entity' => $entity])
 
 <div class="entity-item-snippet">
 
@@ -16,7 +16,7 @@
 
 @if(($showTags ?? false) && $entity->tags->count() > 0)
     <div class="entity-item-tags mt-xs">
-        @include('components.tag-list', ['entity' => $entity, 'linked' => false ])
+        @include('entities.tag-list', ['entity' => $entity, 'linked' => false ])
     </div>
 @endif
 
similarity index 63%
rename from resources/views/partials/entity-list.blade.php
rename to resources/views/entities/list.blade.php
index 393f4e8a792c5af7f92afc92020bb4c599bcb598..25673c583baf4a2cbba4f743552d5373423d76db 100644 (file)
@@ -1,7 +1,7 @@
 @if(count($entities) > 0)
     <div class="entity-list {{ $style ?? '' }}">
         @foreach($entities as $index => $entity)
-            @include('partials.entity-list-item', ['entity' => $entity, 'showPath' => $showPath ?? false, 'showTags' => $showTags ?? false])
+            @include('entities.list-item', ['entity' => $entity, 'showPath' => $showPath ?? false, 'showTags' => $showTags ?? false])
         @endforeach
     </div>
 @else
similarity index 91%
rename from resources/views/partials/entity-search-results.blade.php
rename to resources/views/entities/search-results.blade.php
index 74619831af01021ec8b01479e73eddb22a845355..a3c4aa8f777910c5abace6c7e234f05891fff9d0 100644 (file)
@@ -9,7 +9,7 @@
     </div>
 
     <div refs="entity-search@loadingBlock">
-        @include('partials.loading-icon')
+        @include('common.loading-icon')
     </div>
     <div class="book-contents" refs="entity-search@searchResults"></div>
 </div>
\ No newline at end of file
similarity index 88%
rename from resources/views/components/entity-selector-popup.blade.php
rename to resources/views/entities/selector-popup.blade.php
index ec8712b6a5bae9d533009d2f0c6b49b22e2c1a65..ab73a014fa8d2a6c52b68c3c0ec97823414164e0 100644 (file)
@@ -5,7 +5,7 @@
                 <div class="popup-title">{{ trans('entities.entity_select') }}</div>
                 <button refs="popup@hide" type="button" class="popup-header-close">x</button>
             </div>
-            @include('components.entity-selector', ['name' => 'entity-selector'])
+            @include('entities.selector', ['name' => 'entity-selector'])
             <div class="popup-footer">
                 <button refs="entity-selector-popup@select" type="button" disabled="true" class="button corner-button">{{ trans('common.select') }}</button>
             </div>
similarity index 94%
rename from resources/views/components/entity-selector.blade.php
rename to resources/views/entities/selector.blade.php
index c71fdff633ad3e8f7ab1c9d002b29e253b661398..687392deab25acf175604c3b4067e65d252f7c94 100644 (file)
@@ -5,7 +5,7 @@
          option:entity-selector:entity-permission="{{ $entityPermission ?? 'view' }}">
         <input refs="entity-selector@input" type="hidden" name="{{$name}}" value="">
         <input type="text" placeholder="{{ trans('common.search') }}" @if($autofocus ?? false) autofocus @endif refs="entity-selector@search">
-        <div class="text-center loading" refs="entity-selector@loading">@include('partials.loading-icon')</div>
+        <div class="text-center loading" refs="entity-selector@loading">@include('common.loading-icon')</div>
         <div refs="entity-selector@results"></div>
         @if($showAdd ?? false)
             <div class="entity-selector-add">
similarity index 84%
rename from resources/views/components/tag-manager.blade.php
rename to resources/views/entities/tag-manager.blade.php
index 9e24ba3fdcedadae6140c81122e75fb4fd33750c..5975c4bd97d9676a4fb2c12746a4b0fa0595682e 100644 (file)
@@ -9,7 +9,7 @@
 
         <div component="sortable-list"
              option:sortable-list:handle-selector=".handle">
-            @include('components.tag-manager-list', ['tags' => $entity ? $entity->tags->all() : []])
+            @include('entities.tag-manager-list', ['tags' => $entity ? $entity->tags->all() : []])
         </div>
 
         <button refs="add-remove-rows@add" type="button" class="text-button">{{ trans('entities.tags_add') }}</button>
index c4a5dc78276b27be5f6ac1470b73caeb1e8fa624..a4e5a0dd4c115c30ed1a0df46062109061f9f97d 100644 (file)
@@ -1,4 +1,4 @@
-@extends('simple-layout')
+@extends('layouts.simple')
 
 @section('content')
 <div class="container mt-l">
@@ -28,7 +28,7 @@
                 <div class="card mb-xl">
                     <h3 class="card-title">{{ trans('entities.pages_popular') }}</h3>
                     <div class="px-m">
-                        @include('partials.entity-list', ['entities' => (new \BookStack\Entities\Queries\Popular)->run(10, 0, ['page']), 'style' => 'compact'])
+                        @include('entities.list', ['entities' => (new \BookStack\Entities\Queries\Popular)->run(10, 0, ['page']), 'style' => 'compact'])
                     </div>
                 </div>
             </div>
@@ -36,7 +36,7 @@
                 <div class="card mb-xl">
                     <h3 class="card-title">{{ trans('entities.books_popular') }}</h3>
                     <div class="px-m">
-                        @include('partials.entity-list', ['entities' => (new \BookStack\Entities\Queries\Popular)->run(10, 0, ['book']), 'style' => 'compact'])
+                        @include('entities.list', ['entities' => (new \BookStack\Entities\Queries\Popular)->run(10, 0, ['book']), 'style' => 'compact'])
                     </div>
                 </div>
             </div>
@@ -44,7 +44,7 @@
                 <div class="card mb-xl">
                     <h3 class="card-title">{{ trans('entities.chapters_popular') }}</h3>
                     <div class="px-m">
-                        @include('partials.entity-list', ['entities' => (new \BookStack\Entities\Queries\Popular)->run(10, 0, ['chapter']), 'style' => 'compact'])
+                        @include('entities.list', ['entities' => (new \BookStack\Entities\Queries\Popular)->run(10, 0, ['chapter']), 'style' => 'compact'])
                     </div>
                 </div>
             </div>
index ad759b49dc60bc18139a71022d5b99d42ef2083f..d7d58e4c4a0275feba0ed154d82d611fcf66622a 100644 (file)
@@ -1,4 +1,4 @@
-@extends('base')
+@extends('layouts.base')
 
 @section('content')
 
index 29364606b97d24f58cc977d7adc012fd6bb3a788..9f86bfdc6475d07e7fe7c8f6aed53e41d4c09bfa 100644 (file)
@@ -1,4 +1,4 @@
-@extends('simple-layout')
+@extends('layouts.simple')
 
 @section('content')
 
index 255b591aa9ff790ffef3158dedbd477c48101716..fb2d4e259bb691f77909d77a1e93571b63ed33d2 100644 (file)
@@ -4,7 +4,7 @@ $label
 $errors?
 $model?
 --}}
-@include('components.custom-checkbox', [
+@include('form.custom-checkbox', [
     'name' => $name,
     'label' => $label,
     'value' => 'true',
index d3e89cc447fa07953ac30599e27d9b625ec70c27..ed04bc04124c9c0302568b8b4135390ce1314289 100644 (file)
         <div>
             <div class="form-group">
                 <label for="owner">{{ trans('entities.permissions_owner') }}</label>
-                @include('components.user-select', ['user' => $model->ownedBy, 'name' => 'owned_by', 'compact' => false])
+                @include('form.user-select', ['user' => $model->ownedBy, 'name' => 'owned_by', 'compact' => false])
             </div>
         </div>
     </div>
 
+    @if($model instanceof \BookStack\Entities\Models\Bookshelf)
+        <p class="text-warn">{{ trans('entities.shelves_permissions_cascade_warning') }}</p>
+    @endif
+
     <hr>
 
     <table permissions-table class="table permissions-table toggle-switch-list" style="{{ !$model->restricted ? 'display: none' : '' }}">
index 65a94239e78d83d5b9faadd8b7eafcaa627aaac3..02c477f4af2b4aa5561fba622eff78993dd7c0a9 100644 (file)
@@ -5,7 +5,7 @@ $role
 $action
 $model?
 --}}
-@include('components.custom-checkbox', [
+@include('form.custom-checkbox', [
     'name' => $name . '[' . $role->id . '][' . $action . ']',
     'label' => $label,
     'value' => 'true',
index fc6ad93a8d13284680fd7fc5b267c2ce6453d89d..7e5ca629a8598a4e9cb5b8603e53d08e0d6c9807 100644 (file)
@@ -2,7 +2,7 @@
 <div class="toggle-switch-list dual-column-content">
     @foreach($roles as $role)
         <div>
-            @include('components.custom-checkbox', [
+            @include('form.custom-checkbox', [
                 'name' => $name . '[' . strval($role->id) . ']',
                 'label' => $role->display_name,
                 'value' => $role->id,
similarity index 96%
rename from resources/views/components/user-select.blade.php
rename to resources/views/form/user-select.blade.php
index 50c731efd6e00352f0c388c112fabc516aadfa53..8823bb0750b5fdb014cae14bf0dcf1ca630ce46f 100644 (file)
@@ -27,7 +27,7 @@
                    type="text">
         </div>
         <div refs="dropdown-search@loading" class="text-center">
-            @include('partials.loading-icon')
+            @include('common.loading-icon')
         </div>
         <div refs="dropdown-search@listContainer" class="dropdown-search-list"></div>
     </div>
similarity index 63%
rename from resources/views/common/home-book.blade.php
rename to resources/views/home/books.blade.php
index 9f62d21e7b1c4e1f708e5fc7230f00faf765c8e3..75d4ae14a9daa6b58d67334d73e5327eed11ccbe 100644 (file)
@@ -1,11 +1,11 @@
-@extends('tri-layout')
+@extends('layouts.tri')
 
 @section('body')
-    @include('books.list', ['books' => $books, 'view' => $view])
+    @include('books.parts.list', ['books' => $books, 'view' => $view])
 @stop
 
 @section('left')
-    @include('common.home-sidebar')
+    @include('home.parts.sidebar')
 @stop
 
 @section('right')
@@ -18,9 +18,9 @@
                     <span>{{ trans('entities.books_create') }}</span>
                 </a>
             @endif
-            @include('partials.view-toggle', ['view' => $view, 'type' => 'books'])
-            @include('components.expand-toggle', ['classes' => 'text-primary', 'target' => '.entity-list.compact .entity-item-snippet', 'key' => 'home-details'])
-            @include('partials.dark-mode-toggle', ['classes' => 'icon-list-item text-primary'])
+            @include('entities.view-toggle', ['view' => $view, 'type' => 'books'])
+            @include('home.parts.expand-toggle', ['classes' => 'text-primary', 'target' => '.entity-list.compact .entity-item-snippet', 'key' => 'home-details'])
+            @include('common.dark-mode-toggle', ['classes' => 'icon-list-item text-primary'])
         </div>
     </div>
 @stop
similarity index 83%
rename from resources/views/common/home.blade.php
rename to resources/views/home/default.blade.php
index 187f222b50f1b7b524f9b899d90dd7fb74fade0a..b8866526d3d21521f38f4024e3014316a38e98c2 100644 (file)
@@ -1,4 +1,4 @@
-@extends('simple-layout')
+@extends('layouts.simple')
 
 @section('body')
 
@@ -6,12 +6,12 @@
         <div class="grid half">
             <div>
                 <div class="icon-list inline block">
-                    @include('components.expand-toggle', ['classes' => 'text-muted text-primary', 'target' => '.entity-list.compact .entity-item-snippet', 'key' => 'home-details'])
+                    @include('home.parts.expand-toggle', ['classes' => 'text-muted text-primary', 'target' => '.entity-list.compact .entity-item-snippet', 'key' => 'home-details'])
                 </div>
             </div>
             <div class="text-m-right">
                 <div class="icon-list inline block">
-                    @include('partials.dark-mode-toggle', ['classes' => 'text-muted icon-list-item text-primary'])
+                    @include('common.dark-mode-toggle', ['classes' => 'text-muted icon-list-item text-primary'])
                 </div>
             </div>
         </div>
@@ -24,7 +24,7 @@
                     <div id="recent-drafts" class="card mb-xl">
                         <h3 class="card-title">{{ trans('entities.my_recent_drafts') }}</h3>
                         <div class="px-m">
-                            @include('partials.entity-list', ['entities' => $draftPages, 'style' => 'compact'])
+                            @include('entities.list', ['entities' => $draftPages, 'style' => 'compact'])
                         </div>
                     </div>
                 @endif
@@ -32,7 +32,7 @@
                 <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', [
+                        @include('entities.list', [
                         'entities' => $recents,
                         'style' => 'compact',
                         'emptyText' => auth()->check() ? trans('entities.no_pages_viewed') : trans('entities.books_empty')
@@ -48,7 +48,7 @@
                             <a href="{{ url('/favourites') }}" class="no-color">{{ trans('entities.my_most_viewed_favourites') }}</a>
                         </h3>
                         <div class="px-m">
-                            @include('partials.entity-list', [
+                            @include('entities.list', [
                             'entities' => $favourites,
                             'style' => 'compact',
                             ])
@@ -59,7 +59,7 @@
                 <div id="recent-pages" class="card mb-xl">
                     <h3 class="card-title"><a class="no-color" href="{{ url("/pages/recently-updated") }}">{{ trans('entities.recently_updated_pages') }}</a></h3>
                     <div id="recently-updated-pages" class="px-m">
-                        @include('partials.entity-list', [
+                        @include('entities.list', [
                         'entities' => $recentlyUpdatedPages,
                         'style' => 'compact',
                         'emptyText' => trans('entities.no_pages_recently_updated')
@@ -72,7 +72,7 @@
                 <div id="recent-activity">
                     <div class="card mb-xl">
                         <h3 class="card-title">{{ trans('entities.recent_activity') }}</h3>
-                        @include('partials.activity-list', ['activity' => $activity])
+                        @include('common.activity-list', ['activity' => $activity])
                     </div>
                 </div>
             </div>
similarity index 80%
rename from resources/views/common/home-sidebar.blade.php
rename to resources/views/home/parts/sidebar.blade.php
index 7fe0659f0bdb5e42d824e6a981cb0f9d033f21bc..8dc8118f57f6cf4c12a0e8c11d41fe53b94014b0 100644 (file)
@@ -1,7 +1,7 @@
 @if(count($draftPages) > 0)
     <div id="recent-drafts" class="mb-xl">
         <h5>{{ trans('entities.my_recent_drafts') }}</h5>
-        @include('partials.entity-list', ['entities' => $draftPages, 'style' => 'compact'])
+        @include('entities.list', ['entities' => $draftPages, 'style' => 'compact'])
     </div>
 @endif
 
@@ -10,7 +10,7 @@
         <h5>
             <a href="{{ url('/favourites') }}" class="no-color">{{ trans('entities.my_most_viewed_favourites') }}</a>
         </h5>
-        @include('partials.entity-list', [
+        @include('entities.list', [
             'entities' => $favourites,
             'style' => 'compact',
         ])
@@ -19,7 +19,7 @@
 
 <div class="mb-xl">
     <h5>{{ trans('entities.' . (auth()->check() ? 'my_recently_viewed' : 'books_recent')) }}</h5>
-    @include('partials.entity-list', [
+    @include('entities.list', [
         'entities' => $recents,
         'style' => 'compact',
         'emptyText' => auth()->check() ? trans('entities.no_pages_viewed') : trans('entities.books_empty')
@@ -29,7 +29,7 @@
 <div class="mb-xl">
     <h5><a class="no-color" href="{{ url("/pages/recently-updated") }}">{{ trans('entities.recently_updated_pages') }}</a></h5>
     <div id="recently-updated-pages">
-        @include('partials.entity-list', [
+        @include('entities.list', [
         'entities' => $recentlyUpdatedPages,
         'style' => 'compact',
         'emptyText' => trans('entities.no_pages_recently_updated')
@@ -39,5 +39,5 @@
 
 <div id="recent-activity" class="mb-xl">
     <h5>{{ trans('entities.recent_activity') }}</h5>
-    @include('partials.activity-list', ['activity' => $activity])
+    @include('common.activity-list', ['activity' => $activity])
 </div>
\ No newline at end of file
similarity index 63%
rename from resources/views/common/home-shelves.blade.php
rename to resources/views/home/shelves.blade.php
index 3c32fed740f7c70cb75f0da56b1eb8842eea6cff..c525643b9c0fcb568f842067465d8e2a1061c174 100644 (file)
@@ -1,11 +1,11 @@
-@extends('tri-layout')
+@extends('layouts.tri')
 
 @section('body')
-    @include('shelves.list', ['shelves' => $shelves, 'view' => $view])
+    @include('shelves.parts.list', ['shelves' => $shelves, 'view' => $view])
 @stop
 
 @section('left')
-    @include('common.home-sidebar')
+    @include('home.parts.sidebar')
 @stop
 
 @section('right')
@@ -18,9 +18,9 @@
                     <span>{{ trans('entities.shelves_new_action') }}</span>
                 </a>
             @endif
-            @include('partials.view-toggle', ['view' => $view, 'type' => 'shelves'])
-            @include('components.expand-toggle', ['classes' => 'text-primary', 'target' => '.entity-list.compact .entity-item-snippet', 'key' => 'home-details'])
-            @include('partials.dark-mode-toggle', ['classes' => 'icon-list-item text-primary'])
+            @include('entities.view-toggle', ['view' => $view, 'type' => 'shelves'])
+            @include('home.parts.expand-toggle', ['classes' => 'text-primary', 'target' => '.entity-list.compact .entity-item-snippet', 'key' => 'home-details'])
+            @include('common.dark-mode-toggle', ['classes' => 'icon-list-item text-primary'])
         </div>
     </div>
 @stop
similarity index 62%
rename from resources/views/common/home-custom.blade.php
rename to resources/views/home/specific-page.blade.php
index a22e2600228ceac320eb57b78fb965d52a159d4a..936433b276adb445df7a3713396b645d7728d04f 100644 (file)
@@ -1,25 +1,25 @@
-@extends('tri-layout')
+@extends('layouts.tri')
 
 @section('body')
     <div class="mt-m">
         <main class="content-wrap card">
             <div class="page-content" page-display="{{ $customHomepage->id }}">
-                @include('pages.page-display', ['page' => $customHomepage])
+                @include('pages.parts.page-display', ['page' => $customHomepage])
             </div>
         </main>
     </div>
 @stop
 
 @section('left')
-    @include('common.home-sidebar')
+    @include('home.parts.sidebar')
 @stop
 
 @section('right')
     <div class="actions mb-xl">
         <h5>{{ trans('common.actions') }}</h5>
         <div class="icon-list text-primary">
-            @include('components.expand-toggle', ['classes' => 'text-primary', 'target' => '.entity-list.compact .entity-item-snippet', 'key' => 'home-details'])
-            @include('partials.dark-mode-toggle', ['classes' => 'icon-list-item text-primary'])
+            @include('home.parts.expand-toggle', ['classes' => 'text-primary', 'target' => '.entity-list.compact .entity-item-snippet', 'key' => 'home-details'])
+            @include('common.dark-mode-toggle', ['classes' => 'icon-list-item text-primary'])
         </div>
     </div>
 @stop
\ No newline at end of file
similarity index 87%
rename from resources/views/base.blade.php
rename to resources/views/layouts/base.blade.php
index dc665a888046ddb8d05d46440f5600c119a5a692..1f28e354ce8e119f03a0281fde89563eaee371d5 100644 (file)
@@ -15,7 +15,6 @@
     <meta property="og:title" content="{{ isset($pageTitle) ? $pageTitle . ' | ' : '' }}{{ setting('app-name') }}">
     <meta property="og:url" content="{{ url()->current() }}">
     @stack('social-meta')
-    
 
     <!-- Styles and Fonts -->
     <link rel="stylesheet" href="{{ versioned_asset('dist/styles.css') }}">
@@ -24,8 +23,8 @@
     @yield('head')
 
     <!-- Custom Styles & Head Content -->
-    @include('partials.custom-styles')
-    @include('partials.custom-head')
+    @include('common.custom-styles')
+    @include('common.custom-head')
 
     @stack('head')
 
@@ -34,8 +33,8 @@
 </head>
 <body class="@yield('body-class')">
 
-    @include('common.parts.skip-to-content')
-    @include('partials.notifications')
+    @include('common.skip-to-content')
+    @include('common.notifications')
     @include('common.header')
 
     <div id="content" components="@yield('content-components')" class="block">
@@ -51,7 +50,7 @@
     </div>
 
     @yield('bottom')
-    <script src="{{ versioned_asset('dist/app.js') }}"></script>
+    <script src="{{ versioned_asset('dist/app.js') }}" nonce="{{ $cspNonce }}"></script>
     @yield('scripts')
 
 </body>
similarity index 68%
rename from resources/views/export-layout.blade.php
rename to resources/views/layouts/export.blade.php
index f23b3cca517ec26a2714a46df8fa2e6e3635a0a2..55df43a45a20e0f5474b43691541569b1d4dc957 100644 (file)
@@ -4,8 +4,8 @@
     <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
     <title>@yield('title')</title>
 
-    @include('partials.export-styles', ['format' => $format])
-    @include('partials.export-custom-head')
+    @include('common.export-styles', ['format' => $format])
+    @include('common.export-custom-head')
 </head>
 <body>
 <div class="page-content">
similarity index 90%
rename from resources/views/simple-layout.blade.php
rename to resources/views/layouts/simple.blade.php
index b7d6d3ccddc9f88e4f524ebfbe2569be1c7e3416..5fb231bdb00f0201d09f9c8d75d50ee40cb03d7b 100644 (file)
@@ -1,4 +1,4 @@
-@extends('base')
+@extends('layouts.base')
 
 @section('content')
 
similarity index 98%
rename from resources/views/tri-layout.blade.php
rename to resources/views/layouts/tri.blade.php
index d985db6499d03f003b23d4b16c4e850966706fe5..e95b21445295e851368ead241ef92d08353a6cdc 100644 (file)
@@ -1,4 +1,4 @@
-@extends('base')
+@extends('layouts.base')
 
 @section('body-class', 'tri-layout')
 @section('content-components', 'tri-layout')
diff --git a/resources/views/mfa/backup-codes-generate.blade.php b/resources/views/mfa/backup-codes-generate.blade.php
new file mode 100644 (file)
index 0000000..27ab031
--- /dev/null
@@ -0,0 +1,36 @@
+@extends('layouts.simple')
+
+@section('body')
+
+    <div class="container very-small py-xl">
+        <div class="card content-wrap auto-height">
+            <h1 class="list-heading">{{ trans('auth.mfa_gen_backup_codes_title') }}</h1>
+            <p>{{ trans('auth.mfa_gen_backup_codes_desc') }}</p>
+
+            <div class="text-center mb-xs">
+                <div class="text-bigger code-base p-m" style="column-count: 2">
+                    @foreach($codes as $code)
+                        {{ $code }} <br>
+                    @endforeach
+                </div>
+            </div>
+
+            <p class="text-right">
+                <a href="{{ $downloadUrl }}" download="backup-codes.txt" class="button outline small">{{ trans('auth.mfa_gen_backup_codes_download') }}</a>
+            </p>
+
+            <p class="callout warning">
+                {{ trans('auth.mfa_gen_backup_codes_usage_warning') }}
+            </p>
+
+            <form action="{{ url('/mfa/backup_codes/confirm') }}" method="POST">
+                {{ csrf_field() }}
+                <div class="mt-s text-right">
+                    <a href="{{ url('/mfa/setup') }}" class="button outline">{{ trans('common.cancel') }}</a>
+                    <button class="button">{{ trans('auth.mfa_gen_confirm_and_enable') }}</button>
+                </div>
+            </form>
+        </div>
+    </div>
+
+@stop
diff --git a/resources/views/mfa/parts/setup-method-row.blade.php b/resources/views/mfa/parts/setup-method-row.blade.php
new file mode 100644 (file)
index 0000000..e195174
--- /dev/null
@@ -0,0 +1,30 @@
+<div class="grid half gap-xl">
+    <div>
+        <div class="setting-list-label">{{ trans('auth.mfa_option_' . $method . '_title') }}</div>
+        <p class="small">
+            {{ trans('auth.mfa_option_' . $method . '_desc') }}
+        </p>
+    </div>
+    <div class="pt-m">
+        @if($userMethods->has($method))
+            <div class="text-pos">
+                @icon('check-circle')
+                {{ trans('auth.mfa_setup_configured') }}
+            </div>
+            <a href="{{ url('/mfa/' . $method . '/generate') }}" class="button outline small">{{ trans('auth.mfa_setup_reconfigure') }}</a>
+            <div component="dropdown" class="inline relative">
+                <button type="button" refs="dropdown@toggle" class="button outline small">{{ trans('common.remove') }}</button>
+                <div refs="dropdown@menu" class="dropdown-menu">
+                    <p class="text-neg small px-m mb-xs">{{ trans('auth.mfa_setup_remove_confirmation') }}</p>
+                    <form action="{{ url('/mfa/' . $method . '/remove') }}" method="post">
+                        {{ csrf_field() }}
+                        {{ method_field('delete') }}
+                        <button class="text-primary small delete">{{ trans('common.confirm') }}</button>
+                    </form>
+                </div>
+            </div>
+        @else
+            <a href="{{ url('/mfa/' . $method . '/generate') }}" class="button outline">{{ trans('auth.mfa_setup_action') }}</a>
+        @endif
+    </div>
+</div>
\ No newline at end of file
diff --git a/resources/views/mfa/parts/verify-backup_codes.blade.php b/resources/views/mfa/parts/verify-backup_codes.blade.php
new file mode 100644 (file)
index 0000000..0e5b820
--- /dev/null
@@ -0,0 +1,17 @@
+<div class="setting-list-label">{{ trans('auth.mfa_verify_backup_code') }}</div>
+
+<p class="small mb-m">{{ trans('auth.mfa_verify_backup_code_desc') }}</p>
+
+<form action="{{ url('/mfa/backup_codes/verify') }}" method="post">
+    {{ csrf_field() }}
+    <input type="text"
+           name="code"
+           placeholder="{{ trans('auth.mfa_verify_backup_code_enter_here') }}"
+           class="input-fill-width {{ $errors->has('code') ? 'neg' : '' }}">
+    @if($errors->has('code'))
+        <div class="text-neg text-small px-xs">{{ $errors->first('code') }}</div>
+    @endif
+    <div class="mt-s text-right">
+        <button class="button">{{ trans('common.confirm') }}</button>
+    </div>
+</form>
\ No newline at end of file
diff --git a/resources/views/mfa/parts/verify-totp.blade.php b/resources/views/mfa/parts/verify-totp.blade.php
new file mode 100644 (file)
index 0000000..9a861fc
--- /dev/null
@@ -0,0 +1,17 @@
+<div class="setting-list-label">{{ trans('auth.mfa_option_totp_title') }}</div>
+
+<p class="small mb-m">{{ trans('auth.mfa_verify_totp_desc') }}</p>
+
+<form action="{{ url('/mfa/totp/verify') }}" method="post">
+    {{ csrf_field() }}
+    <input type="text"
+           name="code"
+           placeholder="{{ trans('auth.mfa_gen_totp_provide_code_here') }}"
+           class="input-fill-width {{ $errors->has('code') ? 'neg' : '' }}">
+    @if($errors->has('code'))
+        <div class="text-neg text-small px-xs">{{ $errors->first('code') }}</div>
+    @endif
+    <div class="mt-s text-right">
+        <button class="button">{{ trans('common.confirm') }}</button>
+    </div>
+</form>
\ No newline at end of file
diff --git a/resources/views/mfa/setup.blade.php b/resources/views/mfa/setup.blade.php
new file mode 100644 (file)
index 0000000..702f007
--- /dev/null
@@ -0,0 +1,18 @@
+@extends('layouts.simple')
+
+@section('body')
+    <div class="container small py-xl">
+
+        <div class="card content-wrap auto-height">
+            <h1 class="list-heading">{{ trans('auth.mfa_setup') }}</h1>
+            <p class="mb-none"> {{ trans('auth.mfa_setup_desc') }}</p>
+
+            <div class="setting-list">
+                @foreach(['totp', 'backup_codes'] as $method)
+                    @include('mfa.parts.setup-method-row', ['method' => $method])
+                @endforeach
+            </div>
+
+        </div>
+    </div>
+@stop
diff --git a/resources/views/mfa/totp-generate.blade.php b/resources/views/mfa/totp-generate.blade.php
new file mode 100644 (file)
index 0000000..e99861a
--- /dev/null
@@ -0,0 +1,40 @@
+@extends('layouts.simple')
+
+@section('body')
+
+    <div class="container very-small py-xl">
+        <div class="card content-wrap auto-height">
+            <h1 class="list-heading">{{ trans('auth.mfa_gen_totp_title') }}</h1>
+            <p>{{ trans('auth.mfa_gen_totp_desc') }}</p>
+            <p>{{ trans('auth.mfa_gen_totp_scan') }}</p>
+
+            <div class="text-center">
+                <div class="block inline">
+                    {!! $svg !!}
+                </div>
+                <div class="code-base small text-muted px-s py-xs my-xs" style="overflow-x: scroll; white-space: nowrap;">
+                    {{ $url }}
+                </div>
+            </div>
+
+            <h2 class="list-heading">{{ trans('auth.mfa_gen_totp_verify_setup') }}</h2>
+            <p id="totp-verify-input-details" class="mb-s">{{ trans('auth.mfa_gen_totp_verify_setup_desc') }}</p>
+            <form action="{{ url('/mfa/totp/confirm') }}" method="POST">
+                {{ csrf_field() }}
+                <input type="text"
+                       name="code"
+                       aria-labelledby="totp-verify-input-details"
+                       placeholder="{{ trans('auth.mfa_gen_totp_provide_code_here') }}"
+                       class="input-fill-width {{ $errors->has('code') ? 'neg' : '' }}">
+                @if($errors->has('code'))
+                    <div class="text-neg text-small px-xs">{{ $errors->first('code') }}</div>
+                @endif
+                <div class="mt-s text-right">
+                    <a href="{{ url('/mfa/setup') }}" class="button outline">{{ trans('common.cancel') }}</a>
+                    <button class="button">{{ trans('auth.mfa_gen_confirm_and_enable') }}</button>
+                </div>
+            </form>
+        </div>
+    </div>
+
+@stop
diff --git a/resources/views/mfa/verify.blade.php b/resources/views/mfa/verify.blade.php
new file mode 100644 (file)
index 0000000..3cadeac
--- /dev/null
@@ -0,0 +1,35 @@
+@extends('layouts.simple')
+
+@section('body')
+    <div class="container very-small py-xl">
+
+        <div class="card content-wrap auto-height">
+            <h1 class="list-heading">{{ trans('auth.mfa_verify_access') }}</h1>
+            <p class="mb-none">{{ trans('auth.mfa_verify_access_desc') }}</p>
+
+            @if(!$method)
+                <hr class="my-l">
+                <h5>{{ trans('auth.mfa_verify_no_methods') }}</h5>
+                <p class="small">{{ trans('auth.mfa_verify_no_methods_desc') }}</p>
+                <div>
+                    <a href="{{ url('/mfa/setup') }}" class="button outline">{{ trans('common.configure') }}</a>
+                </div>
+            @endif
+
+            @if($method)
+                <hr class="my-l">
+                @include('mfa.parts.verify-' . $method)
+            @endif
+
+            @if(count($otherMethods) > 0)
+                <hr class="my-l">
+                @foreach($otherMethods as $otherMethod)
+                    <div class="text-center">
+                        <a href="{{ url("/mfa/verify?method={$otherMethod}") }}">{{ trans('auth.mfa_verify_use_' . $otherMethod) }}</a>
+                    </div>
+                @endforeach
+            @endif
+
+        </div>
+    </div>
+@stop
index 0f2af0476e17143b5f8a48df42fccd75d0892f2a..2f24d8165413c9e3e3fcd6586bd1dc82965af23e 100644 (file)
@@ -1,11 +1,11 @@
-@extends('simple-layout')
+@extends('layouts.simple')
 
 @section('body')
 
     <div class="container small">
 
         <div class="my-s">
-            @include('partials.breadcrumbs', ['crumbs' => [
+            @include('entities.breadcrumbs', ['crumbs' => [
                 $page->book,
                 $page->chapter,
                 $page,
@@ -33,7 +33,7 @@
                         <label for="entity_selection">{{ trans('entities.pages_copy_desination') }}</label>
                     </button>
                     <div class="collapse-content" collapsible-content>
-                        @include('components.entity-selector', ['name' => 'entity_selection', 'selectorSize' => 'large', 'entityTypes' => 'book,chapter', 'entityPermission' => 'page-create'])
+                        @include('entities.selector', ['name' => 'entity_selection', 'selectorSize' => 'large', 'entityTypes' => 'book,chapter', 'entityPermission' => 'page-create'])
                     </div>
                 </div>
 
index 2ec046fa03bf1bf39b4a89ff45bfefa8ebf58be6..39cd07bbb1c227070008cce0f0df5b776b006177 100644 (file)
@@ -1,11 +1,11 @@
-@extends('simple-layout')
+@extends('layouts.simple')
 
 @section('body')
 
     <div class="container small">
 
         <div class="my-s">
-            @include('partials.breadcrumbs', ['crumbs' => [
+            @include('entities.breadcrumbs', ['crumbs' => [
                 $page->book,
                 $page->chapter,
                 $page,
index 2120bddb233a7d457bb7fd83d60dbf5449c8c0dd..6d2c3d484d43574ede66250a58347cef8c3f2692 100644 (file)
@@ -1,7 +1,7 @@
-@extends('base')
+@extends('layouts.base')
 
 @section('head')
-    <script src="{{ url('/libs/tinymce/tinymce.min.js?ver=4.9.4') }}"></script>
+    <script src="{{ url('/libs/tinymce/tinymce.min.js?ver=4.9.4') }}" nonce="{{ $cspNonce }}"></script>
 @stop
 
 @section('body-class', 'flexbox')
             @if(!isset($isDraft))
                 <input type="hidden" name="_method" value="PUT">
             @endif
-            @include('pages.form', ['model' => $page])
-            @include('pages.editor-toolbox')
+            @include('pages.parts.form', ['model' => $page])
+            @include('pages.parts.editor-toolbox')
         </form>
     </div>
     
-    @include('components.image-manager', ['uploaded_to' => $page->id])
-    @include('components.code-editor')
-    @include('components.entity-selector-popup')
+    @include('pages.parts.image-manager', ['uploaded_to' => $page->id])
+    @include('pages.parts.code-editor')
+    @include('entities.selector-popup')
 @stop
\ No newline at end of file
index 74d17c128f794be4c7857c6eb584c07211a3a534..d2f448d6e889bd430f3ce53852530d8411c2945c 100644 (file)
@@ -1,13 +1,13 @@
-@extends('export-layout')
+@extends('layouts.export')
 
 @section('title', $page->name)
 
 @section('content')
-    @include('pages.page-display')
+    @include('pages.parts.page-display')
 
     <hr>
 
     <div class="text-muted text-small">
-        @include('partials.entity-export-meta', ['entity' => $page])
+        @include('entities.export-meta', ['entity' => $page])
     </div>
 @endsection
\ No newline at end of file
index 55db85144ae1f9ba147b4da17268c78cfc2589ec..d6e1cae446aeb729228b537595067eca9c8fcb13 100644 (file)
@@ -1,11 +1,11 @@
-@extends('simple-layout')
+@extends('layouts.simple')
 
 @section('body')
 
     <div class="container small">
 
         <div class="my-s">
-            @include('partials.breadcrumbs', ['crumbs' => [
+            @include('entities.breadcrumbs', ['crumbs' => [
                 ($parent->isA('chapter') ? $parent->book : null),
                 $parent,
                 $parent->getUrl('/create-page') => [
index 26b872cdd769952aa843232daf17d68f32d591b3..6df36496a5d585cba4dc7871a2447e637660ef5f 100644 (file)
@@ -1,11 +1,11 @@
-@extends('simple-layout')
+@extends('layouts.simple')
 
 @section('body')
 
     <div class="container small">
 
         <div class="my-s">
-            @include('partials.breadcrumbs', ['crumbs' => [
+            @include('entities.breadcrumbs', ['crumbs' => [
                 $page->book,
                 $page->chapter,
                 $page,
@@ -23,7 +23,7 @@
                 {!! csrf_field() !!}
                 <input type="hidden" name="_method" value="PUT">
 
-                @include('components.entity-selector', ['name' => 'entity_selection', 'selectorSize' => 'large', 'entityTypes' => 'book,chapter', 'entityPermission' => 'page-create', 'autofocus' => true])
+                @include('entities.selector', ['name' => 'entity_selection', 'selectorSize' => 'large', 'entityTypes' => 'book,chapter', 'entityPermission' => 'page-create', 'autofocus' => true])
 
                 <div class="form-group text-right">
                     <a href="{{ $page->getUrl() }}" class="button outline">{{ trans('common.cancel') }}</a>
similarity index 86%
rename from resources/views/pages/editor-toolbox.blade.php
rename to resources/views/pages/parts/editor-toolbox.blade.php
index 87a9cc2de8bff90ba2036b0a312399cd41acbbb5..f3b54ddcd6eb0efb0978dea032555a54feeeab1d 100644 (file)
@@ -12,7 +12,7 @@
     <div toolbox-tab-content="tags">
         <h4>{{ trans('entities.page_tags') }}</h4>
         <div class="px-l">
-            @include('components.tag-manager', ['entity' => $page])
+            @include('entities.tag-manager', ['entity' => $page])
         </div>
     </div>
 
@@ -24,7 +24,7 @@
         <h4>{{ trans('entities.templates') }}</h4>
 
         <div class="px-l">
-            @include('pages.template-manager', ['page' => $page, 'templates' => $templates])
+            @include('pages.parts.template-manager', ['page' => $page, 'templates' => $templates])
         </div>
 
     </div>
similarity index 97%
rename from resources/views/pages/form.blade.php
rename to resources/views/pages/parts/form.blade.php
index 7e8b2fdd64409f18a72378c7e620af5f8a14691a..f6f0143da6f378d0ea03dabe50f864947069ed2c 100644 (file)
 
         {{--WYSIWYG Editor--}}
         @if(setting('app-editor') === 'wysiwyg')
-            @include('pages.wysiwyg-editor', ['model' => $model])
+            @include('pages.parts.wysiwyg-editor', ['model' => $model])
         @endif
 
         {{--Markdown Editor--}}
         @if(setting('app-editor') === 'markdown')
-            @include('pages.markdown-editor', ['model' => $model])
+            @include('pages.parts.markdown-editor', ['model' => $model])
         @endif
 
     </div>
similarity index 98%
rename from resources/views/components/image-manager.blade.php
rename to resources/views/pages/parts/image-manager.blade.php
index 4f03eeaec21c33634992c040bbb56014a6ada5ff..c15c31b86904bc1c7985cb20ac90663c6f053670 100644 (file)
@@ -45,7 +45,7 @@
                 <div class="image-manager-sidebar flex-container-column">
 
                     <div refs="image-manager@dropzoneContainer">
-                        @include('components.dropzone', [
+                        @include('form.dropzone', [
                             'placeholder' => trans('components.image_dropzone'),
                             'successMessage' => trans('components.image_upload_success'),
                             'url' => url('/images/gallery?' . http_build_query(['uploaded_to' => $uploaded_to ?? 0]))
similarity index 60%
rename from resources/views/pages/list-item.blade.php
rename to resources/views/pages/parts/list-item.blade.php
index 1e26cf1d546227566cd2798cd2f305ebbfab8148..5707a9c661933465a2937c2b99d1c433a6d0af66 100644 (file)
@@ -1,4 +1,4 @@
-@component('partials.entity-list-item-basic', ['entity' => $page])
+@component('entities.list-item-basic', ['entity' => $page])
     <div class="entity-item-snippet">
         <p class="text-muted break-text">{{ $page->getExcerpt() }}</p>
     </div>
similarity index 86%
rename from resources/views/pages/template-manager.blade.php
rename to resources/views/pages/parts/template-manager.blade.php
index fbdb70a1be604ad4fbf0638bf775630f5d65476b..66d53ae7e9fc29d845ef3158fd7933d8a3de0403 100644 (file)
@@ -3,7 +3,7 @@
         <p class="text-muted small mb-none">
             {{ trans('entities.templates_explain_set_as_template') }}
         </p>
-        @include('components.toggle-switch', [
+        @include('form.toggle-switch', [
                'name' => 'template',
                'value' => old('template', $page->template ? 'true' : 'false') === 'true',
                'label' => trans('entities.templates_set_as_template')
@@ -20,6 +20,6 @@
     @endif
 
     <div template-manager-list>
-        @include('pages.template-manager-list', ['templates' => $templates])
+        @include('pages.parts.template-manager-list', ['templates' => $templates])
     </div>
 </div>
\ No newline at end of file
index de28137dbe02952d52f5cb563d52d95e992074f2..792015e28bb3a405975e3f12b1888ba662c9eb4a 100644 (file)
@@ -1,11 +1,11 @@
-@extends('simple-layout')
+@extends('layouts.simple')
 
 @section('body')
 
     <div class="container">
 
         <div class="my-s">
-            @include('partials.breadcrumbs', ['crumbs' => [
+            @include('entities.breadcrumbs', ['crumbs' => [
                 $page->book,
                 $page->chapter,
                 $page,
index 0557b6b1cd79be45f57d51074bcbf32ab9753b86..b3208c21131ef67890167b8fd8ec724e5191b45e 100644 (file)
@@ -1,10 +1,10 @@
-@extends('tri-layout')
+@extends('layouts.tri')
 
 @section('left')
     <div id="revision-details" class="entity-details mb-xl">
         <h5>{{ trans('common.details') }}</h5>
         <div class="body text-small text-muted">
-            @include('partials.entity-meta', ['entity' => $revision])
+            @include('entities.meta', ['entity' => $revision])
         </div>
     </div>
 @stop
@@ -12,7 +12,7 @@
 @section('body')
 
     <div class="mb-m print-hidden">
-        @include('partials.breadcrumbs', ['crumbs' => [
+        @include('entities.breadcrumbs', ['crumbs' => [
             $page->$book,
             $page->chapter,
             $page,
@@ -27,7 +27,7 @@
 
     <main class="card content-wrap">
         <div class="page-content page-revision">
-            @include('pages.page-display')
+            @include('pages.parts.page-display')
         </div>
     </main>
 
index 6624620c5e0d46d3d6da4120d01e841e89939965..5508f362d3fab131afd4b70e4370394c974361ec 100644 (file)
@@ -1,10 +1,10 @@
-@extends('simple-layout')
+@extends('layouts.simple')
 
 @section('body')
     <div class="container">
 
         <div class="my-s">
-            @include('partials.breadcrumbs', ['crumbs' => [
+            @include('entities.breadcrumbs', ['crumbs' => [
                 $page->book,
                 $page->chapter,
                 $page,
index 012454e7c4b8abf1dc464ebad37f747471711beb..0111047c6cfec38fe0a2fea304226c7de3a6f114 100644 (file)
@@ -1,4 +1,4 @@
-@extends('tri-layout')
+@extends('layouts.tri')
 
 @push('social-meta')
     <meta property="og:description" content="{{ Str::limit($page->text, 100, '...') }}">
@@ -7,7 +7,7 @@
 @section('body')
 
     <div class="mb-m print-hidden">
-        @include('partials.breadcrumbs', ['crumbs' => [
+        @include('entities.breadcrumbs', ['crumbs' => [
             $page->book,
             $page->hasChapter() ? $page->chapter : null,
             $page,
 
     <main class="content-wrap card">
         <div class="page-content clearfix" page-display="{{ $page->id }}">
-            @include('pages.pointer', ['page' => $page])
-            @include('pages.page-display')
+            @include('pages.parts.pointer', ['page' => $page])
+            @include('pages.parts.page-display')
         </div>
     </main>
 
-    @include('partials.entity-sibling-navigation', ['next' => $next, 'previous' => $previous])
+    @include('entities.sibling-navigation', ['next' => $next, 'previous' => $previous])
 
     @if ($commentsEnabled)
         @if(($previous || $next))
@@ -41,7 +41,7 @@
 
     @if($page->tags->count() > 0)
         <section>
-            @include('components.tag-list', ['entity' => $page])
+            @include('entities.tag-list', ['entity' => $page])
         </section>
     @endif
 
         </nav>
     @endif
 
-    @include('partials.book-tree', ['book' => $book, 'sidebarTree' => $sidebarTree])
+    @include('entities.book-tree', ['book' => $book, 'sidebarTree' => $sidebarTree])
 @stop
 
 @section('right')
     <div id="page-details" class="entity-details mb-xl">
         <h5>{{ trans('common.details') }}</h5>
         <div class="body text-small blended-links">
-            @include('partials.entity-meta', ['entity' => $page])
+            @include('entities.meta', ['entity' => $page])
 
             @if($book->restricted)
                 <div class="active-restriction">
             <hr class="primary-background"/>
 
             @if(signedInUser())
-                @include('partials.entity-favourite-action', ['entity' => $page])
+                @include('entities.favourite-action', ['entity' => $page])
+            @endif
+            @if(userCan('content-export'))
+                @include('entities.export-menu', ['entity' => $page])
             @endif
-            @include('partials.entity-export-menu', ['entity' => $page])
         </div>
 
     </div>
diff --git a/resources/views/pages/sidebar-tree-list.blade.php b/resources/views/pages/sidebar-tree-list.blade.php
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/resources/views/partials/export-custom-head.blade.php b/resources/views/partials/export-custom-head.blade.php
deleted file mode 100644 (file)
index f428e9f..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-@if(setting('app-custom-head'))
-<!-- Custom user content -->
-{!! \BookStack\Util\HtmlContentFilter::removeScripts(setting('app-custom-head')) !!}
-<!-- End custom user content -->
-@endif
\ No newline at end of file
diff --git a/resources/views/readme.md b/resources/views/readme.md
new file mode 100644 (file)
index 0000000..4646db2
--- /dev/null
@@ -0,0 +1,43 @@
+# BookStack Views
+
+All views within this folder are [Laravel blade](https://laravel.com/docs/6.x/blade) views.
+
+### Overriding
+
+Views can be overridden on a per-file basis via the visual theme system.
+More information on this can be found within the `dev/docs/visual-theme-system.md`
+file within this project.
+
+### Convention
+
+Views are broken down into rough domain areas. These aren't too strict although many of the folders
+here will often match up to a HTTP controller. 
+
+Within each folder views will be structured like so:
+
+```txt
+- folder/
+    - page-a.blade.php
+    - page-b.blade.php
+    - parts/
+        - partial-a.blade.php
+        - partial-b.blade.php
+    - subdomain/
+        - subdomain-page-a.blade.php
+        - subdomain-page-b.blade.php
+        - parts/
+            - subdomain-partial-a.blade.php
+            - subdomain-partial-b.blade.php
+```
+
+If a folder contains no pages at all (For example: `attachments`, `form`) and only partials, then 
+the partials can be within the top-level folder instead of pages to prevent unneeded nesting.
+
+If a partial depends on another partial within the same directory, the naming of the child partials should be an extension of the parent.
+For example:
+
+```txt
+- tag-manager.blade.php
+- tag-manager-list.blade.php
+- tag-manager-input.blade.php
+```
\ No newline at end of file
index 49a6c1f653dcaed97e257df2aa15a4314ffd93c3..b9adccca79b893d32abc6e6af090335a4681036f 100644 (file)
@@ -1,4 +1,4 @@
-@extends('simple-layout')
+@extends('layouts.simple')
 
 @section('body')
     <div class="container mt-xl" id="search-system">
                             $types = explode('|', $options->filters['type'] ?? '');
                             $hasTypes = $types[0] !== '';
                             ?>
-                            @include('search.form.type-filter', ['checked' => !$hasTypes || in_array('page', $types), 'entity' => 'page', 'transKey' => 'page'])
-                            @include('search.form.type-filter', ['checked' => !$hasTypes || in_array('chapter', $types), 'entity' => 'chapter', 'transKey' => 'chapter'])
+                            @include('search.parts.type-filter', ['checked' => !$hasTypes || in_array('page', $types), 'entity' => 'page', 'transKey' => 'page'])
+                            @include('search.parts.type-filter', ['checked' => !$hasTypes || in_array('chapter', $types), 'entity' => 'chapter', 'transKey' => 'chapter'])
                             <br>
-                                @include('search.form.type-filter', ['checked' => !$hasTypes || in_array('book', $types), 'entity' => 'book', 'transKey' => 'book'])
-                                @include('search.form.type-filter', ['checked' => !$hasTypes || in_array('bookshelf', $types), 'entity' => 'bookshelf', 'transKey' => 'shelf'])
+                                @include('search.parts.type-filter', ['checked' => !$hasTypes || in_array('book', $types), 'entity' => 'book', 'transKey' => 'book'])
+                                @include('search.parts.type-filter', ['checked' => !$hasTypes || in_array('bookshelf', $types), 'entity' => 'bookshelf', 'transKey' => 'shelf'])
                         </div>
 
                         <h6>{{ trans('entities.search_exact_matches') }}</h6>
-                        @include('search.form.term-list', ['type' => 'exact', 'currentList' => $options->exacts])
+                        @include('search.parts.term-list', ['type' => 'exact', 'currentList' => $options->exacts])
 
                         <h6>{{ trans('entities.search_tags') }}</h6>
-                        @include('search.form.term-list', ['type' => 'tags', 'currentList' => $options->tags])
+                        @include('search.parts.term-list', ['type' => 'tags', 'currentList' => $options->tags])
 
                         @if(signedInUser())
                             <h6>{{ trans('entities.search_options') }}</h6>
 
-                            @component('search.form.boolean-filter', ['filters' => $options->filters, 'name' => 'viewed_by_me', 'value' => null])
+                            @component('search.parts.boolean-filter', ['filters' => $options->filters, 'name' => 'viewed_by_me', 'value' => null])
                                 {{ trans('entities.search_viewed_by_me') }}
                             @endcomponent
-                            @component('search.form.boolean-filter', ['filters' => $options->filters, 'name' => 'not_viewed_by_me', 'value' => null])
+                            @component('search.parts.boolean-filter', ['filters' => $options->filters, 'name' => 'not_viewed_by_me', 'value' => null])
                                 {{ trans('entities.search_not_viewed_by_me') }}
                             @endcomponent
-                            @component('search.form.boolean-filter', ['filters' => $options->filters, 'name' => 'is_restricted', 'value' => null])
+                            @component('search.parts.boolean-filter', ['filters' => $options->filters, 'name' => 'is_restricted', 'value' => null])
                                 {{ trans('entities.search_permissions_set') }}
                             @endcomponent
-                            @component('search.form.boolean-filter', ['filters' => $options->filters, 'name' => 'created_by', 'value' => 'me'])
+                            @component('search.parts.boolean-filter', ['filters' => $options->filters, 'name' => 'created_by', 'value' => 'me'])
                                 {{ trans('entities.search_created_by_me') }}
                             @endcomponent
-                            @component('search.form.boolean-filter', ['filters' => $options->filters, 'name' => 'updated_by', 'value' => 'me'])
+                            @component('search.parts.boolean-filter', ['filters' => $options->filters, 'name' => 'updated_by', 'value' => 'me'])
                                 {{ trans('entities.search_updated_by_me') }}
                             @endcomponent
-                            @component('search.form.boolean-filter', ['filters' => $options->filters, 'name' => 'owned_by', 'value' => 'me'])
+                            @component('search.parts.boolean-filter', ['filters' => $options->filters, 'name' => 'owned_by', 'value' => 'me'])
                                 {{ trans('entities.search_owned_by_me') }}
                             @endcomponent
                         @endif
 
                         <h6>{{ trans('entities.search_date_options') }}</h6>
-                        @include('search.form.date-filter', ['name' => 'updated_after', 'filters' => $options->filters])
-                        @include('search.form.date-filter', ['name' => 'updated_before', 'filters' => $options->filters])
-                        @include('search.form.date-filter', ['name' => 'created_after', 'filters' => $options->filters])
-                        @include('search.form.date-filter', ['name' => 'created_before', 'filters' => $options->filters])
+                        @include('search.parts.date-filter', ['name' => 'updated_after', 'filters' => $options->filters])
+                        @include('search.parts.date-filter', ['name' => 'updated_before', 'filters' => $options->filters])
+                        @include('search.parts.date-filter', ['name' => 'created_after', 'filters' => $options->filters])
+                        @include('search.parts.date-filter', ['name' => 'created_before', 'filters' => $options->filters])
 
                         <button type="submit" class="button">{{ trans('entities.search_update') }}</button>
                     </form>
@@ -77,7 +77,7 @@
 
                     <h6 class="text-muted">{{ trans_choice('entities.search_total_results_found', $totalResults, ['count' => $totalResults]) }}</h6>
                     <div class="book-contents">
-                        @include('partials.entity-list', ['entities' => $entities, 'showPath' => true, 'showTags' => true])
+                        @include('entities.list', ['entities' => $entities, 'showPath' => true, 'showTags' => true])
                     </div>
 
                     @if($hasNextPage)
similarity index 77%
rename from resources/views/search/entity-ajax-list.blade.php
rename to resources/views/search/parts/entity-ajax-list.blade.php
index 36a28b93eb17fe910da3e5e4bd610ec5428f7984..a4eedf75e8c8e3d4a6804b324d84e6fe85cfda7d 100644 (file)
@@ -2,7 +2,7 @@
     @if(count($entities) > 0)
         @foreach($entities as $index => $entity)
 
-            @include('partials.entity-list-item', ['entity' => $entity, 'showPath' => true])
+            @include('entities.list-item', ['entity' => $entity, 'showPath' => true])
             @if($index !== count($entities) - 1)
                 <hr>
             @endif
index 27bf8706417d3e7ce75ed9aaed6727ce24c74665..84f180f3b41fbc43c07933aeab7a4b6910caa5d4 100644 (file)
@@ -1,11 +1,11 @@
-@extends('simple-layout')
+@extends('layouts.simple')
 
 @section('body')
 <div class="container">
 
     <div class="grid left-focus v-center no-row-gap">
         <div class="py-m">
-            @include('settings.navbar', ['selected' => 'audit'])
+            @include('settings.parts.navbar', ['selected' => 'audit'])
         </div>
     </div>
 
@@ -45,7 +45,7 @@
                      component="submit-on-change"
                      option:submit-on-change:filter='[name="user"]'>
                     <label for="owner">{{ trans('settings.audit_table_user') }}</label>
-                    @include('components.user-select', ['user' => $listDetails['user'] ? \BookStack\Auth\User::query()->find($listDetails['user']) : null, 'name' => 'user', 'compact' =>  true])
+                    @include('form.user-select', ['user' => $listDetails['user'] ? \BookStack\Auth\User::query()->find($listDetails['user']) : null, 'name' => 'user', 'compact' =>  true])
                 </div>
             </form>
         </div>
                     <a href="{{ sortUrl('/settings/audit', $listDetails, ['sort' => 'key']) }}">{{ trans('settings.audit_table_event') }}</a>
                 </th>
                 <th>{{ trans('settings.audit_table_related') }}</th>
+                <th>{{ trans('settings.audit_table_ip') }}</th>
                 <th>
                     <a href="{{ sortUrl('/settings/audit', $listDetails, ['sort' => 'created_at']) }}">{{ trans('settings.audit_table_date') }}</a></th>
             </tr>
             @foreach($activities as $activity)
                 <tr>
                     <td>
-                        @include('partials.table-user', ['user' => $activity->user, 'user_id' => $activity->user_id])
+                        @include('settings.parts.table-user', ['user' => $activity->user, 'user_id' => $activity->user_id])
                     </td>
                     <td>{{ $activity->type }}</td>
                     <td width="40%">
@@ -88,6 +89,7 @@
                             <div class="px-m">{{ $activity->detail }}</div>
                         @endif
                     </td>
+                    <td>{{ $activity->ip }}</td>
                     <td>{{ $activity->created_at }}</td>
                 </tr>
             @endforeach
index ad03b6c917fdfecc670a3950650bfa092f7c263a..c87d84c5ef24eeee8252003721317c6b142b51b5 100644 (file)
@@ -1,9 +1,9 @@
-@extends('simple-layout')
+@extends('layouts.simple')
 
 @section('body')
     <div class="container small">
 
-        @include('settings.navbar-with-version', ['selected' => 'settings'])
+        @include('settings.parts.navbar-with-version', ['selected' => 'settings'])
 
         <div class="card content-wrap auto-height">
             <h2 id="features" class="list-heading">{{ trans('settings.app_features_security') }}</h2>
@@ -25,7 +25,7 @@
                             @endif
                         </div>
                         <div>
-                            @include('components.toggle-switch', [
+                            @include('form.toggle-switch', [
                                 'name' => 'setting-app-public',
                                 'value' => setting('app-public'),
                                 'label' => trans('settings.app_public_access_toggle'),
@@ -39,7 +39,7 @@
                             <p class="small">{{ trans('settings.app_secure_images_desc') }}</p>
                         </div>
                         <div>
-                            @include('components.toggle-switch', [
+                            @include('form.toggle-switch', [
                                 'name' => 'setting-app-secure-images',
                                 'value' => setting('app-secure-images'),
                                 'label' => trans('settings.app_secure_images_toggle'),
@@ -53,7 +53,7 @@
                             <p class="small">{!! trans('settings.app_disable_comments_desc') !!}</p>
                         </div>
                         <div>
-                            @include('components.toggle-switch', [
+                            @include('form.toggle-switch', [
                                 'name' => 'setting-app-disable-comments',
                                 'value' => setting('app-disable-comments'),
                                 'label' => trans('settings.app_disable_comments_toggle'),
@@ -85,7 +85,7 @@
                         </div>
                         <div class="pt-xs">
                             <input type="text" value="{{ setting('app-name', 'BookStack') }}" name="setting-app-name" id="setting-app-name">
-                            @include('components.toggle-switch', [
+                            @include('form.toggle-switch', [
                                 'name' => 'setting-app-name-header',
                                 'value' => setting('app-name-header'),
                                 'label' => trans('settings.app_name_header'),
                             <p class="small">{!! trans('settings.app_logo_desc') !!}</p>
                         </div>
                         <div class="pt-xs">
-                            @include('components.image-picker', [
+                            @include('form.image-picker', [
                                      'removeName' => 'setting-app-logo',
                                      'removeValue' => 'none',
                                      'defaultImage' => url('/logo.png'),
                         </div>
                         <div class="grid half pt-m">
                             <div>
-                                @include('components.setting-entity-color-picker', ['type' => 'bookshelf'])
-                                @include('components.setting-entity-color-picker', ['type' => 'book'])
-                                @include('components.setting-entity-color-picker', ['type' => 'chapter'])
+                                @include('settings.parts.setting-entity-color-picker', ['type' => 'bookshelf'])
+                                @include('settings.parts.setting-entity-color-picker', ['type' => 'book'])
+                                @include('settings.parts.setting-entity-color-picker', ['type' => 'chapter'])
                             </div>
                             <div>
-                                @include('components.setting-entity-color-picker', ['type' => 'page'])
-                                @include('components.setting-entity-color-picker', ['type' => 'page-draft'])
+                                @include('settings.parts.setting-entity-color-picker', ['type' => 'page'])
+                                @include('settings.parts.setting-entity-color-picker', ['type' => 'page-draft'])
                             </div>
                         </div>
                     </div>
                             </select>
 
                             <div page-picker-container style="display: none;" class="mt-m">
-                                @include('components.page-picker', ['name' => 'setting-app-homepage', 'placeholder' => trans('settings.app_homepage_select'), 'value' => setting('app-homepage')])
+                                @include('settings.parts.page-picker', ['name' => 'setting-app-homepage', 'placeholder' => trans('settings.app_homepage_select'), 'value' => setting('app-homepage')])
                             </div>
                         </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', [])])
+                        @include('settings.parts.footer-links', ['name' => 'setting-app-footer-links', 'value' => setting('app-footer-links', [])])
                     </div>
 
 
                             <p class="small">{!! trans('settings.reg_enable_desc') !!}</p>
                         </div>
                         <div>
-                            @include('components.toggle-switch', [
+                            @include('form.toggle-switch', [
                                 'name' => 'setting-registration-enabled',
                                 'value' => setting('registration-enabled'),
                                 'label' => trans('settings.reg_enable_toggle')
                             <p class="small">{{ trans('settings.reg_confirm_email_desc') }}</p>
                         </div>
                         <div>
-                            @include('components.toggle-switch', [
+                            @include('form.toggle-switch', [
                                 'name' => 'setting-registration-confirmation',
                                 'value' => setting('registration-confirmation'),
                                 'label' => trans('settings.reg_email_confirmation_toggle')
 
     </div>
 
-    @include('components.entity-selector-popup', ['entityTypes' => 'page'])
+    @include('entities.selector-popup', ['entityTypes' => 'page'])
 @stop
index 941a258d84942e32a1c004e4b0b7013f954984f0..ea94413f2f7028673b9d80177f4f14f20d6b2d3e 100644 (file)
@@ -1,9 +1,9 @@
-@extends('simple-layout')
+@extends('layouts.simple')
 
 @section('body')
 <div class="container small">
 
-    @include('settings.navbar-with-version', ['selected' => 'maintenance'])
+    @include('settings.parts.navbar-with-version', ['selected' => 'maintenance'])
 
     <div class="card content-wrap auto-height pb-xl">
         <h2 class="list-heading">{{ trans('settings.recycle_bin') }}</h2>
similarity index 86%
rename from resources/views/settings/navbar-with-version.blade.php
rename to resources/views/settings/parts/navbar-with-version.blade.php
index c02c370feb83030363a25597efb6032488b41927..09af699a33b8fe8c778a49fe7dbf4bc6cabadd55 100644 (file)
@@ -4,7 +4,7 @@ $version - Version of bookstack to display
 --}}
 <div class="flex-container-row v-center wrap">
     <div class="py-m flex fit-content">
-        @include('settings.navbar', ['selected' => $selected])
+        @include('settings.parts.navbar', ['selected' => $selected])
     </div>
     <div class="flex"></div>
     <div class="text-right p-m flex fit-content">
diff --git a/resources/views/settings/recycle-bin/deletable-entity-list.blade.php b/resources/views/settings/recycle-bin/deletable-entity-list.blade.php
deleted file mode 100644 (file)
index 07ad94f..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-@include('partials.entity-display-item', ['entity' => $entity])
-@if($entity->isA('book'))
-    @foreach($entity->chapters()->withTrashed()->get() as $chapter)
-        @include('partials.entity-display-item', ['entity' => $chapter])
-    @endforeach
-@endif
-@if($entity->isA('book') || $entity->isA('chapter'))
-    @foreach($entity->pages()->withTrashed()->get() as $page)
-        @include('partials.entity-display-item', ['entity' => $page])
-    @endforeach
-@endif
\ No newline at end of file
index bd5ef79f0f601bb655de7e3ec256ec3ef729a942..ab603498444c414213263bea440ea83bdee326cc 100644 (file)
@@ -1,10 +1,10 @@
-@extends('simple-layout')
+@extends('layouts.simple')
 
 @section('body')
     <div class="container small">
 
         <div class="py-m">
-            @include('settings.navbar', ['selected' => 'maintenance'])
+            @include('settings.parts.navbar', ['selected' => 'maintenance'])
         </div>
 
         <div class="card content-wrap auto-height">
@@ -20,7 +20,7 @@
             @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])
+                @include('settings.recycle-bin.parts.deletable-entity-list', ['entity' => $deletion->deletable])
             @endif
 
         </div>
index cf9808d955df243dd66b7df863bb45d9854cd996..b31bf02e545e3ac0b4523772e4868918857dcaab 100644 (file)
@@ -1,10 +1,10 @@
-@extends('simple-layout')
+@extends('layouts.simple')
 
 @section('body')
     <div class="container">
 
         <div class="py-m">
-            @include('settings.navbar', ['selected' => 'maintenance'])
+            @include('settings.parts.navbar', ['selected' => 'maintenance'])
         </div>
 
         <div class="card content-wrap auto-height">
@@ -89,7 +89,7 @@
                         </div>
                         @endif
                     </td>
-                    <td>@include('partials.table-user', ['user' => $deletion->deleter, 'user_id' => $deletion->deleted_by])</td>
+                    <td>@include('settings.parts.table-user', ['user' => $deletion->deleter, 'user_id' => $deletion->deleted_by])</td>
                     <td width="200">{{ $deletion->created_at }}</td>
                     <td width="150" class="text-right">
                         <div component="dropdown" class="dropdown-container">
diff --git a/resources/views/settings/recycle-bin/parts/deletable-entity-list.blade.php b/resources/views/settings/recycle-bin/parts/deletable-entity-list.blade.php
new file mode 100644 (file)
index 0000000..c2d8a42
--- /dev/null
@@ -0,0 +1,11 @@
+@include('settings.recycle-bin.parts.entity-display-item', ['entity' => $entity])
+@if($entity->isA('book'))
+    @foreach($entity->chapters()->withTrashed()->get() as $chapter)
+        @include('settings.recycle-bin.parts.entity-display-item', ['entity' => $chapter])
+    @endforeach
+@endif
+@if($entity->isA('book') || $entity->isA('chapter'))
+    @foreach($entity->pages()->withTrashed()->get() as $page)
+        @include('settings.recycle-bin.parts.entity-display-item', ['entity' => $page])
+    @endforeach
+@endif
\ No newline at end of file
index 8decd13f68a1889d965c291c24e686be9b847340..5268bf0671cd3ad36ea492bdef3e3479cb5ac4f4 100644 (file)
@@ -1,10 +1,10 @@
-@extends('simple-layout')
+@extends('layouts.simple')
 
 @section('body')
     <div class="container small">
 
         <div class="py-m">
-            @include('settings.navbar', ['selected' => 'maintenance'])
+            @include('settings.parts.navbar', ['selected' => 'maintenance'])
         </div>
 
         <div class="card content-wrap auto-height">
@@ -30,7 +30,7 @@
                     @endif
                 </div>
 
-                @include('settings.recycle-bin.deletable-entity-list', ['entity' => $deletion->deletable])
+                @include('settings.recycle-bin.parts.deletable-entity-list', ['entity' => $deletion->deletable])
             @endif
 
         </div>
index df902133f3ee514858ae703df5e52b5ab10f934b..f2edfa1c573aa6c3b3749c343f819d107aef3fc4 100644 (file)
@@ -1,15 +1,15 @@
-@extends('simple-layout')
+@extends('layouts.simple')
 
 @section('body')
 
     <div class="container small">
 
         <div class="py-m">
-            @include('settings.navbar', ['selected' => 'roles'])
+            @include('settings.parts.navbar', ['selected' => 'roles'])
         </div>
 
         <form action="{{ url("/settings/roles/new") }}" method="POST">
-            @include('settings.roles.form', ['title' => trans('settings.role_create')])
+            @include('settings.roles.parts.form', ['title' => trans('settings.role_create')])
         </form>
     </div>
 
index fa7c12b0a2aafab0ec401e4a951a24966a89afa6..52362461d0295a91ea66f2135c0ee355b2943115 100644 (file)
@@ -1,10 +1,10 @@
-@extends('simple-layout')
+@extends('layouts.simple')
 
 @section('body')
     <div class="container small">
 
         <div class="py-m">
-            @include('settings.navbar', ['selected' => 'roles'])
+            @include('settings.parts.navbar', ['selected' => 'roles'])
         </div>
 
         <div class="card content-wrap auto-height">
index 0f83bdb0becca1370a8a3085055cf376e2a0b1e3..e2018d3e9b72a07692a54ceeabb9b74c9e968b83 100644 (file)
@@ -1,15 +1,15 @@
-@extends('simple-layout')
+@extends('layouts.simple')
 
 @section('body')
 
     <div class="container small">
         <div class="py-m">
-            @include('settings.navbar', ['selected' => 'roles'])
+            @include('settings.parts.navbar', ['selected' => 'roles'])
         </div>
 
         <form action="{{ url("/settings/roles/{$role->id}") }}" method="POST">
             <input type="hidden" name="_method" value="PUT">
-            @include('settings.roles.form', ['model' => $role, 'title' => trans('settings.role_edit'), 'icon' => 'edit'])
+            @include('settings.roles.parts.form', ['model' => $role, 'title' => trans('settings.role_edit'), 'icon' => 'edit'])
         </form>
     </div>
 
index 47cd8c920fffa07909215e12e8f8d3d5d8dedee9..6c2996787a68636aab38bc9c7fbfe2fbae9dc403 100644 (file)
@@ -1,11 +1,11 @@
-@extends('simple-layout')
+@extends('layouts.simple')
 
 @section('body')
 
     <div class="container small">
 
         <div class="py-m">
-            @include('settings.navbar', ['selected' => 'roles'])
+            @include('settings.parts.navbar', ['selected' => 'roles'])
         </div>
 
         <div class="card content-wrap auto-height">
                 @foreach($roles as $role)
                     <tr>
                         <td><a href="{{ url("/settings/roles/{$role->id}") }}">{{ $role->display_name }}</a></td>
-                        <td>{{ $role->description }}</td>
+                        <td>
+                            @if($role->mfa_enforced)
+                                <span title="{{ trans('settings.role_mfa_enforced') }}">@icon('lock') </span>
+                            @endif
+                            {{ $role->description }}
+                        </td>
                         <td class="text-center">{{ $role->users->count() }}</td>
                     </tr>
                 @endforeach
similarity index 85%
rename from resources/views/settings/roles/checkbox.blade.php
rename to resources/views/settings/roles/parts/checkbox.blade.php
index 98201da8fc7d82af4ad69c867f159531530c2647..44cdd22bdf6f803089e2063b4fb033b55d61b1a2 100644 (file)
@@ -1,5 +1,5 @@
 
-@include('components.custom-checkbox', [
+@include('form.custom-checkbox', [
        'name' => 'permissions[' . $permission . ']',
        'value' => 'true',
        'checked' => old('permissions'.$permission, false)|| (!old('display_name', false) && (isset($role) && $role->hasPermission($permission))),
similarity index 51%
rename from resources/views/settings/roles/form.blade.php
rename to resources/views/settings/roles/parts/form.blade.php
index 604acbb165021a5f8bc50814106f5447d77dcda0..2f94398b5449846b89c57b3b7686bbe3369686ee 100644 (file)
             </div>
             <div>
                 <div class="form-group">
-                    <label for="name">{{ trans('settings.role_name') }}</label>
+                    <label for="display_name">{{ trans('settings.role_name') }}</label>
                     @include('form.text', ['name' => 'display_name'])
                 </div>
                 <div class="form-group">
-                    <label for="name">{{ trans('settings.role_desc') }}</label>
+                    <label for="description">{{ trans('settings.role_desc') }}</label>
                     @include('form.text', ['name' => 'description'])
                 </div>
+                <div class="form-group">
+                    @include('form.checkbox', ['name' => 'mfa_enforced', 'label' => trans('settings.role_mfa_enforced') ])
+                </div>
 
                 @if(config('auth.method') === 'ldap' || config('auth.method') === 'saml2')
                     <div class="form-group">
 
             <div class="toggle-switch-list grid half mt-m">
                 <div>
-                    <div>@include('settings.roles.checkbox', ['permission' => 'restrictions-manage-all', 'label' => trans('settings.role_manage_entity_permissions')])</div>
-                    <div>@include('settings.roles.checkbox', ['permission' => 'restrictions-manage-own', 'label' => trans('settings.role_manage_own_entity_permissions')])</div>
-                    <div>@include('settings.roles.checkbox', ['permission' => 'templates-manage', 'label' => trans('settings.role_manage_page_templates')])</div>
-                    <div>@include('settings.roles.checkbox', ['permission' => 'access-api', 'label' => trans('settings.role_access_api')])</div>
+                    <div>@include('settings.roles.parts.checkbox', ['permission' => 'restrictions-manage-all', 'label' => trans('settings.role_manage_entity_permissions')])</div>
+                    <div>@include('settings.roles.parts.checkbox', ['permission' => 'restrictions-manage-own', 'label' => trans('settings.role_manage_own_entity_permissions')])</div>
+                    <div>@include('settings.roles.parts.checkbox', ['permission' => 'templates-manage', 'label' => trans('settings.role_manage_page_templates')])</div>
+                    <div>@include('settings.roles.parts.checkbox', ['permission' => 'access-api', 'label' => trans('settings.role_access_api')])</div>
+                    <div>@include('settings.roles.parts.checkbox', ['permission' => 'content-export', 'label' => trans('settings.role_export_content')])</div>
                 </div>
                 <div>
-                    <div>@include('settings.roles.checkbox', ['permission' => 'settings-manage', 'label' => trans('settings.role_manage_settings')])</div>
-                    <div>@include('settings.roles.checkbox', ['permission' => 'users-manage', 'label' => trans('settings.role_manage_users')])</div>
-                    <div>@include('settings.roles.checkbox', ['permission' => 'user-roles-manage', 'label' => trans('settings.role_manage_roles')])</div>
+                    <div>@include('settings.roles.parts.checkbox', ['permission' => 'settings-manage', 'label' => trans('settings.role_manage_settings')])</div>
+                    <div>@include('settings.roles.parts.checkbox', ['permission' => 'users-manage', 'label' => trans('settings.role_manage_users')])</div>
+                    <div>@include('settings.roles.parts.checkbox', ['permission' => 'user-roles-manage', 'label' => trans('settings.role_manage_roles')])</div>
                     <p class="text-warn text-small mt-s mb-none">{{ trans('settings.roles_system_warning') }}</p>
                 </div>
             </div>
                         <a href="#" permissions-table-toggle-all-in-row class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
                     </td>
                     <td>
-                        @include('settings.roles.checkbox', ['permission' => 'bookshelf-create-all', 'label' => trans('settings.role_all')])
+                        @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-create-all', 'label' => trans('settings.role_all')])
                     </td>
                     <td>
-                        @include('settings.roles.checkbox', ['permission' => 'bookshelf-view-own', 'label' => trans('settings.role_own')])
+                        @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-view-own', 'label' => trans('settings.role_own')])
                         <br>
-                        @include('settings.roles.checkbox', ['permission' => 'bookshelf-view-all', 'label' => trans('settings.role_all')])
+                        @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-view-all', 'label' => trans('settings.role_all')])
                     </td>
                     <td>
-                        @include('settings.roles.checkbox', ['permission' => 'bookshelf-update-own', 'label' => trans('settings.role_own')])
+                        @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-update-own', 'label' => trans('settings.role_own')])
                         <br>
-                        @include('settings.roles.checkbox', ['permission' => 'bookshelf-update-all', 'label' => trans('settings.role_all')])
+                        @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-update-all', 'label' => trans('settings.role_all')])
                     </td>
                     <td>
-                        @include('settings.roles.checkbox', ['permission' => 'bookshelf-delete-own', 'label' => trans('settings.role_own')])
+                        @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-delete-own', 'label' => trans('settings.role_own')])
                         <br>
-                        @include('settings.roles.checkbox', ['permission' => 'bookshelf-delete-all', 'label' => trans('settings.role_all')])
+                        @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-delete-all', 'label' => trans('settings.role_all')])
                     </td>
                 </tr>
                 <tr>
                         <a href="#" permissions-table-toggle-all-in-row class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
                     </td>
                     <td>
-                        @include('settings.roles.checkbox', ['permission' => 'book-create-all', 'label' => trans('settings.role_all')])
+                        @include('settings.roles.parts.checkbox', ['permission' => 'book-create-all', 'label' => trans('settings.role_all')])
                     </td>
                     <td>
-                        @include('settings.roles.checkbox', ['permission' => 'book-view-own', 'label' => trans('settings.role_own')])
+                        @include('settings.roles.parts.checkbox', ['permission' => 'book-view-own', 'label' => trans('settings.role_own')])
                         <br>
-                        @include('settings.roles.checkbox', ['permission' => 'book-view-all', 'label' => trans('settings.role_all')])
+                        @include('settings.roles.parts.checkbox', ['permission' => 'book-view-all', 'label' => trans('settings.role_all')])
                     </td>
                     <td>
-                        @include('settings.roles.checkbox', ['permission' => 'book-update-own', 'label' => trans('settings.role_own')])
+                        @include('settings.roles.parts.checkbox', ['permission' => 'book-update-own', 'label' => trans('settings.role_own')])
                         <br>
-                        @include('settings.roles.checkbox', ['permission' => 'book-update-all', 'label' => trans('settings.role_all')])
+                        @include('settings.roles.parts.checkbox', ['permission' => 'book-update-all', 'label' => trans('settings.role_all')])
                     </td>
                     <td>
-                        @include('settings.roles.checkbox', ['permission' => 'book-delete-own', 'label' => trans('settings.role_own')])
+                        @include('settings.roles.parts.checkbox', ['permission' => 'book-delete-own', 'label' => trans('settings.role_own')])
                         <br>
-                        @include('settings.roles.checkbox', ['permission' => 'book-delete-all', 'label' => trans('settings.role_all')])
+                        @include('settings.roles.parts.checkbox', ['permission' => 'book-delete-all', 'label' => trans('settings.role_all')])
                     </td>
                 </tr>
                 <tr>
                         <a href="#" permissions-table-toggle-all-in-row class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
                     </td>
                     <td>
-                        @include('settings.roles.checkbox', ['permission' => 'chapter-create-own', 'label' => trans('settings.role_own')])
+                        @include('settings.roles.parts.checkbox', ['permission' => 'chapter-create-own', 'label' => trans('settings.role_own')])
                         <br>
-                        @include('settings.roles.checkbox', ['permission' => 'chapter-create-all', 'label' => trans('settings.role_all')])
+                        @include('settings.roles.parts.checkbox', ['permission' => 'chapter-create-all', 'label' => trans('settings.role_all')])
                     </td>
                     <td>
-                        @include('settings.roles.checkbox', ['permission' => 'chapter-view-own', 'label' => trans('settings.role_own')])
+                        @include('settings.roles.parts.checkbox', ['permission' => 'chapter-view-own', 'label' => trans('settings.role_own')])
                         <br>
-                        @include('settings.roles.checkbox', ['permission' => 'chapter-view-all', 'label' => trans('settings.role_all')])
+                        @include('settings.roles.parts.checkbox', ['permission' => 'chapter-view-all', 'label' => trans('settings.role_all')])
                     </td>
                     <td>
-                        @include('settings.roles.checkbox', ['permission' => 'chapter-update-own', 'label' => trans('settings.role_own')])
+                        @include('settings.roles.parts.checkbox', ['permission' => 'chapter-update-own', 'label' => trans('settings.role_own')])
                         <br>
-                        @include('settings.roles.checkbox', ['permission' => 'chapter-update-all', 'label' => trans('settings.role_all')])
+                        @include('settings.roles.parts.checkbox', ['permission' => 'chapter-update-all', 'label' => trans('settings.role_all')])
                     </td>
                     <td>
-                        @include('settings.roles.checkbox', ['permission' => 'chapter-delete-own', 'label' => trans('settings.role_own')])
+                        @include('settings.roles.parts.checkbox', ['permission' => 'chapter-delete-own', 'label' => trans('settings.role_own')])
                         <br>
-                        @include('settings.roles.checkbox', ['permission' => 'chapter-delete-all', 'label' => trans('settings.role_all')])
+                        @include('settings.roles.parts.checkbox', ['permission' => 'chapter-delete-all', 'label' => trans('settings.role_all')])
                     </td>
                 </tr>
                 <tr>
                         <a href="#" permissions-table-toggle-all-in-row class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
                     </td>
                     <td>
-                        @include('settings.roles.checkbox', ['permission' => 'page-create-own', 'label' => trans('settings.role_own')])
+                        @include('settings.roles.parts.checkbox', ['permission' => 'page-create-own', 'label' => trans('settings.role_own')])
                         <br>
-                        @include('settings.roles.checkbox', ['permission' => 'page-create-all', 'label' => trans('settings.role_all')])
+                        @include('settings.roles.parts.checkbox', ['permission' => 'page-create-all', 'label' => trans('settings.role_all')])
                     </td>
                     <td>
-                        @include('settings.roles.checkbox', ['permission' => 'page-view-own', 'label' => trans('settings.role_own')])
+                        @include('settings.roles.parts.checkbox', ['permission' => 'page-view-own', 'label' => trans('settings.role_own')])
                         <br>
-                        @include('settings.roles.checkbox', ['permission' => 'page-view-all', 'label' => trans('settings.role_all')])
+                        @include('settings.roles.parts.checkbox', ['permission' => 'page-view-all', 'label' => trans('settings.role_all')])
                     </td>
                     <td>
-                        @include('settings.roles.checkbox', ['permission' => 'page-update-own', 'label' => trans('settings.role_own')])
+                        @include('settings.roles.parts.checkbox', ['permission' => 'page-update-own', 'label' => trans('settings.role_own')])
                         <br>
-                        @include('settings.roles.checkbox', ['permission' => 'page-update-all', 'label' => trans('settings.role_all')])
+                        @include('settings.roles.parts.checkbox', ['permission' => 'page-update-all', 'label' => trans('settings.role_all')])
                     </td>
                     <td>
-                        @include('settings.roles.checkbox', ['permission' => 'page-delete-own', 'label' => trans('settings.role_own')])
+                        @include('settings.roles.parts.checkbox', ['permission' => 'page-delete-own', 'label' => trans('settings.role_own')])
                         <br>
-                        @include('settings.roles.checkbox', ['permission' => 'page-delete-all', 'label' => trans('settings.role_all')])
+                        @include('settings.roles.parts.checkbox', ['permission' => 'page-delete-all', 'label' => trans('settings.role_all')])
                     </td>
                 </tr>
                 <tr>
                         <div>{{ trans('entities.images') }}</div>
                         <a href="#" permissions-table-toggle-all-in-row class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
                     </td>
-                    <td>@include('settings.roles.checkbox', ['permission' => 'image-create-all', 'label' => ''])</td>
+                    <td>@include('settings.roles.parts.checkbox', ['permission' => 'image-create-all', 'label' => ''])</td>
                     <td style="line-height:1.2;"><small class="faded">{{ trans('settings.role_controlled_by_asset') }}</small></td>
                     <td>
-                        @include('settings.roles.checkbox', ['permission' => 'image-update-own', 'label' => trans('settings.role_own')])
+                        @include('settings.roles.parts.checkbox', ['permission' => 'image-update-own', 'label' => trans('settings.role_own')])
                         <br>
-                        @include('settings.roles.checkbox', ['permission' => 'image-update-all', 'label' => trans('settings.role_all')])
+                        @include('settings.roles.parts.checkbox', ['permission' => 'image-update-all', 'label' => trans('settings.role_all')])
                     </td>
                     <td>
-                        @include('settings.roles.checkbox', ['permission' => 'image-delete-own', 'label' => trans('settings.role_own')])
+                        @include('settings.roles.parts.checkbox', ['permission' => 'image-delete-own', 'label' => trans('settings.role_own')])
                         <br>
-                        @include('settings.roles.checkbox', ['permission' => 'image-delete-all', 'label' => trans('settings.role_all')])
+                        @include('settings.roles.parts.checkbox', ['permission' => 'image-delete-all', 'label' => trans('settings.role_all')])
                     </td>
                 </tr>
                 <tr>
                         <div>{{ trans('entities.attachments') }}</div>
                         <a href="#" permissions-table-toggle-all-in-row class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
                     </td>
-                    <td>@include('settings.roles.checkbox', ['permission' => 'attachment-create-all', 'label' => ''])</td>
+                    <td>@include('settings.roles.parts.checkbox', ['permission' => 'attachment-create-all', 'label' => ''])</td>
                     <td style="line-height:1.2;"><small class="faded">{{ trans('settings.role_controlled_by_asset') }}</small></td>
                     <td>
-                        @include('settings.roles.checkbox', ['permission' => 'attachment-update-own', 'label' => trans('settings.role_own')])
+                        @include('settings.roles.parts.checkbox', ['permission' => 'attachment-update-own', 'label' => trans('settings.role_own')])
                         <br>
-                        @include('settings.roles.checkbox', ['permission' => 'attachment-update-all', 'label' => trans('settings.role_all')])
+                        @include('settings.roles.parts.checkbox', ['permission' => 'attachment-update-all', 'label' => trans('settings.role_all')])
                     </td>
                     <td>
-                        @include('settings.roles.checkbox', ['permission' => 'attachment-delete-own', 'label' => trans('settings.role_own')])
+                        @include('settings.roles.parts.checkbox', ['permission' => 'attachment-delete-own', 'label' => trans('settings.role_own')])
                         <br>
-                        @include('settings.roles.checkbox', ['permission' => 'attachment-delete-all', 'label' => trans('settings.role_all')])
+                        @include('settings.roles.parts.checkbox', ['permission' => 'attachment-delete-all', 'label' => trans('settings.role_all')])
                     </td>
                 </tr>
                 <tr>
                         <div>{{ trans('entities.comments') }}</div>
                         <a href="#" permissions-table-toggle-all-in-row class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
                     </td>
-                    <td>@include('settings.roles.checkbox', ['permission' => 'comment-create-all', 'label' => ''])</td>
+                    <td>@include('settings.roles.parts.checkbox', ['permission' => 'comment-create-all', 'label' => ''])</td>
                     <td style="line-height:1.2;"><small class="faded">{{ trans('settings.role_controlled_by_asset') }}</small></td>
                     <td>
-                        @include('settings.roles.checkbox', ['permission' => 'comment-update-own', 'label' => trans('settings.role_own')])
+                        @include('settings.roles.parts.checkbox', ['permission' => 'comment-update-own', 'label' => trans('settings.role_own')])
                         <br>
-                        @include('settings.roles.checkbox', ['permission' => 'comment-update-all', 'label' => trans('settings.role_all')])
+                        @include('settings.roles.parts.checkbox', ['permission' => 'comment-update-all', 'label' => trans('settings.role_all')])
                     </td>
                     <td>
-                        @include('settings.roles.checkbox', ['permission' => 'comment-delete-own', 'label' => trans('settings.role_own')])
+                        @include('settings.roles.parts.checkbox', ['permission' => 'comment-delete-own', 'label' => trans('settings.role_own')])
                         <br>
-                        @include('settings.roles.checkbox', ['permission' => 'comment-delete-all', 'label' => trans('settings.role_all')])
+                        @include('settings.roles.parts.checkbox', ['permission' => 'comment-delete-all', 'label' => trans('settings.role_all')])
                     </td>
                 </tr>
             </table>
 
 <div class="card content-wrap auto-height">
     <h2 class="list-heading">{{ trans('settings.role_users') }}</h2>
-    @if(isset($role) && count($role->users) > 0)
+    @if(count($role->users ?? []) > 0)
         <div class="grid third">
             @foreach($role->users as $user)
                 <div class="user-list-item">
diff --git a/resources/views/shelves/_breadcrumbs.blade.php b/resources/views/shelves/_breadcrumbs.blade.php
deleted file mode 100644 (file)
index 91b4252..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-<div class="breadcrumbs">
-    <a href="{{$shelf->getUrl()}}" class="text-bookshelf text-button">@icon('bookshelf'){{ $shelf->getShortName() }}</a>
-</div>
\ No newline at end of file
index bea20eca93624cf234e89b73099b51fc5574c3c4..95b45906862b60fb5f4390d2ab6035fbac696b3b 100644 (file)
@@ -1,11 +1,11 @@
-@extends('simple-layout')
+@extends('layouts.simple')
 
 @section('body')
 
     <div class="container small">
 
         <div class="my-s">
-            @include('partials.breadcrumbs', ['crumbs' => [
+            @include('entities.breadcrumbs', ['crumbs' => [
                 '/shelves' => [
                     'text' => trans('entities.shelves'),
                     'icon' => 'bookshelf',
@@ -20,7 +20,7 @@
         <main class="card content-wrap">
             <h1 class="list-heading">{{ trans('entities.shelves_create') }}</h1>
             <form action="{{ url("/shelves") }}" method="POST" enctype="multipart/form-data">
-                @include('shelves.form', ['shelf' => null, 'books' => $books])
+                @include('shelves.parts.form', ['shelf' => null, 'books' => $books])
             </form>
         </main>
 
index 2a78227bda656ffd611c21dd5570b96c1e8ea3ac..42d1f5d84aeb74b4cd518d3746ff6d8859546b0c 100644 (file)
@@ -1,11 +1,11 @@
-@extends('simple-layout')
+@extends('layouts.simple')
 
 @section('body')
 
     <div class="container small">
 
         <div class="my-s">
-            @include('partials.breadcrumbs', ['crumbs' => [
+            @include('entities.breadcrumbs', ['crumbs' => [
                 $shelf,
                 $shelf->getUrl('/delete') => [
                     'text' => trans('entities.shelves_delete'),
index 5ae3638fee955ec3f30ee3c068f5c45e6f7b7976..0114678eb0b38ff9aa1ef292910058e32138e3b3 100644 (file)
@@ -1,11 +1,11 @@
-@extends('simple-layout')
+@extends('layouts.simple')
 
 @section('body')
 
     <div class="container small">
 
         <div class="my-s">
-            @include('partials.breadcrumbs', ['crumbs' => [
+            @include('entities.breadcrumbs', ['crumbs' => [
                 $shelf,
                 $shelf->getUrl('/edit') => [
                     'text' => trans('entities.shelves_edit'),
@@ -18,7 +18,7 @@
             <h1 class="list-heading">{{ trans('entities.shelves_edit') }}</h1>
             <form action="{{ $shelf->getUrl() }}" method="POST" enctype="multipart/form-data">
                 <input type="hidden" name="_method" value="PUT">
-                @include('shelves.form', ['model' => $shelf])
+                @include('shelves.parts.form', ['model' => $shelf])
             </form>
         </main>
     </div>
index 21c33aa9c62d1aba748143b8af0e82ac3d01d351..5c25356b084bbe6bda1c666ebd04f0f0225464ff 100644 (file)
@@ -1,7 +1,7 @@
-@extends('tri-layout')
+@extends('layouts.tri')
 
 @section('body')
-    @include('shelves.list', ['shelves' => $shelves, 'view' => $view])
+    @include('shelves.parts.list', ['shelves' => $shelves, 'view' => $view])
 @stop
 
 @section('right')
@@ -15,7 +15,7 @@
                     <span>{{ trans('entities.shelves_new_action') }}</span>
                 </a>
             @endif
-            @include('partials.view-toggle', ['view' => $view, 'type' => 'shelves'])
+            @include('entities.view-toggle', ['view' => $view, 'type' => 'shelves'])
         </div>
     </div>
 
     @if($recents)
         <div id="recents" class="mb-xl">
             <h5>{{ trans('entities.recently_viewed') }}</h5>
-            @include('partials.entity-list', ['entities' => $recents, 'style' => 'compact'])
+            @include('entities.list', ['entities' => $recents, 'style' => 'compact'])
         </div>
     @endif
 
     <div id="popular" class="mb-xl">
         <h5>{{ trans('entities.shelves_popular') }}</h5>
         @if(count($popular) > 0)
-            @include('partials.entity-list', ['entities' => $popular, 'style' => 'compact'])
+            @include('entities.list', ['entities' => $popular, 'style' => 'compact'])
         @else
             <div class="text-muted">{{ trans('entities.shelves_popular_empty') }}</div>
         @endif
@@ -41,7 +41,7 @@
     <div id="new" class="mb-xl">
         <h5>{{ trans('entities.shelves_new') }}</h5>
         @if(count($new) > 0)
-            @include('partials.entity-list', ['entities' => $new, 'style' => 'compact'])
+            @include('entities.list', ['entities' => $new, 'style' => 'compact'])
         @else
             <div class="text-muted">{{ trans('entities.shelves_new_empty') }}</div>
         @endif
similarity index 95%
rename from resources/views/shelves/form.blade.php
rename to resources/views/shelves/parts/form.blade.php
index c088bad892aa4e25bd76d7d2f90e0908f21c94d5..f29c28c8164f5c9330ea626a709f25fabc3663b5 100644 (file)
@@ -46,7 +46,7 @@
     <div class="collapse-content" collapsible-content>
         <p class="small">{{ trans('common.cover_image_description') }}</p>
 
-        @include('components.image-picker', [
+        @include('form.image-picker', [
             'defaultImage' => url('/book_default_cover.png'),
             'currentImage' => (isset($shelf) && $shelf->cover) ? $shelf->getBookCover() : url('/book_default_cover.png') ,
             'name' => 'image',
@@ -60,7 +60,7 @@
         <label for="tag-manager">{{ trans('entities.shelf_tags') }}</label>
     </button>
     <div class="collapse-content" collapsible-content>
-        @include('components.tag-manager', ['entity' => $shelf ?? null])
+        @include('entities.tag-manager', ['entity' => $shelf ?? null])
     </div>
 </div>
 
similarity index 83%
rename from resources/views/shelves/list.blade.php
rename to resources/views/shelves/parts/list.blade.php
index 3600a8c795f70303dc464eb73b6fe503ec1bef90..d78606ac700e081818d7c183e880116692a6b938 100644 (file)
@@ -4,7 +4,7 @@
     <div class="grid half v-center">
         <h1 class="list-heading">{{ trans('entities.shelves') }}</h1>
         <div class="text-right">
-            @include('partials.sort', ['options' => $sortOptions, 'order' => $order, 'sort' => $sort, 'type' => 'bookshelves'])
+            @include('entities.sort', ['options' => $sortOptions, 'order' => $order, 'sort' => $sort, 'type' => 'bookshelves'])
         </div>
     </div>
 
                     @if ($index !== 0)
                         <hr class="my-m">
                     @endif
-                    @include('shelves.list-item', ['shelf' => $shelf])
+                    @include('shelves.parts.list-item', ['shelf' => $shelf])
                 @endforeach
             </div>
         @else
             <div class="grid third">
                 @foreach($shelves as $key => $shelf)
-                    @include('partials.entity-grid-item', ['entity' => $shelf])
+                    @include('entities.grid-item', ['entity' => $shelf])
                 @endforeach
             </div>
         @endif
index df50be8dd1644e6300bf7758b015d1e6c61d36d6..a26325518d6ac5294c38c1a89e5c7a1fdeb117c0 100644 (file)
@@ -1,11 +1,11 @@
-@extends('simple-layout')
+@extends('layouts.simple')
 
 @section('body')
 
     <div class="container small">
 
         <div class="my-s">
-            @include('partials.breadcrumbs', ['crumbs' => [
+            @include('entities.breadcrumbs', ['crumbs' => [
                 $shelf,
                 $shelf->getUrl('/permissions') => [
                     'text' => trans('entities.shelves_permissions'),
@@ -14,7 +14,7 @@
             ]])
         </div>
 
-        <div class="card content-wrap">
+        <div class="card content-wrap auto-height">
             <h1 class="list-heading">{{ trans('entities.shelves_permissions') }}</h1>
             @include('form.entity-permissions', ['model' => $shelf])
         </div>
index f5920d4758d98305b5cf7268349b17c0deb26618..0d592468d1b8a1adace0bc1776695a779a4bd13a 100644 (file)
@@ -1,4 +1,4 @@
-@extends('tri-layout')
+@extends('layouts.tri')
 
 @push('social-meta')
     <meta property="og:description" content="{{ Str::limit($shelf->description, 100, '...') }}">
@@ -10,7 +10,7 @@
 @section('body')
 
     <div class="mb-s">
-        @include('partials.breadcrumbs', ['crumbs' => [
+        @include('entities.breadcrumbs', ['crumbs' => [
             $shelf,
         ]])
     </div>
@@ -21,7 +21,7 @@
             <h1 class="flex fit-content break-text">{{ $shelf->name }}</h1>
             <div class="flex"></div>
             <div class="flex fit-content text-m-right my-m ml-m">
-                @include('partials.sort', ['options' => [
+                @include('entities.sort', ['options' => [
                     'default' => trans('common.sort_default'),
                     'name' => trans('common.sort_name'),
                     'created_at' => trans('common.sort_created_at'),
                 @if($view === 'list')
                     <div class="entity-list">
                         @foreach($sortedVisibleShelfBooks as $book)
-                            @include('books.list-item', ['book' => $book])
+                            @include('books.parts.list-item', ['book' => $book])
                         @endforeach
                     </div>
                 @else
                     <div class="grid third">
                         @foreach($sortedVisibleShelfBooks as $book)
-                            @include('partials.entity-grid-item', ['entity' => $book])
+                            @include('entities.grid-item', ['entity' => $book])
                         @endforeach
                     </div>
                 @endif
 
     @if($shelf->tags->count() > 0)
         <div id="tags" class="mb-xl">
-            @include('components.tag-list', ['entity' => $shelf])
+            @include('entities.tag-list', ['entity' => $shelf])
         </div>
     @endif
 
     <div id="details" class="mb-xl">
         <h5>{{ trans('common.details') }}</h5>
         <div class="text-small text-muted blended-links">
-            @include('partials.entity-meta', ['entity' => $shelf])
+            @include('entities.meta', ['entity' => $shelf])
             @if($shelf->restricted)
                 <div class="active-restriction">
                     @if(userCan('restrictions-manage', $shelf))
@@ -98,7 +98,7 @@
     @if(count($activity) > 0)
         <div class="mb-xl">
             <h5>{{ trans('entities.recent_activity') }}</h5>
-            @include('partials.activity-list', ['activity' => $activity])
+            @include('common.activity-list', ['activity' => $activity])
         </div>
     @endif
 @stop
                 </a>
             @endif
 
-            @include('partials.view-toggle', ['view' => $view, 'type' => 'shelf'])
+            @include('entities.view-toggle', ['view' => $view, 'type' => 'shelf'])
 
             <hr class="primary-background">
 
 
             @if(signedInUser())
                 <hr class="primary-background">
-                @include('partials.entity-favourite-action', ['entity' => $shelf])
+                @include('entities.favourite-action', ['entity' => $shelf])
             @endif
 
         </div>
index 46c3e0b8a2316b555da6b86414b9dbde39dfc1ad..9cf772082d2e8a16af03d44166880ad68ff9291f 100644 (file)
@@ -1,4 +1,4 @@
-@extends('simple-layout')
+@extends('layouts.simple')
 
 @section('body')
 
@@ -11,7 +11,7 @@
                 {!! csrf_field() !!}
 
                 <div class="setting-list">
-                    @include('users.api-tokens.form')
+                    @include('users.api-tokens.parts.form')
 
                     <div>
                         <p class="text-warn italic">
index 8fcfcda95d6b59541be715bd08536dd72e8af977..45f0e2fa0eb808f9047b4ff6defbecf2d47d1556 100644 (file)
@@ -1,4 +1,4 @@
-@extends('simple-layout')
+@extends('layouts.simple')
 
 @section('body')
     <div class="container small pt-xl">
index 821a00d93cfa4590ac32ffad7c312d7b1f5c9112..61c1ac2a63e607099ef70fc12e5ab8680e0fa4cf 100644 (file)
@@ -1,4 +1,4 @@
-@extends('simple-layout')
+@extends('layouts.simple')
 
 @section('body')
 
@@ -36,7 +36,7 @@
                         </div>
                     @endif
 
-                    @include('users.api-tokens.form', ['model' => $token])
+                    @include('users.api-tokens.parts.form', ['model' => $token])
                 </div>
 
                 <div class="grid half gap-xl v-center">
index d953b646afe8c0ba10643863cb61fb384de064b5..960e38f7c077e43ec7b76b137a63363930e139f6 100644 (file)
@@ -1,11 +1,11 @@
-@extends('simple-layout')
+@extends('layouts.simple')
 
 @section('body')
 
     <div class="container small">
 
         <div class="py-m">
-            @include('settings.navbar', ['selected' => 'users'])
+            @include('settings.parts.navbar', ['selected' => 'users'])
         </div>
 
         <main class="card content-wrap">
@@ -15,7 +15,7 @@
                 {!! csrf_field() !!}
 
                 <div class="setting-list">
-                    @include('users.form')
+                    @include('users.parts.form')
                 </div>
 
                 <div class="form-group text-right">
index 7b1d38d340b1eb9cdf789806cf9a0421486be0f3..aea4d7954543b3a138f88bf6fb0ce56de0f2300d 100644 (file)
@@ -1,10 +1,10 @@
-@extends('simple-layout')
+@extends('layouts.simple')
 
 @section('body')
     <div class="container small">
 
         <div class="py-m">
-            @include('settings.navbar', ['selected' => 'users'])
+            @include('settings.parts.navbar', ['selected' => 'users'])
         </div>
 
         <div class="card content-wrap auto-height">
@@ -20,7 +20,7 @@
                     <p class="small">{{ trans('settings.users_migrate_ownership_desc') }}</p>
                 </div>
                 <div>
-                    @include('components.user-select', ['name' => 'new_owner_id', 'user' => null, 'compact' => false])
+                    @include('form.user-select', ['name' => 'new_owner_id', 'user' => null, 'compact' => false])
                 </div>
             </div>
 
index 5712855e6125e393c865c6f4af1fdaf94c11f5c8..997fd1bf0e523aad6a4145f18b8fbe41b6094863 100644 (file)
@@ -1,10 +1,10 @@
-@extends('simple-layout')
+@extends('layouts.simple')
 
 @section('body')
     <div class="container small">
 
         <div class="py-m">
-            @include('settings.navbar', ['selected' => 'users'])
+            @include('settings.parts.navbar', ['selected' => 'users'])
         </div>
 
         <section class="card content-wrap">
@@ -14,7 +14,7 @@
                 <input type="hidden" name="_method" value="PUT">
 
                 <div class="setting-list">
-                    @include('users.form', ['model' => $user, 'authMethod' => $authMethod])
+                    @include('users.parts.form', ['model' => $user, 'authMethod' => $authMethod])
 
                     <div class="grid half gap-xl">
                         <div>
@@ -22,7 +22,7 @@
                             <p class="small">{{ trans('settings.users_avatar_desc') }}</p>
                         </div>
                         <div>
-                            @include('components.image-picker', [
+                            @include('form.image-picker', [
                                 'resizeHeight' => '512',
                                 'resizeWidth' => '512',
                                 'showRemove' => false,
             </form>
         </section>
 
+        <section class="card content-wrap auto-height">
+            <h2 class="list-heading">{{ trans('settings.users_mfa') }}</h2>
+            <p>{{ trans('settings.users_mfa_desc') }}</p>
+            <div class="grid half gap-xl v-center pb-s">
+                <div>
+                    @if ($mfaMethods->count() > 0)
+                        <span class="text-pos">@icon('check-circle')</span>
+                    @else
+                        <span class="text-neg">@icon('cancel')</span>
+                    @endif
+                    {{ trans_choice('settings.users_mfa_x_methods', $mfaMethods->count()) }}
+                </div>
+                <div class="text-m-right">
+                    @if($user->id === user()->id)
+                        <a href="{{ url('/mfa/setup')  }}" class="button outline">{{ trans('settings.users_mfa_configure') }}</a>
+                    @endif
+                </div>
+            </div>
+
+        </section>
+
         @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>
         @endif
 
         @if((user()->id === $user->id && userCan('access-api')) || userCan('users-manage'))
-            @include('users.api-tokens.list', ['user' => $user])
+            @include('users.api-tokens.parts.list', ['user' => $user])
         @endif
     </div>
 
index 5eef511753dc16e41efb10fbff17f3dc9b742fd1..6c79169ca2dbc296857a487c4e376a66387080a1 100644 (file)
@@ -1,10 +1,10 @@
-@extends('simple-layout')
+@extends('layouts.simple')
 
 @section('body')
     <div class="container small">
 
         <div class="py-m">
-            @include('settings.navbar', ['selected' => 'users'])
+            @include('settings.parts.navbar', ['selected' => 'users'])
         </div>
 
         <main class="card content-wrap">
                         <td class="text-center" style="line-height: 0;"><img class="avatar med" src="{{ $user->getAvatar(40)}}" alt="{{ $user->name }}"></td>
                         <td>
                             <a href="{{ url("/settings/users/{$user->id}") }}">
-                                {{ $user->name }} <br> <span class="text-muted">{{ $user->email }}</span>
+                                {{ $user->name }}
+                                <br>
+                                <span class="text-muted">{{ $user->email }}</span>
+                                @if($user->mfa_values_count > 0)
+                                    <span title="MFA Configured" class="text-pos">@icon('lock')</span>
+                                @endif
                             </a>
                         </td>
                         <td>
similarity index 98%
rename from resources/views/users/form.blade.php
rename to resources/views/users/parts/form.blade.php
index 763c387d4601bca12bba95b71dd9ba2cfecb200f..7105e2ff14458578f978249728879bb1667b6c8d 100644 (file)
@@ -56,7 +56,7 @@
                 {{ trans('settings.users_send_invite_text') }}
             </p>
 
-            @include('components.toggle-switch', [
+            @include('form.toggle-switch', [
                 'name' => 'send_invite',
                 'value' => old('send_invite', 'true') === 'true',
                 'label' => trans('settings.users_send_invite_option')
index 5a76a222a7042eed59fb9151fb909ec7269dad98..b59c80ec6a22c99ef23d210adec0ae9c741e1f05 100644 (file)
@@ -1,4 +1,4 @@
-@extends('simple-layout')
+@extends('layouts.simple')
 
 @section('body')
 
@@ -9,7 +9,7 @@
             <div>
                 <section id="recent-user-activity" class="mb-xl">
                     <h5>{{ trans('entities.recent_activity') }}</h5>
-                    @include('partials.activity-list', ['activity' => $activity])
+                    @include('common.activity-list', ['activity' => $activity])
                 </section>
             </div>
 
@@ -64,7 +64,7 @@
                         @endif
                     </h2>
                     @if (count($recentlyCreated['pages']) > 0)
-                        @include('partials.entity-list', ['entities' => $recentlyCreated['pages'], 'showPath' => true])
+                        @include('entities.list', ['entities' => $recentlyCreated['pages'], 'showPath' => true])
                     @else
                         <p class="text-muted">{{ trans('entities.profile_not_created_pages', ['userName' => $user->name]) }}</p>
                     @endif
@@ -78,7 +78,7 @@
                         @endif
                     </h2>
                     @if (count($recentlyCreated['chapters']) > 0)
-                        @include('partials.entity-list', ['entities' => $recentlyCreated['chapters'], 'showPath' => true])
+                        @include('entities.list', ['entities' => $recentlyCreated['chapters'], 'showPath' => true])
                     @else
                         <p class="text-muted">{{ trans('entities.profile_not_created_chapters', ['userName' => $user->name]) }}</p>
                     @endif
@@ -92,7 +92,7 @@
                         @endif
                     </h2>
                     @if (count($recentlyCreated['books']) > 0)
-                        @include('partials.entity-list', ['entities' => $recentlyCreated['books'], 'showPath' => true])
+                        @include('entities.list', ['entities' => $recentlyCreated['books'], 'showPath' => true])
                     @else
                         <p class="text-muted">{{ trans('entities.profile_not_created_books', ['userName' => $user->name]) }}</p>
                     @endif
                         @endif
                     </h2>
                     @if (count($recentlyCreated['shelves']) > 0)
-                        @include('partials.entity-list', ['entities' => $recentlyCreated['shelves'], 'showPath' => true])
+                        @include('entities.list', ['entities' => $recentlyCreated['shelves'], 'showPath' => true])
                     @else
                         <p class="text-muted">{{ trans('entities.profile_not_created_shelves', ['userName' => $user->name]) }}</p>
                     @endif
index a6ed0c8f110702c007db4feceb8075a5f1ab6971..83a411219833ae3daa9048cb21e88ba1d539f29e 100644 (file)
@@ -5,7 +5,6 @@
  * Routes have a uri prefix of /api/.
  * Controllers are all within app/Http/Controllers/Api.
  */
-Route::get('docs', 'ApiDocsController@display');
 Route::get('docs.json', 'ApiDocsController@json');
 
 Route::get('books', 'BookApiController@list');
index bc9705e10107b22080834ba302ebe67a6ce6fb43..08adeceb947318a9f017f55424e4b5e91e09ccb7 100644 (file)
@@ -1,7 +1,7 @@
 <?php
 
 Route::get('/status', 'StatusController@show');
-Route::get('/robots.txt', 'HomeController@getRobots');
+Route::get('/robots.txt', 'HomeController@robots');
 
 // Authenticated routes...
 Route::group(['middleware' => 'auth'], function () {
@@ -10,6 +10,9 @@ Route::group(['middleware' => 'auth'], function () {
     Route::get('/uploads/images/{path}', 'Images\ImageController@showImage')
         ->where('path', '.*$');
 
+    // API docs routes
+    Route::get('/api/docs', 'Api\ApiDocsController@display');
+
     Route::get('/pages/recently-updated', 'PageController@showRecentlyUpdated');
 
     // Shelves
@@ -225,12 +228,25 @@ Route::group(['middleware' => 'auth'], function () {
     });
 });
 
+// MFA routes
+Route::group(['middleware' => 'mfa-setup'], function () {
+    Route::get('/mfa/setup', 'Auth\MfaController@setup');
+    Route::get('/mfa/totp/generate', 'Auth\MfaTotpController@generate');
+    Route::post('/mfa/totp/confirm', 'Auth\MfaTotpController@confirm');
+    Route::get('/mfa/backup_codes/generate', 'Auth\MfaBackupCodesController@generate');
+    Route::post('/mfa/backup_codes/confirm', 'Auth\MfaBackupCodesController@confirm');
+});
+Route::group(['middleware' => 'guest'], function () {
+    Route::get('/mfa/verify', 'Auth\MfaController@verify');
+    Route::post('/mfa/totp/verify', 'Auth\MfaTotpController@verify');
+    Route::post('/mfa/backup_codes/verify', 'Auth\MfaBackupCodesController@verify');
+});
+Route::delete('/mfa/{method}/remove', 'Auth\MfaController@remove')->middleware('auth');
+
 // Social auth routes
 Route::get('/login/service/{socialDriver}', 'Auth\SocialController@login');
 Route::get('/login/service/{socialDriver}/callback', 'Auth\SocialController@callback');
-Route::group(['middleware' => 'auth'], function () {
-    Route::post('/login/service/{socialDriver}/detach', 'Auth\SocialController@detach');
-});
+Route::post('/login/service/{socialDriver}/detach', 'Auth\SocialController@detach')->middleware('auth');
 Route::get('/register/service/{socialDriver}', 'Auth\SocialController@register');
 
 // Login/Logout routes
@@ -263,4 +279,4 @@ Route::post('/password/email', 'Auth\ForgotPasswordController@sendResetLinkEmail
 Route::get('/password/reset/{token}', 'Auth\ResetPasswordController@showResetForm');
 Route::post('/password/reset', 'Auth\ResetPasswordController@reset');
 
-Route::fallback('HomeController@getNotFound')->name('fallback');
+Route::fallback('HomeController@notFound')->name('fallback');
diff --git a/tests/ActivityTrackingTest.php b/tests/ActivityTrackingTest.php
deleted file mode 100644 (file)
index 494a1f5..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-<?php
-
-namespace Tests;
-
-use BookStack\Entities\Models\Book;
-
-class ActivityTrackingTest extends BrowserKitTest
-{
-    public function test_recently_viewed_books()
-    {
-        $books = Book::all()->take(10);
-
-        $this->asAdmin()->visit('/books')
-            ->dontSeeInElement('#recents', $books[0]->name)
-            ->dontSeeInElement('#recents', $books[1]->name)
-            ->visit($books[0]->getUrl())
-            ->visit($books[1]->getUrl())
-            ->visit('/books')
-            ->seeInElement('#recents', $books[0]->name)
-            ->seeInElement('#recents', $books[1]->name);
-    }
-
-    public function test_popular_books()
-    {
-        $books = Book::all()->take(10);
-
-        $this->asAdmin()->visit('/books')
-            ->dontSeeInElement('#popular', $books[0]->name)
-            ->dontSeeInElement('#popular', $books[1]->name)
-            ->visit($books[0]->getUrl())
-            ->visit($books[1]->getUrl())
-            ->visit($books[0]->getUrl())
-            ->visit('/books')
-            ->seeInNthElement('#popular .book', 0, $books[0]->name)
-            ->seeInNthElement('#popular .book', 1, $books[1]->name);
-    }
-}
index 90d107eb34aa7b170d73fe2999014c7789248176..062adce5376821a8b1914b734039c107edf6fa2b 100644 (file)
@@ -2,7 +2,6 @@
 
 namespace Tests\Api;
 
-use BookStack\Auth\User;
 use Tests\TestCase;
 
 class ApiDocsTest extends TestCase
@@ -11,16 +10,6 @@ class ApiDocsTest extends TestCase
 
     protected $endpoint = '/api/docs';
 
-    public function test_docs_page_not_visible_to_normal_viewers()
-    {
-        $viewer = $this->getViewer();
-        $resp = $this->actingAs($viewer)->get($this->endpoint);
-        $resp->assertStatus(403);
-
-        $resp = $this->actingAsApiEditor()->get($this->endpoint);
-        $resp->assertStatus(200);
-    }
-
     public function test_docs_page_returns_view_with_docs_content()
     {
         $resp = $this->actingAsApiEditor()->get($this->endpoint);
@@ -42,19 +31,4 @@ class ApiDocsTest extends TestCase
             ]],
         ]);
     }
-
-    public function test_docs_page_visible_by_public_user_if_given_permission()
-    {
-        $this->setSettings(['app-public' => true]);
-        $guest = User::getDefault();
-
-        $this->startSession();
-        $resp = $this->get('/api/docs');
-        $resp->assertStatus(403);
-
-        $this->giveUserPermissions($guest, ['access-api']);
-
-        $resp = $this->get('/api/docs');
-        $resp->assertStatus(200);
-    }
 }
index 279c7ad9a73b75b0617e3045b1315b1ee3ad837f..91e2db9e52de5c4cf7bddd0df659e6cdc79c42c8 100644 (file)
@@ -155,4 +155,17 @@ class BooksApiTest extends TestCase
         $resp->assertSee('# ' . $book->pages()->first()->name);
         $resp->assertSee('# ' . $book->chapters()->first()->name);
     }
+
+    public function test_cant_export_when_not_have_permission()
+    {
+        $types = ['html', 'plaintext', 'pdf', 'markdown'];
+        $this->actingAsApiEditor();
+        $this->removePermissionFromUser($this->getEditor(), 'content-export');
+
+        $book = Book::visible()->first();
+        foreach ($types as $type) {
+            $resp = $this->get($this->baseEndpoint . "/{$book->id}/export/{$type}");
+            $this->assertPermissionError($resp);
+        }
+    }
 }
index b3dd0ae6b408ecb20badca3dcb552b193f8da3eb..c9ed1a2892e19a715dd344e4a4ac8e6b5302b41a 100644 (file)
@@ -200,4 +200,17 @@ class ChaptersApiTest extends TestCase
         $resp->assertSee('# ' . $chapter->name);
         $resp->assertSee('# ' . $chapter->pages()->first()->name);
     }
+
+    public function test_cant_export_when_not_have_permission()
+    {
+        $types = ['html', 'plaintext', 'pdf', 'markdown'];
+        $this->actingAsApiEditor();
+        $this->removePermissionFromUser($this->getEditor(), 'content-export');
+
+        $chapter = Chapter::visible()->has('pages')->first();
+        foreach ($types as $type) {
+            $resp = $this->get($this->baseEndpoint . "/{$chapter->id}/export/{$type}");
+            $this->assertPermissionError($resp);
+        }
+    }
 }
index d52c6b513892374d955a5c758ad8ca5a46562837..4eb109d9dec3acf35653740aa51f5af714d122c4 100644 (file)
@@ -219,6 +219,27 @@ class PagesApiTest extends TestCase
         $resp->assertStatus(403);
     }
 
+    public function test_update_endpoint_does_not_wipe_content_if_no_html_or_md_provided()
+    {
+        $this->actingAsApiEditor();
+        $page = Page::visible()->first();
+        $originalContent = $page->html;
+        $details = [
+            'name' => 'My updated API page',
+            'tags' => [
+                [
+                    'name'  => 'freshtag',
+                    'value' => 'freshtagval',
+                ],
+            ],
+        ];
+
+        $this->putJson($this->baseEndpoint . "/{$page->id}", $details);
+        $page->refresh();
+
+        $this->assertEquals($originalContent, $page->html);
+    }
+
     public function test_delete_endpoint()
     {
         $this->actingAsApiEditor();
@@ -271,4 +292,17 @@ class PagesApiTest extends TestCase
         $resp->assertSee('# ' . $page->name);
         $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.md"');
     }
+
+    public function test_cant_export_when_not_have_permission()
+    {
+        $types = ['html', 'plaintext', 'pdf', 'markdown'];
+        $this->actingAsApiEditor();
+        $this->removePermissionFromUser($this->getEditor(), 'content-export');
+
+        $page = Page::visible()->first();
+        foreach ($types as $type) {
+            $resp = $this->get($this->baseEndpoint . "/{$page->id}/export/{$type}");
+            $this->assertPermissionError($resp);
+        }
+    }
 }
index bc36a184d129f3465a9e9fc855dfcabd21b98d5d..8d13670ca95e85a084a1269d178a9dd088a259bc 100644 (file)
@@ -140,4 +140,53 @@ class AuditLogTest extends TestCase
         $resp->assertSeeText($chapter->name);
         $resp->assertDontSeeText($page->name);
     }
+
+    public function test_ip_address_logged_and_visible()
+    {
+        config()->set('app.proxies', '*');
+        $editor = $this->getEditor();
+        /** @var Page $page */
+        $page = Page::query()->first();
+
+        $this->actingAs($editor)->put($page->getUrl(), [
+            'name' => 'Updated page',
+            'html' => '<p>Updated content</p>',
+        ], [
+            'X-Forwarded-For' => '192.123.45.1',
+        ])->assertRedirect($page->refresh()->getUrl());
+
+        $this->assertDatabaseHas('activities', [
+            'type'      => ActivityType::PAGE_UPDATE,
+            'ip'        => '192.123.45.1',
+            'user_id'   => $editor->id,
+            'entity_id' => $page->id,
+        ]);
+
+        $resp = $this->asAdmin()->get('/settings/audit');
+        $resp->assertSee('192.123.45.1');
+    }
+
+    public function test_ip_address_not_logged_in_demo_mode()
+    {
+        config()->set('app.proxies', '*');
+        config()->set('app.env', 'demo');
+        $editor = $this->getEditor();
+        /** @var Page $page */
+        $page = Page::query()->first();
+
+        $this->actingAs($editor)->put($page->getUrl(), [
+            'name' => 'Updated page',
+            'html' => '<p>Updated content</p>',
+        ], [
+            'X-Forwarded-For' => '192.123.45.1',
+            'REMOTE_ADDR'     => '192.123.45.2',
+        ])->assertRedirect($page->refresh()->getUrl());
+
+        $this->assertDatabaseHas('activities', [
+            'type'      => ActivityType::PAGE_UPDATE,
+            'ip'        => '127.0.0.1',
+            'user_id'   => $editor->id,
+            'entity_id' => $page->id,
+        ]);
+    }
 }
index febf583998d460e10440aa0ec36a2852eaa01641..d037b57011fada64a1c1755ab9418b9ea03af828 100644 (file)
@@ -2,49 +2,42 @@
 
 namespace Tests\Auth;
 
-use BookStack\Auth\Role;
+use BookStack\Auth\Access\Mfa\MfaSession;
 use BookStack\Auth\User;
 use BookStack\Entities\Models\Page;
 use BookStack\Notifications\ConfirmEmail;
 use BookStack\Notifications\ResetPassword;
-use BookStack\Settings\SettingService;
-use DB;
-use Hash;
+use Illuminate\Support\Facades\DB;
 use Illuminate\Support\Facades\Notification;
-use Illuminate\Support\Str;
-use Tests\BrowserKitTest;
+use Tests\TestCase;
+use Tests\TestResponse;
 
-class AuthTest extends BrowserKitTest
+class AuthTest extends TestCase
 {
     public function test_auth_working()
     {
-        $this->visit('/')
-            ->seePageIs('/login');
+        $this->get('/')->assertRedirect('/login');
     }
 
     public function test_login()
     {
-        $this->login('[email protected]', 'password')
-            ->seePageIs('/');
+        $this->login('[email protected]', 'password')->assertRedirect('/');
     }
 
     public function test_public_viewing()
     {
-        $settings = app(SettingService::class);
-        $settings->put('app-public', 'true');
-        $this->visit('/')
-            ->seePageIs('/')
-            ->see('Log In');
+        $this->setSettings(['app-public' => 'true']);
+        $this->get('/')
+            ->assertOk()
+            ->assertSee('Log in');
     }
 
     public function test_registration_showing()
     {
         // Ensure registration form is showing
         $this->setSettings(['registration-enabled' => 'true']);
-        $this->visit('/login')
-            ->see('Sign up')
-            ->click('Sign up')
-            ->seePageIs('/register');
+        $this->get('/login')
+            ->assertElementContains('a[href="' . url('/register') . '"]', 'Sign up');
     }
 
     public function test_normal_registration()
@@ -54,15 +47,17 @@ class AuthTest extends BrowserKitTest
         $user = factory(User::class)->make();
 
         // Test form and ensure user is created
-        $this->visit('/register')
-            ->see('Sign Up')
-            ->type($user->name, '#name')
-            ->type($user->email, '#email')
-            ->type($user->password, '#password')
-            ->press('Create Account')
-            ->seePageIs('/')
-            ->see($user->name)
-            ->seeInDatabase('users', ['name' => $user->name, 'email' => $user->email]);
+        $this->get('/register')
+            ->assertSee('Sign Up')
+            ->assertElementContains('form[action="' . url('/register') . '"]', 'Create Account');
+
+        $resp = $this->post('/register', $user->only('password', 'name', 'email'));
+        $resp->assertRedirect('/');
+
+        $resp = $this->get('/');
+        $resp->assertOk();
+        $resp->assertSee($user->name);
+        $this->assertDatabaseHas('users', ['name' => $user->name, 'email' => $user->email]);
     }
 
     public function test_empty_registration_redirects_back_with_errors()
@@ -71,36 +66,33 @@ class AuthTest extends BrowserKitTest
         $this->setSettings(['registration-enabled' => 'true']);
 
         // Test form and ensure user is created
-        $this->visit('/register')
-            ->press('Create Account')
-            ->see('The name field is required')
-            ->seePageIs('/register');
+        $this->get('/register');
+        $this->post('/register', [])->assertRedirect('/register');
+        $this->get('/register')->assertSee('The name field is required');
     }
 
     public function test_registration_validation()
     {
         $this->setSettings(['registration-enabled' => 'true']);
 
-        $this->visit('/register')
-            ->type('1', '#name')
-            ->type('1', '#email')
-            ->type('1', '#password')
-            ->press('Create Account')
-            ->see('The name must be at least 2 characters.')
-            ->see('The email must be a valid email address.')
-            ->see('The password must be at least 8 characters.')
-            ->seePageIs('/register');
+        $this->get('/register');
+        $resp = $this->followingRedirects()->post('/register', [
+            'name'     => '1',
+            'email'    => '1',
+            'password' => '1',
+        ]);
+        $resp->assertSee('The name must be at least 2 characters.');
+        $resp->assertSee('The email must be a valid email address.');
+        $resp->assertSee('The password must be at least 8 characters.');
     }
 
     public function test_sign_up_link_on_login()
     {
-        $this->visit('/login')
-            ->dontSee('Sign up');
+        $this->get('/login')->assertDontSee('Sign up');
 
         $this->setSettings(['registration-enabled' => 'true']);
 
-        $this->visit('/login')
-            ->see('Sign up');
+        $this->get('/login')->assertSee('Sign up');
     }
 
     public function test_confirmed_registration()
@@ -113,26 +105,24 @@ class AuthTest extends BrowserKitTest
         $user = factory(User::class)->make();
 
         // Go through registration process
-        $this->visit('/register')
-            ->see('Sign Up')
-            ->type($user->name, '#name')
-            ->type($user->email, '#email')
-            ->type($user->password, '#password')
-            ->press('Create Account')
-            ->seePageIs('/register/confirm')
-            ->seeInDatabase('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]);
+        $resp = $this->post('/register', $user->only('name', 'email', 'password'));
+        $resp->assertRedirect('/register/confirm');
+        $this->assertDatabaseHas('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]);
 
         // Ensure notification sent
-        $dbUser = User::where('email', '=', $user->email)->first();
+        /** @var User $dbUser */
+        $dbUser = User::query()->where('email', '=', $user->email)->first();
         Notification::assertSentTo($dbUser, ConfirmEmail::class);
 
         // Test access and resend confirmation email
-        $this->login($user->email, $user->password)
-            ->seePageIs('/register/confirm/awaiting')
-            ->see('Resend')
-            ->visit('/books')
-            ->seePageIs('/register/confirm/awaiting')
-            ->press('Resend Confirmation Email');
+        $resp = $this->login($user->email, $user->password);
+        $resp->assertRedirect('/register/confirm/awaiting');
+
+        $resp = $this->get('/register/confirm/awaiting');
+        $resp->assertElementContains('form[action="' . url('/register/confirm/resend') . '"]', 'Resend');
+
+        $this->get('/books')->assertRedirect('/login');
+        $this->post('/register/confirm/resend', $user->only('email'));
 
         // Get confirmation and confirm notification matches
         $emailConfirmation = DB::table('email_confirmations')->where('user_id', '=', $dbUser->id)->first();
@@ -141,262 +131,166 @@ class AuthTest extends BrowserKitTest
         });
 
         // Check confirmation email confirmation activation.
-        $this->visit('/register/confirm/' . $emailConfirmation->token)
-            ->seePageIs('/')
-            ->see($user->name)
-            ->notSeeInDatabase('email_confirmations', ['token' => $emailConfirmation->token])
-            ->seeInDatabase('users', ['name' => $dbUser->name, 'email' => $dbUser->email, 'email_confirmed' => true]);
+        $this->get('/register/confirm/' . $emailConfirmation->token)->assertRedirect('/');
+        $this->get('/')->assertSee($user->name);
+        $this->assertDatabaseMissing('email_confirmations', ['token' => $emailConfirmation->token]);
+        $this->assertDatabaseHas('users', ['name' => $dbUser->name, 'email' => $dbUser->email, 'email_confirmed' => true]);
     }
 
     public function test_restricted_registration()
     {
         $this->setSettings(['registration-enabled' => 'true', 'registration-confirmation' => 'true', 'registration-restrict' => 'example.com']);
         $user = factory(User::class)->make();
+
         // Go through registration process
-        $this->visit('/register')
-            ->type($user->name, '#name')
-            ->type($user->email, '#email')
-            ->type($user->password, '#password')
-            ->press('Create Account')
-            ->seePageIs('/register')
-            ->dontSeeInDatabase('users', ['email' => $user->email])
-            ->see('That email domain does not have access to this application');
+        $this->post('/register', $user->only('name', 'email', 'password'))
+            ->assertRedirect('/register');
+        $resp = $this->get('/register');
+        $resp->assertSee('That email domain does not have access to this application');
+        $this->assertDatabaseMissing('users', $user->only('email'));
 
         $user->email = '[email protected]';
 
-        $this->visit('/register')
-            ->type($user->name, '#name')
-            ->type($user->email, '#email')
-            ->type($user->password, '#password')
-            ->press('Create Account')
-            ->seePageIs('/register/confirm')
-            ->seeInDatabase('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]);
-
-        $this->visit('/')
-            ->seePageIs('/register/confirm/awaiting');
-
-        auth()->logout();
-
-        $this->visit('/')->seePageIs('/login')
-            ->type($user->email, '#email')
-            ->type($user->password, '#password')
-            ->press('Log In')
-            ->seePageIs('/register/confirm/awaiting')
-            ->seeText('Email Address Not Confirmed');
+        $this->post('/register', $user->only('name', 'email', 'password'))
+            ->assertRedirect('/register/confirm');
+        $this->assertDatabaseHas('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]);
+
+        $this->assertNull(auth()->user());
+
+        $this->get('/')->assertRedirect('/login');
+        $resp = $this->followingRedirects()->post('/login', $user->only('email', 'password'));
+        $resp->assertSee('Email Address Not Confirmed');
+        $this->assertNull(auth()->user());
     }
 
     public function test_restricted_registration_with_confirmation_disabled()
     {
         $this->setSettings(['registration-enabled' => 'true', 'registration-confirmation' => 'false', 'registration-restrict' => 'example.com']);
         $user = factory(User::class)->make();
+
         // Go through registration process
-        $this->visit('/register')
-            ->type($user->name, '#name')
-            ->type($user->email, '#email')
-            ->type($user->password, '#password')
-            ->press('Create Account')
-            ->seePageIs('/register')
-            ->dontSeeInDatabase('users', ['email' => $user->email])
-            ->see('That email domain does not have access to this application');
+        $this->post('/register', $user->only('name', 'email', 'password'))
+            ->assertRedirect('/register');
+        $this->assertDatabaseMissing('users', $user->only('email'));
+        $this->get('/register')->assertSee('That email domain does not have access to this application');
 
         $user->email = '[email protected]';
 
-        $this->visit('/register')
-            ->type($user->name, '#name')
-            ->type($user->email, '#email')
-            ->type($user->password, '#password')
-            ->press('Create Account')
-            ->seePageIs('/register/confirm')
-            ->seeInDatabase('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]);
-
-        $this->visit('/')
-            ->seePageIs('/register/confirm/awaiting');
-
-        auth()->logout();
-        $this->visit('/')->seePageIs('/login')
-            ->type($user->email, '#email')
-            ->type($user->password, '#password')
-            ->press('Log In')
-            ->seePageIs('/register/confirm/awaiting')
-            ->seeText('Email Address Not Confirmed');
-    }
+        $this->post('/register', $user->only('name', 'email', 'password'))
+            ->assertRedirect('/register/confirm');
+        $this->assertDatabaseHas('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]);
 
-    public function test_user_creation()
-    {
-        /** @var User $user */
-        $user = factory(User::class)->make();
-        $adminRole = Role::getRole('admin');
-
-        $this->asAdmin()
-            ->visit('/settings/users')
-            ->click('Add New User')
-            ->type($user->name, '#name')
-            ->type($user->email, '#email')
-            ->check("roles[{$adminRole->id}]")
-            ->type($user->password, '#password')
-            ->type($user->password, '#password-confirm')
-            ->press('Save')
-            ->seePageIs('/settings/users')
-            ->seeInDatabase('users', $user->only(['name', 'email']))
-            ->see($user->name);
-
-        $user->refresh();
-        $this->assertStringStartsWith(Str::slug($user->name), $user->slug);
-    }
+        $this->assertNull(auth()->user());
 
-    public function test_user_updating()
-    {
-        $user = $this->getNormalUser();
-        $password = $user->password;
-        $this->asAdmin()
-            ->visit('/settings/users')
-            ->click($user->name)
-            ->seePageIs('/settings/users/' . $user->id)
-            ->see($user->email)
-            ->type('Barry Scott', '#name')
-            ->press('Save')
-            ->seePageIs('/settings/users')
-            ->seeInDatabase('users', ['id' => $user->id, 'name' => 'Barry Scott', 'password' => $password])
-            ->notSeeInDatabase('users', ['name' => $user->name]);
-
-        $user->refresh();
-        $this->assertStringStartsWith(Str::slug($user->name), $user->slug);
+        $this->get('/')->assertRedirect('/login');
+        $resp = $this->post('/login', $user->only('email', 'password'));
+        $resp->assertRedirect('/register/confirm/awaiting');
+        $this->get('/register/confirm/awaiting')->assertSee('Email Address Not Confirmed');
+        $this->assertNull(auth()->user());
     }
 
-    public function test_user_password_update()
+    public function test_logout()
     {
-        $user = $this->getNormalUser();
-        $userProfilePage = '/settings/users/' . $user->id;
-        $this->asAdmin()
-            ->visit($userProfilePage)
-            ->type('newpassword', '#password')
-            ->press('Save')
-            ->seePageIs($userProfilePage)
-            ->see('Password confirmation required')
-
-            ->type('newpassword', '#password')
-            ->type('newpassword', '#password-confirm')
-            ->press('Save')
-            ->seePageIs('/settings/users');
-
-        $userPassword = User::find($user->id)->password;
-        $this->assertTrue(Hash::check('newpassword', $userPassword));
+        $this->asAdmin()->get('/')->assertOk();
+        $this->get('/logout')->assertRedirect('/');
+        $this->get('/')->assertRedirect('/login');
     }
 
-    public function test_user_deletion()
+    public function test_mfa_session_cleared_on_logout()
     {
-        $userDetails = factory(User::class)->make();
-        $user = $this->getEditor($userDetails->toArray());
-
-        $this->asAdmin()
-            ->visit('/settings/users/' . $user->id)
-            ->click('Delete User')
-            ->see($user->name)
-            ->press('Confirm')
-            ->seePageIs('/settings/users')
-            ->notSeeInDatabase('users', ['name' => $user->name]);
-    }
+        $user = $this->getEditor();
+        $mfaSession = $this->app->make(MfaSession::class);
 
-    public function test_user_cannot_be_deleted_if_last_admin()
-    {
-        $adminRole = Role::getRole('admin');
-
-        // Delete all but one admin user if there are more than one
-        $adminUsers = $adminRole->users;
-        if (count($adminUsers) > 1) {
-            foreach ($adminUsers->splice(1) as $user) {
-                $user->delete();
-            }
-        }
-
-        // Ensure we currently only have 1 admin user
-        $this->assertEquals(1, $adminRole->users()->count());
-        $user = $adminRole->users->first();
-
-        $this->asAdmin()->visit('/settings/users/' . $user->id)
-            ->click('Delete User')
-            ->press('Confirm')
-            ->seePageIs('/settings/users/' . $user->id)
-            ->see('You cannot delete the only admin');
-    }
+        $mfaSession->markVerifiedForUser($user);
+        $this->assertTrue($mfaSession->isVerifiedForUser($user));
 
-    public function test_logout()
-    {
-        $this->asAdmin()
-            ->visit('/')
-            ->seePageIs('/')
-            ->visit('/logout')
-            ->visit('/')
-            ->seePageIs('/login');
+        $this->asAdmin()->get('/logout');
+        $this->assertFalse($mfaSession->isVerifiedForUser($user));
     }
 
     public function test_reset_password_flow()
     {
         Notification::fake();
 
-        $this->visit('/login')->click('Forgot Password?')
-            ->seePageIs('/password/email')
-            ->type('[email protected]', 'email')
-            ->press('Send Reset Link')
-            ->see('A password reset link will be sent to [email protected] if that email address is found in the system.');
+        $this->get('/login')
+            ->assertElementContains('a[href="' . url('/password/email') . '"]', 'Forgot Password?');
 
-        $this->seeInDatabase('password_resets', [
+        $this->get('/password/email')
+            ->assertElementContains('form[action="' . url('/password/email') . '"]', 'Send Reset Link');
+
+        $resp = $this->post('/password/email', [
+            'email' => '[email protected]',
+        ]);
+        $resp->assertRedirect('/password/email');
+
+        $resp = $this->get('/password/email');
+        $resp->assertSee('A password reset link will be sent to [email protected] if that email address is found in the system.');
+
+        $this->assertDatabaseHas('password_resets', [
             'email' => '[email protected]',
         ]);
 
-        $user = User::where('email', '=', '[email protected]')->first();
+        /** @var User $user */
+        $user = User::query()->where('email', '=', '[email protected]')->first();
 
         Notification::assertSentTo($user, ResetPassword::class);
         $n = Notification::sent($user, ResetPassword::class);
 
-        $this->visit('/password/reset/' . $n->first()->token)
-            ->see('Reset Password')
-            ->submitForm('Reset Password', [
-                'email'                 => '[email protected]',
-                'password'              => 'randompass',
-                'password_confirmation' => 'randompass',
-            ])->seePageIs('/')
-            ->see('Your password has been successfully reset');
+        $this->get('/password/reset/' . $n->first()->token)
+            ->assertOk()
+            ->assertSee('Reset Password');
+
+        $resp = $this->post('/password/reset', [
+            'email'                 => '[email protected]',
+            'password'              => 'randompass',
+            'password_confirmation' => 'randompass',
+            'token'                 => $n->first()->token,
+        ]);
+        $resp->assertRedirect('/');
+
+        $this->get('/')->assertSee('Your password has been successfully reset');
     }
 
     public function test_reset_password_flow_shows_success_message_even_if_wrong_password_to_prevent_user_discovery()
     {
-        $this->visit('/login')->click('Forgot Password?')
-            ->seePageIs('/password/email')
-            ->type('[email protected]', 'email')
-            ->press('Send Reset Link')
-            ->see('A password reset link will be sent to [email protected] if that email address is found in the system.')
-            ->dontSee('We can\'t find a user');
-
-        $this->visit('/password/reset/arandometokenvalue')
-            ->see('Reset Password')
-            ->submitForm('Reset Password', [
-                'email'                 => '[email protected]',
-                'password'              => 'randompass',
-                'password_confirmation' => 'randompass',
-            ])->followRedirects()
-            ->seePageIs('/password/reset/arandometokenvalue')
-            ->dontSee('We can\'t find a user')
-            ->see('The password reset token is invalid for this email address.');
+        $this->get('/password/email');
+        $resp = $this->followingRedirects()->post('/password/email', [
+            'email' => '[email protected]',
+        ]);
+        $resp->assertSee('A password reset link will be sent to [email protected] if that email address is found in the system.');
+        $resp->assertDontSee('We can\'t find a user');
+
+        $this->get('/password/reset/arandometokenvalue')->assertSee('Reset Password');
+        $resp = $this->post('/password/reset', [
+            'email'                 => '[email protected]',
+            'password'              => 'randompass',
+            'password_confirmation' => 'randompass',
+            'token'                 => 'arandometokenvalue',
+        ]);
+        $resp->assertRedirect('/password/reset/arandometokenvalue');
+
+        $this->get('/password/reset/arandometokenvalue')
+            ->assertDontSee('We can\'t find a user')
+            ->assertSee('The password reset token is invalid for this email address.');
     }
 
     public function test_reset_password_page_shows_sign_links()
     {
         $this->setSettings(['registration-enabled' => 'true']);
-        $this->visit('/password/email')
-            ->seeLink('Log in')
-            ->seeLink('Sign up');
+        $this->get('/password/email')
+            ->assertElementContains('a', 'Log in')
+            ->assertElementContains('a', 'Sign up');
     }
 
     public function test_login_redirects_to_initially_requested_url_correctly()
     {
         config()->set('app.url', 'http://localhost');
+        /** @var Page $page */
         $page = Page::query()->first();
 
-        $this->visit($page->getUrl())
-            ->seePageUrlIs(url('/login'));
+        $this->get($page->getUrl())->assertRedirect(url('/login'));
         $this->login('[email protected]', 'password')
-            ->seePageUrlIs($page->getUrl());
+            ->assertRedirect($page->getUrl());
     }
 
     public function test_login_intended_redirect_does_not_redirect_to_external_pages()
@@ -407,7 +301,15 @@ class AuthTest extends BrowserKitTest
         $this->get('/login', ['referer' => 'https://example.com']);
         $login = $this->post('/login', ['email' => '[email protected]', 'password' => 'password']);
 
-        $login->assertRedirectedTo('http://localhost');
+        $login->assertRedirect('http://localhost');
+    }
+
+    public function test_login_intended_redirect_does_not_factor_mfa_routes()
+    {
+        $this->get('/books')->assertRedirect('/login');
+        $this->get('/mfa/setup')->assertRedirect('/login');
+        $login = $this->post('/login', ['email' => '[email protected]', 'password' => 'password']);
+        $login->assertRedirect('/books');
     }
 
     public function test_login_authenticates_admins_on_all_guards()
@@ -442,14 +344,25 @@ class AuthTest extends BrowserKitTest
         $this->assertFalse($log->hasWarningThatContains('Failed login for [email protected]'));
     }
 
+    public function test_logged_in_user_with_unconfirmed_email_is_logged_out()
+    {
+        $this->setSettings(['registration-confirmation' => 'true']);
+        $user = $this->getEditor();
+        $user->email_confirmed = false;
+        $user->save();
+
+        auth()->login($user);
+        $this->assertTrue(auth()->check());
+
+        $this->get('/books')->assertRedirect('/');
+        $this->assertFalse(auth()->check());
+    }
+
     /**
      * Perform a login.
      */
-    protected function login(string $email, string $password): AuthTest
+    protected function login(string $email, string $password): TestResponse
     {
-        return $this->visit('/login')
-            ->type($email, '#email')
-            ->type($password, '#password')
-            ->press('Log In');
+        return $this->post('/login', compact('email', 'password'));
     }
 }
index 9d491fd0f75b641f5b6978eadb266a7881e38372..9e0729a8e9fc4d278f83b2db67daafc449dd83a6 100644 (file)
@@ -651,9 +651,9 @@ class LdapTest extends TestCase
             'services.ldap.remove_from_groups' => true,
         ]);
 
-        $this->commonLdapMocks(1, 1, 3, 4, 3, 2);
+        $this->commonLdapMocks(1, 1, 6, 8, 6, 4);
         $this->mockLdap->shouldReceive('searchAndGetEntries')
-            ->times(3)
+            ->times(6)
             ->andReturn(['count' => 1, 0 => [
                 'uid'      => [$user->name],
                 'cn'       => [$user->name],
@@ -665,7 +665,8 @@ class LdapTest extends TestCase
                 ],
             ]]);
 
-        $this->followingRedirects()->mockUserLogin()->assertSee('Thanks for registering!');
+        $login = $this->followingRedirects()->mockUserLogin();
+        $login->assertSee('Thanks for registering!');
         $this->assertDatabaseHas('users', [
             'email'           => $user->email,
             'email_confirmed' => false,
@@ -677,8 +678,13 @@ class LdapTest extends TestCase
             'role_id' => $roleToReceive->id,
         ]);
 
+        $this->assertNull(auth()->user());
+
         $homePage = $this->get('/');
-        $homePage->assertRedirect('/register/confirm/awaiting');
+        $homePage->assertRedirect('/login');
+
+        $login = $this->followingRedirects()->mockUserLogin();
+        $login->assertSee('Email Address Not Confirmed');
     }
 
     public function test_failed_logins_are_logged_when_message_configured()
diff --git a/tests/Auth/MfaConfigurationTest.php b/tests/Auth/MfaConfigurationTest.php
new file mode 100644 (file)
index 0000000..685aad8
--- /dev/null
@@ -0,0 +1,167 @@
+<?php
+
+namespace Tests\Auth;
+
+use BookStack\Actions\ActivityType;
+use BookStack\Auth\Access\Mfa\MfaValue;
+use BookStack\Auth\User;
+use PragmaRX\Google2FA\Google2FA;
+use Tests\TestCase;
+
+class MfaConfigurationTest extends TestCase
+{
+    public function test_totp_setup()
+    {
+        $editor = $this->getEditor();
+        $this->assertDatabaseMissing('mfa_values', ['user_id' => $editor->id]);
+
+        // Setup page state
+        $resp = $this->actingAs($editor)->get('/mfa/setup');
+        $resp->assertElementContains('a[href$="/mfa/totp/generate"]', 'Setup');
+
+        // Generate page access
+        $resp = $this->get('/mfa/totp/generate');
+        $resp->assertSee('Mobile App Setup');
+        $resp->assertSee('Verify Setup');
+        $resp->assertElementExists('form[action$="/mfa/totp/confirm"] button');
+        $this->assertSessionHas('mfa-setup-totp-secret');
+        $svg = $resp->getElementHtml('#main-content .card svg');
+
+        // Validation error, code should remain the same
+        $resp = $this->post('/mfa/totp/confirm', [
+            'code' => 'abc123',
+        ]);
+        $resp->assertRedirect('/mfa/totp/generate');
+        $resp = $this->followRedirects($resp);
+        $resp->assertSee('The provided code is not valid or has expired.');
+        $revisitSvg = $resp->getElementHtml('#main-content .card svg');
+        $this->assertTrue($svg === $revisitSvg);
+        $secret = decrypt(session()->get('mfa-setup-totp-secret'));
+
+        $resp->assertSee(htmlentities("?secret={$secret}&issuer=BookStack&algorithm=SHA1&digits=6&period=30"));
+
+        // Successful confirmation
+        $google2fa = new Google2FA();
+        $otp = $google2fa->getCurrentOtp($secret);
+        $resp = $this->post('/mfa/totp/confirm', [
+            'code' => $otp,
+        ]);
+        $resp->assertRedirect('/mfa/setup');
+
+        // Confirmation of setup
+        $resp = $this->followRedirects($resp);
+        $resp->assertSee('Multi-factor method successfully configured');
+        $resp->assertElementContains('a[href$="/mfa/totp/generate"]', 'Reconfigure');
+
+        $this->assertDatabaseHas('mfa_values', [
+            'user_id' => $editor->id,
+            'method'  => 'totp',
+        ]);
+        $this->assertFalse(session()->has('mfa-setup-totp-secret'));
+        $value = MfaValue::query()->where('user_id', '=', $editor->id)
+            ->where('method', '=', 'totp')->first();
+        $this->assertEquals($secret, decrypt($value->value));
+    }
+
+    public function test_backup_codes_setup()
+    {
+        $editor = $this->getEditor();
+        $this->assertDatabaseMissing('mfa_values', ['user_id' => $editor->id]);
+
+        // Setup page state
+        $resp = $this->actingAs($editor)->get('/mfa/setup');
+        $resp->assertElementContains('a[href$="/mfa/backup_codes/generate"]', 'Setup');
+
+        // Generate page access
+        $resp = $this->get('/mfa/backup_codes/generate');
+        $resp->assertSee('Backup Codes');
+        $resp->assertElementContains('form[action$="/mfa/backup_codes/confirm"]', 'Confirm and Enable');
+        $this->assertSessionHas('mfa-setup-backup-codes');
+        $codes = decrypt(session()->get('mfa-setup-backup-codes'));
+        // Check code format
+        $this->assertCount(16, $codes);
+        $this->assertEquals(16 * 11, strlen(implode('', $codes)));
+        // Check download link
+        $resp->assertSee(base64_encode(implode("\n\n", $codes)));
+
+        // Confirm submit
+        $resp = $this->post('/mfa/backup_codes/confirm');
+        $resp->assertRedirect('/mfa/setup');
+
+        // Confirmation of setup
+        $resp = $this->followRedirects($resp);
+        $resp->assertSee('Multi-factor method successfully configured');
+        $resp->assertElementContains('a[href$="/mfa/backup_codes/generate"]', 'Reconfigure');
+
+        $this->assertDatabaseHas('mfa_values', [
+            'user_id' => $editor->id,
+            'method'  => 'backup_codes',
+        ]);
+        $this->assertFalse(session()->has('mfa-setup-backup-codes'));
+        $value = MfaValue::query()->where('user_id', '=', $editor->id)
+            ->where('method', '=', 'backup_codes')->first();
+        $this->assertEquals($codes, json_decode(decrypt($value->value)));
+    }
+
+    public function test_backup_codes_cannot_be_confirmed_if_not_previously_generated()
+    {
+        $resp = $this->asEditor()->post('/mfa/backup_codes/confirm');
+        $resp->assertStatus(500);
+    }
+
+    public function test_mfa_method_count_is_visible_on_user_edit_page()
+    {
+        $user = $this->getEditor();
+        $resp = $this->actingAs($this->getAdmin())->get($user->getEditUrl());
+        $resp->assertSee('0 methods configured');
+
+        MfaValue::upsertWithValue($user, MfaValue::METHOD_TOTP, 'test');
+        $resp = $this->get($user->getEditUrl());
+        $resp->assertSee('1 method configured');
+
+        MfaValue::upsertWithValue($user, MfaValue::METHOD_BACKUP_CODES, 'test');
+        $resp = $this->get($user->getEditUrl());
+        $resp->assertSee('2 methods configured');
+    }
+
+    public function test_mfa_setup_link_only_shown_when_viewing_own_user_edit_page()
+    {
+        $admin = $this->getAdmin();
+        $resp = $this->actingAs($admin)->get($admin->getEditUrl());
+        $resp->assertElementExists('a[href$="/mfa/setup"]');
+
+        $resp = $this->actingAs($admin)->get($this->getEditor()->getEditUrl());
+        $resp->assertElementNotExists('a[href$="/mfa/setup"]');
+    }
+
+    public function test_mfa_indicator_shows_in_user_list()
+    {
+        $admin = $this->getAdmin();
+        User::query()->where('id', '!=', $admin->id)->delete();
+
+        $resp = $this->actingAs($admin)->get('/settings/users');
+        $resp->assertElementNotExists('[title="MFA Configured"] svg');
+
+        MfaValue::upsertWithValue($admin, MfaValue::METHOD_TOTP, 'test');
+        $resp = $this->actingAs($admin)->get('/settings/users');
+        $resp->assertElementExists('[title="MFA Configured"] svg');
+    }
+
+    public function test_remove_mfa_method()
+    {
+        $admin = $this->getAdmin();
+
+        MfaValue::upsertWithValue($admin, MfaValue::METHOD_TOTP, 'test');
+        $this->assertEquals(1, $admin->mfaValues()->count());
+        $resp = $this->actingAs($admin)->get('/mfa/setup');
+        $resp->assertElementExists('form[action$="/mfa/totp/remove"]');
+
+        $resp = $this->delete('/mfa/totp/remove');
+        $resp->assertRedirect('/mfa/setup');
+        $resp = $this->followRedirects($resp);
+        $resp->assertSee('Multi-factor method successfully removed');
+
+        $this->assertActivityExists(ActivityType::MFA_REMOVE_METHOD);
+        $this->assertEquals(0, $admin->mfaValues()->count());
+    }
+}
diff --git a/tests/Auth/MfaVerificationTest.php b/tests/Auth/MfaVerificationTest.php
new file mode 100644 (file)
index 0000000..ee6f3ec
--- /dev/null
@@ -0,0 +1,278 @@
+<?php
+
+namespace Tests\Auth;
+
+use BookStack\Auth\Access\LoginService;
+use BookStack\Auth\Access\Mfa\MfaValue;
+use BookStack\Auth\Access\Mfa\TotpService;
+use BookStack\Auth\Role;
+use BookStack\Auth\User;
+use BookStack\Exceptions\StoppedAuthenticationException;
+use Illuminate\Support\Facades\Hash;
+use PragmaRX\Google2FA\Google2FA;
+use Tests\TestCase;
+use Tests\TestResponse;
+
+class MfaVerificationTest extends TestCase
+{
+    public function test_totp_verification()
+    {
+        [$user, $secret, $loginResp] = $this->startTotpLogin();
+        $loginResp->assertRedirect('/mfa/verify');
+
+        $resp = $this->get('/mfa/verify');
+        $resp->assertSee('Verify Access');
+        $resp->assertSee('Enter the code, generated using your mobile app, below:');
+        $resp->assertElementExists('form[action$="/mfa/totp/verify"] input[name="code"]');
+
+        $google2fa = new Google2FA();
+        $resp = $this->post('/mfa/totp/verify', [
+            'code' => $google2fa->getCurrentOtp($secret),
+        ]);
+        $resp->assertRedirect('/');
+        $this->assertEquals($user->id, auth()->user()->id);
+    }
+
+    public function test_totp_verification_fails_on_missing_invalid_code()
+    {
+        [$user, $secret, $loginResp] = $this->startTotpLogin();
+
+        $resp = $this->get('/mfa/verify');
+        $resp = $this->post('/mfa/totp/verify', [
+            'code' => '',
+        ]);
+        $resp->assertRedirect('/mfa/verify');
+
+        $resp = $this->get('/mfa/verify');
+        $resp->assertSeeText('The code field is required.');
+        $this->assertNull(auth()->user());
+
+        $resp = $this->post('/mfa/totp/verify', [
+            'code' => '123321',
+        ]);
+        $resp->assertRedirect('/mfa/verify');
+        $resp = $this->get('/mfa/verify');
+
+        $resp->assertSeeText('The provided code is not valid or has expired.');
+        $this->assertNull(auth()->user());
+    }
+
+    public function test_backup_code_verification()
+    {
+        [$user, $codes, $loginResp] = $this->startBackupCodeLogin();
+        $loginResp->assertRedirect('/mfa/verify');
+
+        $resp = $this->get('/mfa/verify');
+        $resp->assertSee('Verify Access');
+        $resp->assertSee('Backup Code');
+        $resp->assertSee('Enter one of your remaining backup codes below:');
+        $resp->assertElementExists('form[action$="/mfa/backup_codes/verify"] input[name="code"]');
+
+        $resp = $this->post('/mfa/backup_codes/verify', [
+            'code' => $codes[1],
+        ]);
+
+        $resp->assertRedirect('/');
+        $this->assertEquals($user->id, auth()->user()->id);
+        // Ensure code no longer exists in available set
+        $userCodes = MfaValue::getValueForUser($user, MfaValue::METHOD_BACKUP_CODES);
+        $this->assertStringNotContainsString($codes[1], $userCodes);
+        $this->assertStringContainsString($codes[0], $userCodes);
+    }
+
+    public function test_backup_code_verification_fails_on_missing_or_invalid_code()
+    {
+        [$user, $codes, $loginResp] = $this->startBackupCodeLogin();
+
+        $resp = $this->get('/mfa/verify');
+        $resp = $this->post('/mfa/backup_codes/verify', [
+            'code' => '',
+        ]);
+        $resp->assertRedirect('/mfa/verify');
+
+        $resp = $this->get('/mfa/verify');
+        $resp->assertSeeText('The code field is required.');
+        $this->assertNull(auth()->user());
+
+        $resp = $this->post('/mfa/backup_codes/verify', [
+            'code' => 'ab123-ab456',
+        ]);
+        $resp->assertRedirect('/mfa/verify');
+
+        $resp = $this->get('/mfa/verify');
+        $resp->assertSeeText('The provided code is not valid or has already been used.');
+        $this->assertNull(auth()->user());
+    }
+
+    public function test_backup_code_verification_fails_on_attempted_code_reuse()
+    {
+        [$user, $codes, $loginResp] = $this->startBackupCodeLogin();
+
+        $this->post('/mfa/backup_codes/verify', [
+            'code' => $codes[0],
+        ]);
+        $this->assertNotNull(auth()->user());
+        auth()->logout();
+        session()->flush();
+
+        $this->post('/login', ['email' => $user->email, 'password' => 'password']);
+        $this->get('/mfa/verify');
+        $resp = $this->post('/mfa/backup_codes/verify', [
+            'code' => $codes[0],
+        ]);
+        $resp->assertRedirect('/mfa/verify');
+        $this->assertNull(auth()->user());
+
+        $resp = $this->get('/mfa/verify');
+        $resp->assertSeeText('The provided code is not valid or has already been used.');
+    }
+
+    public function test_backup_code_verification_shows_warning_when_limited_codes_remain()
+    {
+        [$user, $codes, $loginResp] = $this->startBackupCodeLogin(['abc12-def45', 'abc12-def46']);
+
+        $resp = $this->post('/mfa/backup_codes/verify', [
+            'code' => $codes[0],
+        ]);
+        $resp = $this->followRedirects($resp);
+        $resp->assertSeeText('You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.');
+    }
+
+    public function test_both_mfa_options_available_if_set_on_profile()
+    {
+        $user = $this->getEditor();
+        $user->password = Hash::make('password');
+        $user->save();
+
+        MfaValue::upsertWithValue($user, MfaValue::METHOD_TOTP, 'abc123');
+        MfaValue::upsertWithValue($user, MfaValue::METHOD_BACKUP_CODES, '["abc12-def456"]');
+
+        /** @var TestResponse $mfaView */
+        $mfaView = $this->followingRedirects()->post('/login', [
+            'email'    => $user->email,
+            'password' => 'password',
+        ]);
+
+        // Totp shown by default
+        $mfaView->assertElementExists('form[action$="/mfa/totp/verify"] input[name="code"]');
+        $mfaView->assertElementContains('a[href$="/mfa/verify?method=backup_codes"]', 'Verify using a backup code');
+
+        // Ensure can view backup_codes view
+        $resp = $this->get('/mfa/verify?method=backup_codes');
+        $resp->assertElementExists('form[action$="/mfa/backup_codes/verify"] input[name="code"]');
+        $resp->assertElementContains('a[href$="/mfa/verify?method=totp"]', 'Verify using a mobile app');
+    }
+
+    public function test_mfa_required_with_no_methods_leads_to_setup()
+    {
+        $user = $this->getEditor();
+        $user->password = Hash::make('password');
+        $user->save();
+        /** @var Role $role */
+        $role = $user->roles->first();
+        $role->mfa_enforced = true;
+        $role->save();
+
+        $this->assertDatabaseMissing('mfa_values', [
+            'user_id' => $user->id,
+        ]);
+
+        /** @var TestResponse $resp */
+        $resp = $this->followingRedirects()->post('/login', [
+            'email'    => $user->email,
+            'password' => 'password',
+        ]);
+
+        $resp->assertSeeText('No Methods Configured');
+        $resp->assertElementContains('a[href$="/mfa/setup"]', 'Configure');
+
+        $this->get('/mfa/backup_codes/generate');
+        $resp = $this->post('/mfa/backup_codes/confirm');
+        $resp->assertRedirect('/login');
+        $this->assertDatabaseHas('mfa_values', [
+            'user_id' => $user->id,
+        ]);
+
+        $resp = $this->get('/login');
+        $resp->assertSeeText('Multi-factor method configured, Please now login again using the configured method.');
+
+        $resp = $this->followingRedirects()->post('/login', [
+            'email'    => $user->email,
+            'password' => 'password',
+        ]);
+        $resp->assertSeeText('Enter one of your remaining backup codes below:');
+    }
+
+    public function test_mfa_setup_route_access()
+    {
+        $routes = [
+            ['get', '/mfa/setup'],
+            ['get', '/mfa/totp/generate'],
+            ['post', '/mfa/totp/confirm'],
+            ['get', '/mfa/backup_codes/generate'],
+            ['post', '/mfa/backup_codes/confirm'],
+        ];
+
+        // Non-auth access
+        foreach ($routes as [$method, $path]) {
+            $resp = $this->call($method, $path);
+            $resp->assertRedirect('/login');
+        }
+
+        // Attempted login user, who has configured mfa, access
+        // Sets up user that has MFA required after attempted login.
+        $loginService = $this->app->make(LoginService::class);
+        $user = $this->getEditor();
+        /** @var Role $role */
+        $role = $user->roles->first();
+        $role->mfa_enforced = true;
+        $role->save();
+
+        try {
+            $loginService->login($user, 'testing');
+        } catch (StoppedAuthenticationException $e) {
+        }
+        $this->assertNotNull($loginService->getLastLoginAttemptUser());
+
+        MfaValue::upsertWithValue($user, MfaValue::METHOD_BACKUP_CODES, '[]');
+        foreach ($routes as [$method, $path]) {
+            $resp = $this->call($method, $path);
+            $resp->assertRedirect('/login');
+        }
+    }
+
+    /**
+     * @return Array<User, string, TestResponse>
+     */
+    protected function startTotpLogin(): array
+    {
+        $secret = $this->app->make(TotpService::class)->generateSecret();
+        $user = $this->getEditor();
+        $user->password = Hash::make('password');
+        $user->save();
+        MfaValue::upsertWithValue($user, MfaValue::METHOD_TOTP, $secret);
+        $loginResp = $this->post('/login', [
+            'email'    => $user->email,
+            'password' => 'password',
+        ]);
+
+        return [$user, $secret, $loginResp];
+    }
+
+    /**
+     * @return Array<User, string, TestResponse>
+     */
+    protected function startBackupCodeLogin($codes = ['kzzu6-1pgll', 'bzxnf-plygd', 'bwdsp-ysl51', '1vo93-ioy7n', 'lf7nw-wdyka', 'xmtrd-oplac']): array
+    {
+        $user = $this->getEditor();
+        $user->password = Hash::make('password');
+        $user->save();
+        MfaValue::upsertWithValue($user, MfaValue::METHOD_BACKUP_CODES, json_encode($codes));
+        $loginResp = $this->post('/login', [
+            'email'    => $user->email,
+            'password' => 'password',
+        ]);
+
+        return [$user, $codes, $loginResp];
+    }
+}
index 096f862e7c94c2244477322b23b37702a54389b7..8ace3e2ee4f19dd9ea8fee11f606f2225f601277 100644 (file)
@@ -289,16 +289,18 @@ class Saml2Test extends TestCase
 
             $this->assertEquals('http://localhost/register/confirm', url()->current());
             $acsPost->assertSee('Please check your email and click the confirmation button to access BookStack.');
+            /** @var User $user */
             $user = User::query()->where('external_auth_id', '=', 'user')->first();
 
             $userRoleIds = $user->roles()->pluck('id');
             $this->assertContains($memberRole->id, $userRoleIds, 'User was assigned to member role');
             $this->assertContains($adminRole->id, $userRoleIds, 'User was assigned to admin role');
-            $this->assertTrue($user->email_confirmed == false, 'User email remains unconfirmed');
+            $this->assertFalse(boolval($user->email_confirmed), 'User email remains unconfirmed');
         });
 
+        $this->assertNull(auth()->user());
         $homeGet = $this->get('/');
-        $homeGet->assertRedirect('/register/confirm/awaiting');
+        $homeGet->assertRedirect('/login');
     }
 
     public function test_login_where_existing_non_saml_user_shows_warning()
index 5818cbb742bbe57c68292fef25ed59f0deacc473..f70263dd278ae1a8bf93efe26c896bda300d97e3 100644 (file)
@@ -2,9 +2,10 @@
 
 namespace Tests\Auth;
 
+use BookStack\Actions\ActivityType;
 use BookStack\Auth\SocialAccount;
 use BookStack\Auth\User;
-use DB;
+use Illuminate\Support\Facades\DB;
 use Laravel\Socialite\Contracts\Factory;
 use Laravel\Socialite\Contracts\Provider;
 use Mockery;
@@ -82,6 +83,7 @@ class SocialAuthTest extends TestCase
         ]);
         $resp = $this->followingRedirects()->get('/login/service/github/callback');
         $resp->assertDontSee('login-form');
+        $this->assertActivityExists(ActivityType::AUTH_LOGIN, null, 'github; (' . $this->getAdmin()->id . ') ' . $this->getAdmin()->name);
     }
 
     public function test_social_account_detach()
index c5c4b01af065ea607471c64562e554f8c51feed3..dcf9e23df9b829a10cfd175800d058247c1333fe 100644 (file)
@@ -6,9 +6,9 @@ use BookStack\Auth\Access\UserInviteService;
 use BookStack\Auth\User;
 use BookStack\Notifications\UserInvite;
 use Carbon\Carbon;
-use DB;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Notification;
 use Illuminate\Support\Str;
-use Notification;
 use Tests\TestCase;
 
 class UserInviteTest extends TestCase
diff --git a/tests/BrowserKitTest.php b/tests/BrowserKitTest.php
deleted file mode 100644 (file)
index 23eb108..0000000
+++ /dev/null
@@ -1,218 +0,0 @@
-<?php
-
-namespace Tests;
-
-use BookStack\Auth\Permissions\PermissionService;
-use BookStack\Auth\User;
-use BookStack\Entities\Models\Book;
-use BookStack\Entities\Models\Chapter;
-use BookStack\Entities\Models\Entity;
-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;
-
-abstract class BrowserKitTest extends TestCase
-{
-    use DatabaseTransactions;
-    use SharedTestHelpers;
-
-    /**
-     * The base URL to use while testing the application.
-     *
-     * @var string
-     */
-    protected $baseUrl = 'http://localhost';
-
-    public function tearDown(): void
-    {
-        DB::disconnect();
-        parent::tearDown();
-    }
-
-    /**
-     * Creates the application.
-     *
-     * @return Application
-     */
-    public function createApplication()
-    {
-        $app = require __DIR__ . '/../bootstrap/app.php';
-
-        $app->make(Kernel::class)->bootstrap();
-
-        return $app;
-    }
-
-    /**
-     * Quickly sets an array of settings.
-     *
-     * @param $settingsArray
-     */
-    protected function setSettings($settingsArray)
-    {
-        $settings = app(SettingService::class);
-        foreach ($settingsArray as $key => $value) {
-            $settings->put($key, $value);
-        }
-    }
-
-    /**
-     * Create a group of entities that belong to a specific user.
-     */
-    protected function createEntityChainBelongingToUser(User $creatorUser, ?User $updaterUser = null): array
-    {
-        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 compact('book', 'chapter', 'page');
-    }
-
-    /**
-     * Helper for updating entity permissions.
-     *
-     * @param Entity $entity
-     */
-    protected function updateEntityPermissions(Entity $entity)
-    {
-        $restrictionService = $this->app[PermissionService::class];
-        $restrictionService->buildJointPermissionsForEntity($entity);
-    }
-
-    /**
-     * Quick way to create a new user without any permissions.
-     *
-     * @param array $attributes
-     *
-     * @return mixed
-     */
-    protected function getNewBlankUser($attributes = [])
-    {
-        $user = factory(User::class)->create($attributes);
-
-        return $user;
-    }
-
-    /**
-     * Assert that a given string is seen inside an element.
-     *
-     * @param bool|string|null $element
-     * @param int              $position
-     * @param string           $text
-     * @param bool             $negate
-     *
-     * @return $this
-     */
-    protected function seeInNthElement($element, $position, $text, $negate = false)
-    {
-        $method = $negate ? 'assertDoesNotMatchRegularExpression' : 'assertMatchesRegularExpression';
-
-        $rawPattern = preg_quote($text, '/');
-
-        $escapedPattern = preg_quote(e($text), '/');
-
-        $content = $this->crawler->filter($element)->eq($position)->html();
-
-        $pattern = $rawPattern == $escapedPattern
-            ? $rawPattern : "({$rawPattern}|{$escapedPattern})";
-
-        $this->$method("/$pattern/i", $content);
-
-        return $this;
-    }
-
-    /**
-     * Assert that the current page matches a given URI.
-     *
-     * @param string $uri
-     *
-     * @return $this
-     */
-    protected function seePageUrlIs($uri)
-    {
-        $this->assertEquals(
-            $uri,
-            $this->currentUri,
-            "Did not land on expected page [{$uri}].\n"
-        );
-
-        return $this;
-    }
-
-    /**
-     * Do a forced visit that does not error out on exception.
-     *
-     * @param string $uri
-     * @param array  $parameters
-     * @param array  $cookies
-     * @param array  $files
-     *
-     * @return $this
-     */
-    protected function forceVisit($uri, $parameters = [], $cookies = [], $files = [])
-    {
-        $method = 'GET';
-        $uri = $this->prepareUrlForRequest($uri);
-        $this->call($method, $uri, $parameters, $cookies, $files);
-        $this->clearInputs()->followRedirects();
-        $this->currentUri = $this->app->make('request')->fullUrl();
-        $this->crawler = new Crawler($this->response->getContent(), $uri);
-
-        return $this;
-    }
-
-    /**
-     * Click the text within the selected element.
-     *
-     * @param $parentElement
-     * @param $linkText
-     *
-     * @return $this
-     */
-    protected function clickInElement($parentElement, $linkText)
-    {
-        $elem = $this->crawler->filter($parentElement);
-        $link = $elem->selectLink($linkText);
-        $this->visit($link->link()->getUri());
-
-        return $this;
-    }
-
-    /**
-     * Check if the page contains the given element.
-     *
-     * @param string $selector
-     */
-    protected function pageHasElement($selector)
-    {
-        $elements = $this->crawler->filter($selector);
-        $this->assertTrue(count($elements) > 0, 'The page does not contain an element matching ' . $selector);
-
-        return $this;
-    }
-
-    /**
-     * Check if the page contains the given element.
-     *
-     * @param string $selector
-     */
-    protected function pageNotHasElement($selector)
-    {
-        $elements = $this->crawler->filter($selector);
-        $this->assertFalse(count($elements) > 0, 'The page contains ' . count($elements) . ' elements matching ' . $selector);
-
-        return $this;
-    }
-}
diff --git a/tests/Commands/ResetMfaCommandTest.php b/tests/Commands/ResetMfaCommandTest.php
new file mode 100644 (file)
index 0000000..e65a048
--- /dev/null
@@ -0,0 +1,65 @@
+<?php
+
+namespace Tests\Commands;
+
+use BookStack\Auth\Access\Mfa\MfaValue;
+use BookStack\Auth\User;
+use Tests\TestCase;
+
+class ResetMfaCommandTest extends TestCase
+{
+    public function test_command_requires_email_or_id_option()
+    {
+        $this->artisan('bookstack:reset-mfa')
+            ->expectsOutput('Either a --id=<number> or --email=<email> option must be provided.')
+            ->assertExitCode(1);
+    }
+
+    public function test_command_runs_with_provided_email()
+    {
+        /** @var User $user */
+        $user = User::query()->first();
+        MfaValue::upsertWithValue($user, MfaValue::METHOD_TOTP, 'test');
+
+        $this->assertEquals(1, $user->mfaValues()->count());
+        $this->artisan("bookstack:reset-mfa --email={$user->email}")
+            ->expectsQuestion('Are you sure you want to proceed?', true)
+            ->expectsOutput('User MFA methods have been reset.')
+            ->assertExitCode(0);
+        $this->assertEquals(0, $user->mfaValues()->count());
+    }
+
+    public function test_command_runs_with_provided_id()
+    {
+        /** @var User $user */
+        $user = User::query()->first();
+        MfaValue::upsertWithValue($user, MfaValue::METHOD_TOTP, 'test');
+
+        $this->assertEquals(1, $user->mfaValues()->count());
+        $this->artisan("bookstack:reset-mfa --id={$user->id}")
+            ->expectsQuestion('Are you sure you want to proceed?', true)
+            ->expectsOutput('User MFA methods have been reset.')
+            ->assertExitCode(0);
+        $this->assertEquals(0, $user->mfaValues()->count());
+    }
+
+    public function test_saying_no_to_confirmation_does_not_reset_mfa()
+    {
+        /** @var User $user */
+        $user = User::query()->first();
+        MfaValue::upsertWithValue($user, MfaValue::METHOD_TOTP, 'test');
+
+        $this->assertEquals(1, $user->mfaValues()->count());
+        $this->artisan("bookstack:reset-mfa --id={$user->id}")
+            ->expectsQuestion('Are you sure you want to proceed?', false)
+            ->assertExitCode(1);
+        $this->assertEquals(1, $user->mfaValues()->count());
+    }
+
+    public function test_giving_non_existing_user_shows_error_message()
+    {
+        $this->artisan('bookstack:reset-mfa [email protected]')
+            ->expectsOutput('A user where [email protected] could not be found.')
+            ->assertExitCode(1);
+    }
+}
index cc9a7e44ea2a3c247022bfb49fd9789376cc08d8..1780ddee819504ebbd94bae50163653934a0198e 100644 (file)
@@ -308,6 +308,13 @@ class BookShelfTest extends TestCase
         $this->assertDatabaseHas('entity_permissions', ['restrictable_id' => $child->id, 'action' => 'update', 'role_id' => $editorRole->id]);
     }
 
+    public function test_permission_page_has_a_warning_about_no_cascading()
+    {
+        $shelf = Bookshelf::first();
+        $resp = $this->asAdmin()->get($shelf->getUrl('/permissions'));
+        $resp->assertSeeText('Permissions on bookshelves do not automatically cascade to contained books.');
+    }
+
     public function test_bookshelves_show_in_breadcrumbs_if_in_context()
     {
         $shelf = Bookshelf::first();
@@ -362,4 +369,12 @@ class BookShelfTest extends TestCase
         $resp = $this->asEditor()->get($newBook->getUrl());
         $resp->assertDontSee($shelfInfo['name']);
     }
+
+    public function test_cancel_on_child_book_creation_returns_to_original_shelf()
+    {
+        /** @var Bookshelf $shelf */
+        $shelf = Bookshelf::query()->first();
+        $resp = $this->asEditor()->get($shelf->getUrl('/create-book'));
+        $resp->assertElementContains('form a[href="' . $shelf->getUrl() . '"]', 'Cancel');
+    }
 }
index b4ba2fa8229a82ea7c9c709204dce46dda4781bb..fa63c0bf98c1ef3bbf8865294685b05e45ca3d24 100644 (file)
@@ -7,7 +7,69 @@ use Tests\TestCase;
 
 class BookTest extends TestCase
 {
-    public function test_book_delete()
+    public function test_create()
+    {
+        $book = factory(Book::class)->make([
+            'name' => 'My First Book',
+        ]);
+
+        $resp = $this->asEditor()->get('/books');
+        $resp->assertElementContains('a[href="' . url('/create-book') . '"]', 'Create New Book');
+
+        $resp = $this->get('/create-book');
+        $resp->assertElementContains('form[action="' . url('/books') . '"][method="POST"]', 'Save Book');
+
+        $resp = $this->post('/books', $book->only('name', 'description'));
+        $resp->assertRedirect('/books/my-first-book');
+
+        $resp = $this->get('/books/my-first-book');
+        $resp->assertSee($book->name);
+        $resp->assertSee($book->description);
+    }
+
+    public function test_create_uses_different_slugs_when_name_reused()
+    {
+        $book = factory(Book::class)->make([
+            'name' => 'My First Book',
+        ]);
+
+        $this->asEditor()->post('/books', $book->only('name', 'description'));
+        $this->asEditor()->post('/books', $book->only('name', 'description'));
+
+        $books = Book::query()->where('name', '=', $book->name)
+            ->orderBy('id', 'desc')
+            ->take(2)
+            ->get();
+
+        $this->assertMatchesRegularExpression('/my-first-book-[0-9a-zA-Z]{3}/', $books[0]->slug);
+        $this->assertEquals('my-first-book', $books[1]->slug);
+    }
+
+    public function test_update()
+    {
+        /** @var Book $book */
+        $book = Book::query()->first();
+        // Cheeky initial update to refresh slug
+        $this->asEditor()->put($book->getUrl(), ['name' => $book->name . '5', 'description' => $book->description]);
+        $book->refresh();
+
+        $newName = $book->name . ' Updated';
+        $newDesc = $book->description . ' with more content';
+
+        $resp = $this->get($book->getUrl('/edit'));
+        $resp->assertSee($book->name);
+        $resp->assertSee($book->description);
+        $resp->assertElementContains('form[action="' . $book->getUrl() . '"]', 'Save Book');
+
+        $resp = $this->put($book->getUrl(), ['name' => $newName, 'description' => $newDesc]);
+        $resp->assertRedirect($book->getUrl() . '-updated');
+
+        $resp = $this->get($book->getUrl() . '-updated');
+        $resp->assertSee($newName);
+        $resp->assertSee($newDesc);
+    }
+
+    public function test_delete()
     {
         $book = Book::query()->whereHas('pages')->whereHas('chapters')->first();
         $this->assertNull($book->deleted_at);
@@ -34,6 +96,20 @@ class BookTest extends TestCase
         $redirectReq->assertNotificationContains('Book Successfully Deleted');
     }
 
+    public function test_cancel_on_create_page_leads_back_to_books_listing()
+    {
+        $resp = $this->asEditor()->get('/create-book');
+        $resp->assertElementContains('form a[href="' . url('/books') . '"]', 'Cancel');
+    }
+
+    public function test_cancel_on_edit_book_page_leads_back_to_book()
+    {
+        /** @var Book $book */
+        $book = Book::query()->first();
+        $resp = $this->asEditor()->get($book->getUrl('/edit'));
+        $resp->assertElementContains('form a[href="' . $book->getUrl() . '"]', 'Cancel');
+    }
+
     public function test_next_previous_navigation_controls_show_within_book_content()
     {
         $book = Book::query()->first();
@@ -48,4 +124,84 @@ class BookTest extends TestCase
         $resp->assertElementContains('#sibling-navigation', 'Previous');
         $resp->assertElementContains('#sibling-navigation', substr($chapter->name, 0, 20));
     }
+
+    public function test_recently_viewed_books_updates_as_expected()
+    {
+        $books = Book::all()->take(2);
+
+        $this->asAdmin()->get('/books')
+            ->assertElementNotContains('#recents', $books[0]->name)
+            ->assertElementNotContains('#recents', $books[1]->name);
+
+        $this->get($books[0]->getUrl());
+        $this->get($books[1]->getUrl());
+
+        $this->get('/books')
+            ->assertElementContains('#recents', $books[0]->name)
+            ->assertElementContains('#recents', $books[1]->name);
+    }
+
+    public function test_popular_books_updates_upon_visits()
+    {
+        $books = Book::all()->take(2);
+
+        $this->asAdmin()->get('/books')
+            ->assertElementNotContains('#popular', $books[0]->name)
+            ->assertElementNotContains('#popular', $books[1]->name);
+
+        $this->get($books[0]->getUrl());
+        $this->get($books[1]->getUrl());
+        $this->get($books[0]->getUrl());
+
+        $this->get('/books')
+            ->assertElementContains('#popular .book:nth-child(1)', $books[0]->name)
+            ->assertElementContains('#popular .book:nth-child(2)', $books[1]->name);
+    }
+
+    public function test_books_view_shows_view_toggle_option()
+    {
+        /** @var Book $book */
+        $editor = $this->getEditor();
+        setting()->putUser($editor, 'books_view_type', 'list');
+
+        $resp = $this->actingAs($editor)->get('/books');
+        $resp->assertElementContains('form[action$="/settings/users/' . $editor->id . '/switch-books-view"]', 'Grid View');
+        $resp->assertElementExists('input[name="view_type"][value="grid"]');
+
+        $resp = $this->patch("/settings/users/{$editor->id}/switch-books-view", ['view_type' => 'grid']);
+        $resp->assertRedirect();
+        $this->assertEquals('grid', setting()->getUser($editor, 'books_view_type'));
+
+        $resp = $this->actingAs($editor)->get('/books');
+        $resp->assertElementContains('form[action$="/settings/users/' . $editor->id . '/switch-books-view"]', 'List View');
+        $resp->assertElementExists('input[name="view_type"][value="list"]');
+
+        $resp = $this->patch("/settings/users/{$editor->id}/switch-books-view", ['view_type' => 'list']);
+        $resp->assertRedirect();
+        $this->assertEquals('list', setting()->getUser($editor, 'books_view_type'));
+    }
+
+    public function test_slug_multi_byte_url_safe()
+    {
+        $book = $this->newBook([
+            'name' => 'информация',
+        ]);
+
+        $this->assertEquals('informatsiya', $book->slug);
+
+        $book = $this->newBook([
+            'name' => '¿Qué?',
+        ]);
+
+        $this->assertEquals('que', $book->slug);
+    }
+
+    public function test_slug_format()
+    {
+        $book = $this->newBook([
+            'name' => 'PartA / PartB / PartC',
+        ]);
+
+        $this->assertEquals('parta-partb-partc', $book->slug);
+    }
 }
index 45c132e8917428d90b8333243513e77c3a38936c..ea29ece5d2c51db2633e13f2236e9e3e8af043c9 100644 (file)
@@ -2,12 +2,36 @@
 
 namespace Tests\Entity;
 
+use BookStack\Entities\Models\Book;
 use BookStack\Entities\Models\Chapter;
 use Tests\TestCase;
 
 class ChapterTest extends TestCase
 {
-    public function test_chapter_delete()
+    public function test_create()
+    {
+        /** @var Book $book */
+        $book = Book::query()->first();
+
+        $chapter = factory(Chapter::class)->make([
+            'name' => 'My First Chapter',
+        ]);
+
+        $resp = $this->asEditor()->get($book->getUrl());
+        $resp->assertElementContains('a[href="' . $book->getUrl('/create-chapter') . '"]', 'New Chapter');
+
+        $resp = $this->get($book->getUrl('/create-chapter'));
+        $resp->assertElementContains('form[action="' . $book->getUrl('/create-chapter') . '"][method="POST"]', 'Save Chapter');
+
+        $resp = $this->post($book->getUrl('/create-chapter'), $chapter->only('name', 'description'));
+        $resp->assertRedirect($book->getUrl('/chapter/my-first-chapter'));
+
+        $resp = $this->get($book->getUrl('/chapter/my-first-chapter'));
+        $resp->assertSee($chapter->name);
+        $resp->assertSee($chapter->description);
+    }
+
+    public function test_delete()
     {
         $chapter = Chapter::query()->whereHas('pages')->first();
         $this->assertNull($chapter->deleted_at);
diff --git a/tests/Entity/EntityAccessTest.php b/tests/Entity/EntityAccessTest.php
new file mode 100644 (file)
index 0000000..f2f2445
--- /dev/null
@@ -0,0 +1,52 @@
+<?php
+
+namespace Tests\Entity;
+
+use BookStack\Auth\UserRepo;
+use BookStack\Entities\Models\Entity;
+use BookStack\Entities\Repos\PageRepo;
+use Tests\TestCase;
+
+class EntityAccessTest extends TestCase
+{
+    public function test_entities_viewable_after_creator_deletion()
+    {
+        // Create required assets and revisions
+        $creator = $this->getEditor();
+        $updater = $this->getViewer();
+        $entities = $this->createEntityChainBelongingToUser($creator, $updater);
+        app()->make(UserRepo::class)->destroy($creator);
+        app()->make(PageRepo::class)->update($entities['page'], ['html' => '<p>hello!</p>>']);
+
+        $this->checkEntitiesViewable($entities);
+    }
+
+    public function test_entities_viewable_after_updater_deletion()
+    {
+        // Create required assets and revisions
+        $creator = $this->getViewer();
+        $updater = $this->getEditor();
+        $entities = $this->createEntityChainBelongingToUser($creator, $updater);
+        app()->make(UserRepo::class)->destroy($updater);
+        app()->make(PageRepo::class)->update($entities['page'], ['html' => '<p>Hello there!</p>']);
+
+        $this->checkEntitiesViewable($entities);
+    }
+
+    /**
+     * @param array<string, Entity> $entities
+     */
+    private function checkEntitiesViewable(array $entities)
+    {
+        // Check pages and books are visible.
+        $this->asAdmin();
+        foreach ($entities as $entity) {
+            $this->get($entity->getUrl())
+                ->assertStatus(200)
+                ->assertSee($entity->name);
+        }
+
+        // Check revision listing shows no errors.
+        $this->get($entities['page']->getUrl('/revisions'))->assertStatus(200);
+    }
+}
diff --git a/tests/Entity/EntityTest.php b/tests/Entity/EntityTest.php
deleted file mode 100644 (file)
index f8c88b1..0000000
+++ /dev/null
@@ -1,319 +0,0 @@
-<?php
-
-namespace Tests\Entity;
-
-use BookStack\Auth\UserRepo;
-use BookStack\Entities\Models\Book;
-use BookStack\Entities\Models\Bookshelf;
-use BookStack\Entities\Models\Chapter;
-use BookStack\Entities\Models\Page;
-use BookStack\Entities\Repos\PageRepo;
-use Carbon\Carbon;
-use Tests\BrowserKitTest;
-
-class EntityTest extends BrowserKitTest
-{
-    public function test_entity_creation()
-    {
-        // Test Creation
-        $book = $this->bookCreation();
-        $chapter = $this->chapterCreation($book);
-        $this->pageCreation($chapter);
-
-        // Test Updating
-        $this->bookUpdate($book);
-    }
-
-    public function bookUpdate(Book $book)
-    {
-        $newName = $book->name . ' Updated';
-        $this->asAdmin()
-            // Go to edit screen
-            ->visit($book->getUrl() . '/edit')
-            ->see($book->name)
-            // Submit new name
-            ->type($newName, '#name')
-            ->press('Save Book')
-            // Check page url and text
-            ->seePageIs($book->getUrl() . '-updated')
-            ->see($newName);
-
-        return Book::find($book->id);
-    }
-
-    public function test_book_sort_page_shows()
-    {
-        $books = Book::all();
-        $bookToSort = $books[0];
-        $this->asAdmin()
-            ->visit($bookToSort->getUrl())
-            ->click('Sort')
-            ->seePageIs($bookToSort->getUrl() . '/sort')
-            ->seeStatusCode(200)
-            ->see($bookToSort->name);
-    }
-
-    public function test_book_sort_item_returns_book_content()
-    {
-        $books = Book::all();
-        $bookToSort = $books[0];
-        $firstPage = $bookToSort->pages[0];
-        $firstChapter = $bookToSort->chapters[0];
-        $this->asAdmin()
-            ->visit($bookToSort->getUrl() . '/sort-item')
-            // Ensure book details are returned
-            ->see($bookToSort->name)
-            ->see($firstPage->name)
-            ->see($firstChapter->name);
-    }
-
-    public function test_toggle_book_view()
-    {
-        $editor = $this->getEditor();
-        setting()->putUser($editor, 'books_view_type', 'grid');
-
-        $this->actingAs($editor)
-            ->visit('/books')
-            ->pageHasElement('.featured-image-container')
-            ->submitForm('List View')
-            // Check redirection.
-            ->seePageIs('/books')
-            ->pageNotHasElement('.featured-image-container');
-
-        $this->actingAs($editor)
-            ->visit('/books')
-            ->submitForm('Grid View')
-            ->seePageIs('/books')
-            ->pageHasElement('.featured-image-container');
-    }
-
-    public function pageCreation($chapter)
-    {
-        $page = factory(Page::class)->make([
-            'name' => 'My First Page',
-        ]);
-
-        $this->asAdmin()
-            // Navigate to page create form
-            ->visit($chapter->getUrl())
-            ->click('New Page');
-
-        $draftPage = Page::where('draft', '=', true)->orderBy('created_at', 'desc')->first();
-
-        $this->seePageIs($draftPage->getUrl())
-            // Fill out form
-            ->type($page->name, '#name')
-            ->type($page->html, '#html')
-            ->press('Save Page')
-            // Check redirect and page
-            ->seePageIs($chapter->book->getUrl() . '/page/my-first-page')
-            ->see($page->name);
-
-        $page = Page::where('slug', '=', 'my-first-page')->where('chapter_id', '=', $chapter->id)->first();
-
-        return $page;
-    }
-
-    public function chapterCreation(Book $book)
-    {
-        $chapter = factory(Chapter::class)->make([
-            'name' => 'My First Chapter',
-        ]);
-
-        $this->asAdmin()
-            // Navigate to chapter create page
-            ->visit($book->getUrl())
-            ->click('New Chapter')
-            ->seePageIs($book->getUrl() . '/create-chapter')
-            // Fill out form
-            ->type($chapter->name, '#name')
-            ->type($chapter->description, '#description')
-            ->press('Save Chapter')
-            // Check redirect and landing page
-            ->seePageIs($book->getUrl() . '/chapter/my-first-chapter')
-            ->see($chapter->name)->see($chapter->description);
-
-        $chapter = Chapter::where('slug', '=', 'my-first-chapter')->where('book_id', '=', $book->id)->first();
-
-        return $chapter;
-    }
-
-    public function bookCreation()
-    {
-        $book = factory(Book::class)->make([
-            'name' => 'My First Book',
-        ]);
-        $this->asAdmin()
-            ->visit('/books')
-            // Choose to create a book
-            ->click('Create New Book')
-            ->seePageIs('/create-book')
-            // Fill out form & save
-            ->type($book->name, '#name')
-            ->type($book->description, '#description')
-            ->press('Save Book')
-            // Check it redirects correctly
-            ->seePageIs('/books/my-first-book')
-            ->see($book->name)->see($book->description);
-
-        // Ensure duplicate names are given different slugs
-        $this->asAdmin()
-            ->visit('/create-book')
-            ->type($book->name, '#name')
-            ->type($book->description, '#description')
-            ->press('Save Book');
-
-        $expectedPattern = '/\/books\/my-first-book-[0-9a-zA-Z]{3}/';
-        $this->assertMatchesRegularExpression($expectedPattern, $this->currentUri, "Did not land on expected page [$expectedPattern].\n");
-
-        $book = Book::where('slug', '=', 'my-first-book')->first();
-
-        return $book;
-    }
-
-    public function test_entities_viewable_after_creator_deletion()
-    {
-        // Create required assets and revisions
-        $creator = $this->getEditor();
-        $updater = $this->getEditor();
-        $entities = $this->createEntityChainBelongingToUser($creator, $updater);
-        $this->actingAs($creator);
-        app(UserRepo::class)->destroy($creator);
-        app(PageRepo::class)->update($entities['page'], ['html' => '<p>hello!</p>>']);
-
-        $this->checkEntitiesViewable($entities);
-    }
-
-    public function test_entities_viewable_after_updater_deletion()
-    {
-        // Create required assets and revisions
-        $creator = $this->getEditor();
-        $updater = $this->getEditor();
-        $entities = $this->createEntityChainBelongingToUser($creator, $updater);
-        $this->actingAs($updater);
-        app(UserRepo::class)->destroy($updater);
-        app(PageRepo::class)->update($entities['page'], ['html' => '<p>Hello there!</p>']);
-
-        $this->checkEntitiesViewable($entities);
-    }
-
-    private function checkEntitiesViewable($entities)
-    {
-        // Check pages and books are visible.
-        $this->asAdmin();
-        $this->visit($entities['book']->getUrl())->seeStatusCode(200)
-            ->visit($entities['chapter']->getUrl())->seeStatusCode(200)
-            ->visit($entities['page']->getUrl())->seeStatusCode(200);
-        // Check revision listing shows no errors.
-        $this->visit($entities['page']->getUrl())
-            ->click('Revisions')->seeStatusCode(200);
-    }
-
-    public function test_recently_updated_pages_view()
-    {
-        $user = $this->getEditor();
-        $content = $this->createEntityChainBelongingToUser($user);
-
-        $this->asAdmin()->visit('/pages/recently-updated')
-            ->seeInNthElement('.entity-list .page', 0, $content['page']->name);
-    }
-
-    public function test_old_page_slugs_redirect_to_new_pages()
-    {
-        $page = Page::first();
-        $pageUrl = $page->getUrl();
-        $newPageUrl = '/books/' . $page->book->slug . '/page/super-test-page';
-        // Need to save twice since revisions are not generated in seeder.
-        $this->asAdmin()->visit($pageUrl)
-            ->clickInElement('#content', 'Edit')
-            ->type('super test', '#name')
-            ->press('Save Page');
-
-        $page = Page::first();
-        $pageUrl = $page->getUrl();
-
-        // Second Save
-        $this->visit($pageUrl)
-            ->clickInElement('#content', 'Edit')
-            ->type('super test page', '#name')
-            ->press('Save Page')
-            // Check redirect
-            ->seePageIs($newPageUrl);
-
-        $this->visit($pageUrl)
-            ->seePageIs($newPageUrl);
-    }
-
-    public function test_recently_updated_pages_on_home()
-    {
-        $page = Page::orderBy('updated_at', 'asc')->first();
-        Page::where('id', '!=', $page->id)->update([
-            'updated_at' => Carbon::now()->subSecond(1),
-        ]);
-        $this->asAdmin()->visit('/')
-            ->dontSeeInElement('#recently-updated-pages', $page->name);
-        $this->visit($page->getUrl() . '/edit')
-            ->press('Save Page')
-            ->visit('/')
-            ->seeInElement('#recently-updated-pages', $page->name);
-    }
-
-    public function test_slug_multi_byte_url_safe()
-    {
-        $book = $this->newBook([
-            'name' => 'информация',
-        ]);
-
-        $this->assertEquals('informatsiya', $book->slug);
-
-        $book = $this->newBook([
-            'name' => '¿Qué?',
-        ]);
-
-        $this->assertEquals('que', $book->slug);
-    }
-
-    public function test_slug_format()
-    {
-        $book = $this->newBook([
-            'name' => 'PartA / PartB / PartC',
-        ]);
-
-        $this->assertEquals('parta-partb-partc', $book->slug);
-    }
-
-    public function test_shelf_cancel_creation_returns_to_correct_place()
-    {
-        $shelf = Bookshelf::first();
-
-        // Cancel button from shelf goes back to shelf
-        $this->asEditor()->visit($shelf->getUrl('/create-book'))
-            ->see('Cancel')
-            ->click('Cancel')
-            ->seePageIs($shelf->getUrl());
-
-        // Cancel button from books goes back to books
-        $this->asEditor()->visit('/create-book')
-            ->see('Cancel')
-            ->click('Cancel')
-            ->seePageIs('/books');
-
-        // Cancel button from book edit goes back to book
-        $book = Book::first();
-
-        $this->asEditor()->visit($book->getUrl('/edit'))
-            ->see('Cancel')
-            ->click('Cancel')
-            ->seePageIs($book->getUrl());
-    }
-
-    public function test_page_within_chapter_deletion_returns_to_chapter()
-    {
-        $chapter = Chapter::query()->first();
-        $page = $chapter->pages()->first();
-
-        $this->asEditor()->visit($page->getUrl('/delete'))
-            ->submitForm('Confirm')
-            ->seePageIs($chapter->getUrl());
-    }
-}
index 4c6fb1a74f43da2c09011c42f9dabb1267bca328..aebc5f2455f31a2e2678df44371badabdd1cb89b 100644 (file)
@@ -2,6 +2,7 @@
 
 namespace Tests\Entity;
 
+use BookStack\Auth\Role;
 use BookStack\Entities\Models\Book;
 use BookStack\Entities\Models\Chapter;
 use BookStack\Entities\Models\Page;
@@ -340,4 +341,45 @@ class ExportTest extends TestCase
         $resp->assertSee('# ' . $chapter->name);
         $resp->assertSee('# ' . $page->name);
     }
+
+    public function test_export_option_only_visible_and_accessible_with_permission()
+    {
+        $book = Book::query()->whereHas('pages')->whereHas('chapters')->first();
+        $chapter = $book->chapters()->first();
+        $page = $chapter->pages()->first();
+        $entities = [$book, $chapter, $page];
+        $user = $this->getViewer();
+        $this->actingAs($user);
+
+        foreach ($entities as $entity) {
+            $resp = $this->get($entity->getUrl());
+            $resp->assertSee('/export/pdf');
+        }
+
+        /** @var Role $role */
+        $this->removePermissionFromUser($user, 'content-export');
+
+        foreach ($entities as $entity) {
+            $resp = $this->get($entity->getUrl());
+            $resp->assertDontSee('/export/pdf');
+            $resp = $this->get($entity->getUrl('/export/pdf'));
+            $this->assertPermissionError($resp);
+        }
+    }
+
+    public function test_wkhtmltopdf_only_used_when_allow_untrusted_is_true()
+    {
+        /** @var Page $page */
+        $page = Page::query()->first();
+
+        config()->set('snappy.pdf.binary', '/abc123');
+        config()->set('app.allow_untrusted_server_fetching', false);
+
+        $resp = $this->asEditor()->get($page->getUrl('/export/pdf'));
+        $resp->assertStatus(200); // Sucessful response with invalid snappy binary indicates dompdf usage.
+
+        config()->set('app.allow_untrusted_server_fetching', true);
+        $resp = $this->get($page->getUrl('/export/pdf'));
+        $resp->assertStatus(500); // Bad response indicates wkhtml usage
+    }
 }
diff --git a/tests/Entity/MarkdownTest.php b/tests/Entity/MarkdownTest.php
deleted file mode 100644 (file)
index 7de7ea1..0000000
+++ /dev/null
@@ -1,53 +0,0 @@
-<?php
-
-namespace Tests\Entity;
-
-use Tests\BrowserKitTest;
-
-class MarkdownTest extends BrowserKitTest
-{
-    protected $page;
-
-    public function setUp(): void
-    {
-        parent::setUp();
-        $this->page = \BookStack\Entities\Models\Page::first();
-    }
-
-    protected function setMarkdownEditor()
-    {
-        $this->setSettings(['app-editor' => 'markdown']);
-    }
-
-    public function test_default_editor_is_wysiwyg()
-    {
-        $this->assertEquals(setting('app-editor'), 'wysiwyg');
-        $this->asAdmin()->visit($this->page->getUrl() . '/edit')
-            ->pageHasElement('#html-editor');
-    }
-
-    public function test_markdown_setting_shows_markdown_editor()
-    {
-        $this->setMarkdownEditor();
-        $this->asAdmin()->visit($this->page->getUrl() . '/edit')
-            ->pageNotHasElement('#html-editor')
-            ->pageHasElement('#markdown-editor');
-    }
-
-    public function test_markdown_content_given_to_editor()
-    {
-        $this->setMarkdownEditor();
-        $mdContent = '# hello. This is a test';
-        $this->page->markdown = $mdContent;
-        $this->page->save();
-        $this->asAdmin()->visit($this->page->getUrl() . '/edit')
-            ->seeInField('markdown', $mdContent);
-    }
-
-    public function test_html_content_given_to_editor_if_no_markdown()
-    {
-        $this->setMarkdownEditor();
-        $this->asAdmin()->visit($this->page->getUrl() . '/edit')
-            ->seeInField('markdown', $this->page->html);
-    }
-}
index 6021987259db69290053cc344c4f3f75957c9788..45c27c9f9545cb4cec5b7a919a8e56d8df5a1ec4 100644 (file)
@@ -135,14 +135,26 @@ class PageContentTest extends TestCase
         }
     }
 
-    public function test_iframe_js_and_base64_urls_are_removed()
+    public function test_js_and_base64_src_urls_are_removed()
     {
         $checks = [
             '<iframe src="javascript:alert(document.cookie)"></iframe>',
+            '<iframe src="JavAScRipT:alert(document.cookie)"></iframe>',
+            '<iframe src="JavAScRipT:alert(document.cookie)"></iframe>',
             '<iframe SRC=" javascript: alert(document.cookie)"></iframe>',
             '<iframe src="data:text/html;base64,PHNjcmlwdD5hbGVydCgnaGVsbG8nKTwvc2NyaXB0Pg==" frameborder="0"></iframe>',
+            '<iframe src="DaTa:text/html;base64,PHNjcmlwdD5hbGVydCgnaGVsbG8nKTwvc2NyaXB0Pg==" frameborder="0"></iframe>',
             '<iframe src=" data:text/html;base64,PHNjcmlwdD5hbGVydCgnaGVsbG8nKTwvc2NyaXB0Pg==" frameborder="0"></iframe>',
+            '<img src="javascript:alert(document.cookie)"/>',
+            '<img src="JavAScRipT:alert(document.cookie)"/>',
+            '<img src="JavAScRipT:alert(document.cookie)"/>',
+            '<img SRC=" javascript: alert(document.cookie)"/>',
+            '<img src="data:text/html;base64,PHNjcmlwdD5hbGVydCgnaGVsbG8nKTwvc2NyaXB0Pg=="/>',
+            '<img src="DaTa:text/html;base64,PHNjcmlwdD5hbGVydCgnaGVsbG8nKTwvc2NyaXB0Pg=="/>',
+            '<img src=" data:text/html;base64,PHNjcmlwdD5hbGVydCgnaGVsbG8nKTwvc2NyaXB0Pg=="/>',
             '<iframe srcdoc="<script>window.alert(document.cookie)</script>"></iframe>',
+            '<iframe SRCdoc="<script>window.alert(document.cookie)</script>"></iframe>',
+            '<IMG SRC=`javascript:alert("RSnake says, \'XSS\'")`>',
         ];
 
         $this->asEditor();
@@ -155,6 +167,7 @@ class PageContentTest extends TestCase
             $pageView = $this->get($page->getUrl());
             $pageView->assertStatus(200);
             $pageView->assertElementNotContains('.page-content', '<iframe>');
+            $pageView->assertElementNotContains('.page-content', '<img');
             $pageView->assertElementNotContains('.page-content', '</iframe>');
             $pageView->assertElementNotContains('.page-content', 'src=');
             $pageView->assertElementNotContains('.page-content', 'javascript:');
@@ -168,6 +181,8 @@ class PageContentTest extends TestCase
         $checks = [
             '<a id="xss" href="javascript:alert(document.cookie)>Click me</a>',
             '<a id="xss" href="javascript: alert(document.cookie)>Click me</a>',
+            '<a id="xss" href="JaVaScRiPt: alert(document.cookie)>Click me</a>',
+            '<a id="xss" href=" JaVaScRiPt: alert(document.cookie)>Click me</a>',
         ];
 
         $this->asEditor();
@@ -179,7 +194,7 @@ class PageContentTest extends TestCase
 
             $pageView = $this->get($page->getUrl());
             $pageView->assertStatus(200);
-            $pageView->assertElementNotContains('.page-content', '<a id="xss">');
+            $pageView->assertElementNotContains('.page-content', '<a id="xss"');
             $pageView->assertElementNotContains('.page-content', 'href=javascript:');
         }
     }
@@ -188,8 +203,10 @@ class PageContentTest extends TestCase
     {
         $checks = [
             '<form><input id="xss" type=submit formaction=javascript:alert(document.domain) value=Submit><input></form>',
+            '<form ><button id="xss" formaction="JaVaScRiPt:alert(document.domain)">Click me</button></form>',
             '<form ><button id="xss" formaction=javascript:alert(document.domain)>Click me</button></form>',
             '<form id="xss" action=javascript:alert(document.domain)><input type=submit value=Submit></form>',
+            '<form id="xss" action="JaVaScRiPt:alert(document.domain)"><input type=submit value=Submit></form>',
         ];
 
         $this->asEditor();
@@ -213,6 +230,8 @@ class PageContentTest extends TestCase
     {
         $checks = [
             '<meta http-equiv="refresh" content="0; url=//external_url">',
+            '<meta http-equiv="refresh" ConTeNt="0; url=//external_url">',
+            '<meta http-equiv="refresh" content="0; UrL=//external_url">',
         ];
 
         $this->asEditor();
@@ -249,11 +268,13 @@ class PageContentTest extends TestCase
     {
         $checks = [
             '<p onclick="console.log(\'test\')">Hello</p>',
+            '<p OnCliCk="console.log(\'test\')">Hello</p>',
             '<div>Lorem ipsum dolor sit amet.</div><p onclick="console.log(\'test\')">Hello</p>',
             '<div>Lorem ipsum dolor sit amet.<p onclick="console.log(\'test\')">Hello</p></div>',
             '<div><div><div><div>Lorem ipsum dolor sit amet.<p onclick="console.log(\'test\')">Hello</p></div></div></div></div>',
             '<div onclick="console.log(\'test\')">Lorem ipsum dolor sit amet.</div><p onclick="console.log(\'test\')">Hello</p><div></div>',
             '<a a="<img src=1 onerror=\'alert(1)\'> ',
+            '\<a onclick="alert(document.cookie)"\>xss link\</a\>',
         ];
 
         $this->asEditor();
@@ -284,6 +305,28 @@ class PageContentTest extends TestCase
         $pageView->assertDontSee('abc123abc123');
     }
 
+    public function test_svg_xlink_hrefs_are_removed()
+    {
+        $checks = [
+            '<svg id="test" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100" height="100"><a xlink:href="javascript:alert(document.domain)"><rect x="0" y="0" width="100" height="100" /></a></svg>',
+            '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><use xlink:href="data:application/xml;base64 ,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIj4KPGRlZnM+CjxjaXJjbGUgaWQ9InRlc3QiIHI9IjAiIGN4PSIwIiBjeT0iMCIgc3R5bGU9ImZpbGw6ICNGMDAiPgo8c2V0IGF0dHJpYnV0ZU5hbWU9ImZpbGwiIGF0dHJpYnV0ZVR5cGU9IkNTUyIgb25iZWdpbj0nYWxlcnQoZG9jdW1lbnQuZG9tYWluKScKb25lbmQ9J2FsZXJ0KCJvbmVuZCIpJyB0bz0iIzAwRiIgYmVnaW49IjBzIiBkdXI9Ijk5OXMiIC8+CjwvY2lyY2xlPgo8L2RlZnM+Cjx1c2UgeGxpbms6aHJlZj0iI3Rlc3QiLz4KPC9zdmc+#test"/></svg>',
+        ];
+
+        $this->asEditor();
+        $page = Page::query()->first();
+
+        foreach ($checks as $check) {
+            $page->html = $check;
+            $page->save();
+
+            $pageView = $this->get($page->getUrl());
+            $pageView->assertStatus(200);
+            $pageView->assertElementNotContains('.page-content', 'alert');
+            $pageView->assertElementNotContains('.page-content', 'xlink:href');
+            $pageView->assertElementNotContains('.page-content', 'application/xml');
+        }
+    }
+
     public function test_page_inline_on_attributes_show_if_configured()
     {
         $this->asEditor();
@@ -464,7 +507,8 @@ class PageContentTest extends TestCase
         $this->assertStringContainsString('type="checkbox"', $page->html);
 
         $pageView = $this->get($page->getUrl());
-        $pageView->assertElementExists('.page-content input[type=checkbox]');
+        $pageView->assertElementExists('.page-content li.task-list-item input[type=checkbox]');
+        $pageView->assertElementExists('.page-content li.task-list-item input[type=checkbox][checked=checked]');
     }
 
     public function test_page_markdown_strikethrough_rendering()
index 68059af6e7126d1c2b75487d425b6c684a8aca3e..b2fa4bb318968edccec1965e6b128940b8575217 100644 (file)
@@ -2,12 +2,16 @@
 
 namespace Tests\Entity;
 
+use BookStack\Entities\Models\Book;
 use BookStack\Entities\Models\Page;
 use BookStack\Entities\Repos\PageRepo;
-use Tests\BrowserKitTest;
+use Tests\TestCase;
 
-class PageDraftTest extends BrowserKitTest
+class PageDraftTest extends TestCase
 {
+    /**
+     * @var Page
+     */
     protected $page;
 
     /**
@@ -18,99 +22,101 @@ class PageDraftTest extends BrowserKitTest
     public function setUp(): void
     {
         parent::setUp();
-        $this->page = \BookStack\Entities\Models\Page::first();
-        $this->pageRepo = app(PageRepo::class);
+        $this->page = Page::query()->first();
+        $this->pageRepo = app()->make(PageRepo::class);
     }
 
     public function test_draft_content_shows_if_available()
     {
         $addedContent = '<p>test message content</p>';
-        $this->asAdmin()->visit($this->page->getUrl('/edit'))
-            ->dontSeeInField('html', $addedContent);
+
+        $this->asAdmin()->get($this->page->getUrl('/edit'))
+            ->assertElementNotContains('[name="html"]', $addedContent);
 
         $newContent = $this->page->html . $addedContent;
         $this->pageRepo->updatePageDraft($this->page, ['html' => $newContent]);
-        $this->asAdmin()->visit($this->page->getUrl('/edit'))
-            ->seeInField('html', $newContent);
+        $this->asAdmin()->get($this->page->getUrl('/edit'))
+            ->assertElementContains('[name="html"]', $newContent);
     }
 
     public function test_draft_not_visible_by_others()
     {
         $addedContent = '<p>test message content</p>';
-        $this->asAdmin()->visit($this->page->getUrl('/edit'))
-            ->dontSeeInField('html', $addedContent);
+        $this->asAdmin()->get($this->page->getUrl('/edit'))
+            ->assertElementNotContains('[name="html"]', $addedContent);
 
         $newContent = $this->page->html . $addedContent;
         $newUser = $this->getEditor();
         $this->pageRepo->updatePageDraft($this->page, ['html' => $newContent]);
 
-        $this->actingAs($newUser)->visit($this->page->getUrl('/edit'))
-            ->dontSeeInField('html', $newContent);
+        $this->actingAs($newUser)->get($this->page->getUrl('/edit'))
+            ->assertElementNotContains('[name="html"]', $newContent);
     }
 
     public function test_alert_message_shows_if_editing_draft()
     {
         $this->asAdmin();
         $this->pageRepo->updatePageDraft($this->page, ['html' => 'test content']);
-        $this->asAdmin()->visit($this->page->getUrl('/edit'))
-            ->see('You are currently editing a draft');
+        $this->asAdmin()->get($this->page->getUrl('/edit'))
+            ->assertSee('You are currently editing a draft');
     }
 
     public function test_alert_message_shows_if_someone_else_editing()
     {
-        $nonEditedPage = \BookStack\Entities\Models\Page::take(10)->get()->last();
+        $nonEditedPage = Page::query()->take(10)->get()->last();
         $addedContent = '<p>test message content</p>';
-        $this->asAdmin()->visit($this->page->getUrl('/edit'))
-            ->dontSeeInField('html', $addedContent);
+        $this->asAdmin()->get($this->page->getUrl('/edit'))
+            ->assertElementNotContains('[name="html"]', $addedContent);
 
         $newContent = $this->page->html . $addedContent;
         $newUser = $this->getEditor();
         $this->pageRepo->updatePageDraft($this->page, ['html' => $newContent]);
 
         $this->actingAs($newUser)
-            ->visit($this->page->getUrl('/edit'))
-            ->see('Admin has started editing this page');
+            ->get($this->page->getUrl('/edit'))
+            ->assertSee('Admin has started editing this page');
         $this->flushSession();
-        $this->visit($nonEditedPage->getUrl() . '/edit')
-            ->dontSeeInElement('.notification', 'Admin has started editing this page');
+        $this->get($nonEditedPage->getUrl() . '/edit')
+            ->assertElementNotContains('.notification', 'Admin has started editing this page');
     }
 
     public function test_draft_pages_show_on_homepage()
     {
-        $book = \BookStack\Entities\Models\Book::first();
-        $this->asAdmin()->visit('/')
-            ->dontSeeInElement('#recent-drafts', 'New Page')
-            ->visit($book->getUrl() . '/create-page')
-            ->visit('/')
-            ->seeInElement('#recent-drafts', 'New Page');
+        /** @var Book $book */
+        $book = Book::query()->first();
+        $this->asAdmin()->get('/')
+            ->assertElementNotContains('#recent-drafts', 'New Page');
+
+        $this->get($book->getUrl() . '/create-page');
+
+        $this->get('/')->assertElementContains('#recent-drafts', 'New Page');
     }
 
     public function test_draft_pages_not_visible_by_others()
     {
-        $book = \BookStack\Entities\Models\Book::first();
+        /** @var Book $book */
+        $book = Book::query()->first();
         $chapter = $book->chapters->first();
         $newUser = $this->getEditor();
 
-        $this->actingAs($newUser)->visit('/')
-            ->visit($book->getUrl('/create-page'))
-            ->visit($chapter->getUrl('/create-page'))
-            ->visit($book->getUrl())
-            ->seeInElement('.book-contents', 'New Page');
-
-        $this->asAdmin()
-            ->visit($book->getUrl())
-            ->dontSeeInElement('.book-contents', 'New Page')
-            ->visit($chapter->getUrl())
-            ->dontSeeInElement('.book-contents', 'New Page');
+        $this->actingAs($newUser)->get($book->getUrl('/create-page'));
+        $this->get($chapter->getUrl('/create-page'));
+        $this->get($book->getUrl())
+            ->assertElementContains('.book-contents', 'New Page');
+
+        $this->asAdmin()->get($book->getUrl())
+            ->assertElementNotContains('.book-contents', 'New Page');
+        $this->get($chapter->getUrl())
+            ->assertElementNotContains('.book-contents', 'New Page');
     }
 
     public function test_page_html_in_ajax_fetch_response()
     {
         $this->asAdmin();
+        /** @var Page $page */
         $page = Page::query()->first();
 
-        $this->getJson('/ajax/page/' . $page->id);
-        $this->seeJson([
+        $this->getJson('/ajax/page/' . $page->id)->assertJson([
             'html' => $page->html,
         ]);
     }
diff --git a/tests/Entity/PageEditorTest.php b/tests/Entity/PageEditorTest.php
new file mode 100644 (file)
index 0000000..9b0a8f1
--- /dev/null
@@ -0,0 +1,77 @@
+<?php
+
+namespace Tests\Entity;
+
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Page;
+use Tests\TestCase;
+
+class PageEditorTest extends TestCase
+{
+    /** @var Page */
+    protected $page;
+
+    public function setUp(): void
+    {
+        parent::setUp();
+        $this->page = Page::query()->first();
+    }
+
+    public function test_default_editor_is_wysiwyg()
+    {
+        $this->assertEquals('wysiwyg', setting('app-editor'));
+        $this->asAdmin()->get($this->page->getUrl() . '/edit')
+            ->assertElementExists('#html-editor');
+    }
+
+    public function test_markdown_setting_shows_markdown_editor()
+    {
+        $this->setSettings(['app-editor' => 'markdown']);
+        $this->asAdmin()->get($this->page->getUrl() . '/edit')
+            ->assertElementNotExists('#html-editor')
+            ->assertElementExists('#markdown-editor');
+    }
+
+    public function test_markdown_content_given_to_editor()
+    {
+        $this->setSettings(['app-editor' => 'markdown']);
+
+        $mdContent = '# hello. This is a test';
+        $this->page->markdown = $mdContent;
+        $this->page->save();
+
+        $this->asAdmin()->get($this->page->getUrl() . '/edit')
+            ->assertElementContains('[name="markdown"]', $mdContent);
+    }
+
+    public function test_html_content_given_to_editor_if_no_markdown()
+    {
+        $this->setSettings(['app-editor' => 'markdown']);
+        $this->asAdmin()->get($this->page->getUrl() . '/edit')
+            ->assertElementContains('[name="markdown"]', $this->page->html);
+    }
+
+    public function test_empty_markdown_still_saves_without_error()
+    {
+        $this->setSettings(['app-editor' => 'markdown']);
+        /** @var Book $book */
+        $book = Book::query()->first();
+
+        $this->asEditor()->get($book->getUrl('/create-page'));
+        $draft = Page::query()->where('book_id', '=', $book->id)
+            ->where('draft', '=', true)->first();
+
+        $details = [
+            'name'     => 'my page',
+            'markdown' => '',
+        ];
+        $resp = $this->post($book->getUrl("/draft/{$draft->id}"), $details);
+        $resp->assertRedirect();
+
+        $this->assertDatabaseHas('pages', [
+            'markdown' => $details['markdown'],
+            'id'       => $draft->id,
+            'draft'    => false,
+        ]);
+    }
+}
index 2721c225cd66c487f823d6e075e552f406ce6549..313fc77f060f51e7ddb0c15bf9eba8070a18a1ed 100644 (file)
@@ -3,11 +3,40 @@
 namespace Tests\Entity;
 
 use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Chapter;
 use BookStack\Entities\Models\Page;
+use Carbon\Carbon;
 use Tests\TestCase;
 
 class PageTest extends TestCase
 {
+    public function test_create()
+    {
+        /** @var Chapter $chapter */
+        $chapter = Chapter::query()->first();
+        $page = factory(Page::class)->make([
+            'name' => 'My First Page',
+        ]);
+
+        $resp = $this->asEditor()->get($chapter->getUrl());
+        $resp->assertElementContains('a[href="' . $chapter->getUrl('/create-page') . '"]', 'New Page');
+
+        $resp = $this->get($chapter->getUrl('/create-page'));
+        /** @var Page $draftPage */
+        $draftPage = Page::query()
+            ->where('draft', '=', true)
+            ->orderBy('created_at', 'desc')
+            ->first();
+        $resp->assertRedirect($draftPage->getUrl());
+
+        $resp = $this->get($draftPage->getUrl());
+        $resp->assertElementContains('form[action="' . $draftPage->getUrl() . '"][method="POST"]', 'Save Page');
+
+        $resp = $this->post($draftPage->getUrl(), $draftPage->only('name', 'html'));
+        $draftPage->refresh();
+        $resp->assertRedirect($draftPage->getUrl());
+    }
+
     public function test_page_view_when_creator_is_deleted_but_owner_exists()
     {
         $page = Page::query()->first();
@@ -190,26 +219,65 @@ class PageTest extends TestCase
         ]);
     }
 
-    public function test_empty_markdown_still_saves_without_error()
+    public function test_old_page_slugs_redirect_to_new_pages()
     {
-        $this->setSettings(['app-editor' => 'markdown']);
-        $book = Book::query()->first();
+        /** @var Page $page */
+        $page = Page::query()->first();
 
-        $this->asEditor()->get($book->getUrl('/create-page'));
-        $draft = Page::query()->where('book_id', '=', $book->id)
-            ->where('draft', '=', true)->first();
+        // Need to save twice since revisions are not generated in seeder.
+        $this->asAdmin()->put($page->getUrl(), [
+            'name' => 'super test',
+            'html' => '<p></p>',
+        ]);
 
-        $details = [
-            'name'     => 'my page',
-            'markdown' => '',
-        ];
-        $resp = $this->post($book->getUrl("/draft/{$draft->id}"), $details);
-        $resp->assertRedirect();
+        $page->refresh();
+        $pageUrl = $page->getUrl();
 
-        $this->assertDatabaseHas('pages', [
-            'markdown' => $details['markdown'],
-            'id'       => $draft->id,
-            'draft'    => false,
+        $this->put($pageUrl, [
+            'name' => 'super test page',
+            'html' => '<p></p>',
+        ]);
+
+        $this->get($pageUrl)
+            ->assertRedirect("/books/{$page->book->slug}/page/super-test-page");
+    }
+
+    public function test_page_within_chapter_deletion_returns_to_chapter()
+    {
+        /** @var Chapter $chapter */
+        $chapter = Chapter::query()->first();
+        $page = $chapter->pages()->first();
+
+        $this->asEditor()->delete($page->getUrl())
+            ->assertRedirect($chapter->getUrl());
+    }
+
+    public function test_recently_updated_pages_view()
+    {
+        $user = $this->getEditor();
+        $content = $this->createEntityChainBelongingToUser($user);
+
+        $this->asAdmin()->get('/pages/recently-updated')
+            ->assertElementContains('.entity-list .page:nth-child(1)', $content['page']->name);
+    }
+
+    public function test_recently_updated_pages_on_home()
+    {
+        /** @var Page $page */
+        $page = Page::query()->orderBy('updated_at', 'asc')->first();
+        Page::query()->where('id', '!=', $page->id)->update([
+            'updated_at' => Carbon::now()->subSecond(1),
         ]);
+
+        $this->asAdmin()->get('/')
+            ->assertElementNotContains('#recently-updated-pages', $page->name);
+
+        $this->put($page->getUrl(), [
+            'name' => $page->name,
+            'html' => $page->html,
+        ]);
+
+        $this->get('/')
+            ->assertElementContains('#recently-updated-pages', $page->name);
     }
 }
index f3d50b67d96940dec49edc73b02de062edac2e4d..5cfc5c3c5b935f9f2396fc38b4bf0521bfed1b6f 100644 (file)
@@ -216,6 +216,19 @@ class SortTest extends TestCase
         $this->assertEquals($newBook->id, $pageToCheck->book_id);
     }
 
+    public function test_book_sort_page_shows()
+    {
+        /** @var Book $bookToSort */
+        $bookToSort = Book::query()->first();
+
+        $resp = $this->asAdmin()->get($bookToSort->getUrl());
+        $resp->assertElementExists('a[href="' . $bookToSort->getUrl('/sort') . '"]');
+
+        $resp = $this->get($bookToSort->getUrl('/sort'));
+        $resp->assertStatus(200);
+        $resp->assertSee($bookToSort->name);
+    }
+
     public function test_book_sort()
     {
         $oldBook = Book::query()->first();
@@ -258,4 +271,40 @@ class SortTest extends TestCase
         $checkResp = $this->get(Page::find($checkPage->id)->getUrl());
         $checkResp->assertSee($newBook->name);
     }
+
+    public function test_book_sort_item_returns_book_content()
+    {
+        $books = Book::all();
+        $bookToSort = $books[0];
+        $firstPage = $bookToSort->pages[0];
+        $firstChapter = $bookToSort->chapters[0];
+
+        $resp = $this->asAdmin()->get($bookToSort->getUrl() . '/sort-item');
+
+        // Ensure book details are returned
+        $resp->assertSee($bookToSort->name);
+        $resp->assertSee($firstPage->name);
+        $resp->assertSee($firstChapter->name);
+    }
+
+    public function test_pages_in_book_show_sorted_by_priority()
+    {
+        /** @var Book $book */
+        $book = Book::query()->whereHas('pages')->first();
+        $book->chapters()->forceDelete();
+        /** @var Page[] $pages */
+        $pages = $book->pages()->where('chapter_id', '=', 0)->take(2)->get();
+        $book->pages()->whereNotIn('id', $pages->pluck('id'))->delete();
+
+        $resp = $this->asEditor()->get($book->getUrl());
+        $resp->assertElementContains('.content-wrap a.page:nth-child(1)', $pages[0]->name);
+        $resp->assertElementContains('.content-wrap a.page:nth-child(2)', $pages[1]->name);
+
+        $pages[0]->forceFill(['priority' => 10])->save();
+        $pages[1]->forceFill(['priority' => 5])->save();
+
+        $resp = $this->asEditor()->get($book->getUrl());
+        $resp->assertElementContains('.content-wrap a.page:nth-child(1)', $pages[1]->name);
+        $resp->assertElementContains('.content-wrap a.page:nth-child(2)', $pages[0]->name);
+    }
 }
index db4e94c6d11b1a03fe6a0a12e40e61f36dddbfdc..e27b787745ff3db74ed5db183fb2fe7b9afbd30b 100644 (file)
@@ -5,6 +5,7 @@ namespace Tests;
 use BookStack\Auth\Role;
 use BookStack\Auth\User;
 use BookStack\Entities\Models\Bookshelf;
+use BookStack\Entities\Models\Page;
 
 class HomepageTest extends TestCase
 {
@@ -78,6 +79,25 @@ class HomepageTest extends TestCase
         $pageDeleteReq->assertSessionMissing('error');
     }
 
+    public function test_custom_homepage_renders_includes()
+    {
+        $this->asEditor();
+        /** @var Page $included */
+        $included = Page::query()->first();
+        $content = str_repeat('This is the body content of my custom homepage.', 20);
+        $included->html = $content;
+        $included->save();
+
+        $name = 'My custom homepage';
+        $customPage = $this->newPage(['name' => $name, 'html' => '{{@' . $included->id . '}}']);
+        $this->setSettings(['app-homepage' => $customPage->id]);
+        $this->setSettings(['app-homepage-type' => 'page']);
+
+        $homeVisit = $this->get('/');
+        $homeVisit->assertSee($name);
+        $homeVisit->assertSee($content);
+    }
+
     public function test_set_book_homepage()
     {
         $editor = $this->getEditor();
index 77c62fdb500b88352aa5354305d0a1612ae57047..bb011cfc6f5df2d0d59d837be2437859b6635f43 100644 (file)
@@ -9,9 +9,9 @@ use BookStack\Entities\Models\Chapter;
 use BookStack\Entities\Models\Entity;
 use BookStack\Entities\Models\Page;
 use Illuminate\Support\Str;
-use Tests\BrowserKitTest;
+use Tests\TestCase;
 
-class EntityPermissionsTest extends BrowserKitTest
+class EntityPermissionsTest extends TestCase
 {
     /**
      * @var User
@@ -41,608 +41,598 @@ class EntityPermissionsTest extends BrowserKitTest
 
     public function test_bookshelf_view_restriction()
     {
-        $shelf = Bookshelf::first();
+        /** @var Bookshelf $shelf */
+        $shelf = Bookshelf::query()->first();
 
         $this->actingAs($this->user)
-            ->visit($shelf->getUrl())
-            ->seePageIs($shelf->getUrl());
+            ->get($shelf->getUrl())
+            ->assertStatus(200);
 
         $this->setRestrictionsForTestRoles($shelf, []);
 
-        $this->forceVisit($shelf->getUrl())
-            ->see('Bookshelf not found');
+        $this->followingRedirects()->get($shelf->getUrl())
+            ->assertSee('Bookshelf not found');
 
         $this->setRestrictionsForTestRoles($shelf, ['view']);
 
-        $this->visit($shelf->getUrl())
-            ->see($shelf->name);
+        $this->get($shelf->getUrl())
+            ->assertSee($shelf->name);
     }
 
     public function test_bookshelf_update_restriction()
     {
-        $shelf = Bookshelf::first();
+        /** @var Bookshelf $shelf */
+        $shelf = Bookshelf::query()->first();
 
         $this->actingAs($this->user)
-            ->visit($shelf->getUrl('/edit'))
-            ->see('Edit Book');
+            ->get($shelf->getUrl('/edit'))
+            ->assertSee('Edit Book');
 
         $this->setRestrictionsForTestRoles($shelf, ['view', 'delete']);
 
-        $this->forceVisit($shelf->getUrl('/edit'))
-            ->see('You do not have permission')->seePageIs('/');
+        $resp = $this->get($shelf->getUrl('/edit'))
+            ->assertRedirect('/');
+        $this->followRedirects($resp)->assertSee('You do not have permission');
 
         $this->setRestrictionsForTestRoles($shelf, ['view', 'update']);
 
-        $this->visit($shelf->getUrl('/edit'))
-            ->seePageIs($shelf->getUrl('/edit'));
+        $this->get($shelf->getUrl('/edit'))
+            ->assertOk();
     }
 
     public function test_bookshelf_delete_restriction()
     {
-        $shelf = Book::first();
+        /** @var Bookshelf $shelf */
+        $shelf = Bookshelf::query()->first();
 
         $this->actingAs($this->user)
-            ->visit($shelf->getUrl('/delete'))
-            ->see('Delete Book');
+            ->get($shelf->getUrl('/delete'))
+            ->assertSee('Delete Book');
 
         $this->setRestrictionsForTestRoles($shelf, ['view', 'update']);
 
-        $this->forceVisit($shelf->getUrl('/delete'))
-            ->see('You do not have permission')->seePageIs('/');
+        $this->get($shelf->getUrl('/delete'))->assertRedirect('/');
+        $this->get('/')->assertSee('You do not have permission');
 
         $this->setRestrictionsForTestRoles($shelf, ['view', 'delete']);
 
-        $this->visit($shelf->getUrl('/delete'))
-            ->seePageIs($shelf->getUrl('/delete'))->see('Delete Book');
+        $this->get($shelf->getUrl('/delete'))
+            ->assertOk()
+            ->assertSee('Delete Book');
     }
 
     public function test_book_view_restriction()
     {
-        $book = Book::first();
+        /** @var Book $book */
+        $book = Book::query()->first();
         $bookPage = $book->pages->first();
         $bookChapter = $book->chapters->first();
 
         $bookUrl = $book->getUrl();
         $this->actingAs($this->user)
-            ->visit($bookUrl)
-            ->seePageIs($bookUrl);
+            ->get($bookUrl)
+            ->assertOk();
 
         $this->setRestrictionsForTestRoles($book, []);
 
-        $this->forceVisit($bookUrl)
-            ->see('Book not found');
-        $this->forceVisit($bookPage->getUrl())
-            ->see('Page not found');
-        $this->forceVisit($bookChapter->getUrl())
-            ->see('Chapter not found');
+        $this->followingRedirects()->get($bookUrl)
+            ->assertSee('Book not found');
+        $this->followingRedirects()->get($bookPage->getUrl())
+            ->assertSee('Page not found');
+        $this->followingRedirects()->get($bookChapter->getUrl())
+            ->assertSee('Chapter not found');
 
         $this->setRestrictionsForTestRoles($book, ['view']);
 
-        $this->visit($bookUrl)
-            ->see($book->name);
-        $this->visit($bookPage->getUrl())
-            ->see($bookPage->name);
-        $this->visit($bookChapter->getUrl())
-            ->see($bookChapter->name);
+        $this->get($bookUrl)
+            ->assertSee($book->name);
+        $this->get($bookPage->getUrl())
+            ->assertSee($bookPage->name);
+        $this->get($bookChapter->getUrl())
+            ->assertSee($bookChapter->name);
     }
 
     public function test_book_create_restriction()
     {
-        $book = Book::first();
+        /** @var Book $book */
+        $book = Book::query()->first();
 
         $bookUrl = $book->getUrl();
         $this->actingAs($this->viewer)
-            ->visit($bookUrl)
-            ->dontSeeInElement('.actions', 'New Page')
-            ->dontSeeInElement('.actions', 'New Chapter');
+            ->get($bookUrl)
+            ->assertElementNotContains('.actions', 'New Page')
+            ->assertElementNotContains('.actions', 'New Chapter');
         $this->actingAs($this->user)
-            ->visit($bookUrl)
-            ->seeInElement('.actions', 'New Page')
-            ->seeInElement('.actions', 'New Chapter');
+            ->get($bookUrl)
+            ->assertElementContains('.actions', 'New Page')
+            ->assertElementContains('.actions', 'New Chapter');
 
         $this->setRestrictionsForTestRoles($book, ['view', 'delete', 'update']);
 
-        $this->forceVisit($bookUrl . '/create-chapter')
-            ->see('You do not have permission')->seePageIs('/');
-        $this->forceVisit($bookUrl . '/create-page')
-            ->see('You do not have permission')->seePageIs('/');
-        $this->visit($bookUrl)->dontSeeInElement('.actions', 'New Page')
-            ->dontSeeInElement('.actions', 'New Chapter');
+        $this->get($bookUrl . '/create-chapter')->assertRedirect('/');
+        $this->get('/')->assertSee('You do not have permission');
+
+        $this->get($bookUrl . '/create-page')->assertRedirect('/');
+        $this->get('/')->assertSee('You do not have permission');
+
+        $this->get($bookUrl)
+            ->assertElementNotContains('.actions', 'New Page')
+            ->assertElementNotContains('.actions', 'New Chapter');
 
         $this->setRestrictionsForTestRoles($book, ['view', 'create']);
 
-        $this->visit($bookUrl . '/create-chapter')
-            ->type('test chapter', 'name')
-            ->type('test description for chapter', 'description')
-            ->press('Save Chapter')
-            ->seePageIs($bookUrl . '/chapter/test-chapter');
-        $this->visit($bookUrl . '/create-page')
-            ->type('test page', 'name')
-            ->type('test content', 'html')
-            ->press('Save Page')
-            ->seePageIs($bookUrl . '/page/test-page');
-        $this->visit($bookUrl)->seeInElement('.actions', 'New Page')
-            ->seeInElement('.actions', 'New Chapter');
+        $resp = $this->post($book->getUrl('/create-chapter'), [
+            'name'        => 'test chapter',
+            'description' => 'desc',
+        ]);
+        $resp->assertRedirect($book->getUrl('/chapter/test-chapter'));
+
+        $this->get($book->getUrl('/create-page'));
+        /** @var Page $page */
+        $page = Page::query()->where('draft', '=', true)->orderBy('id', 'desc')->first();
+        $resp = $this->post($page->getUrl(), [
+            'name' => 'test page',
+            'html' => 'test content',
+        ]);
+        $resp->assertRedirect($book->getUrl('/page/test-page'));
+
+        $this->get($bookUrl)
+            ->assertElementContains('.actions', 'New Page')
+            ->assertElementContains('.actions', 'New Chapter');
     }
 
     public function test_book_update_restriction()
     {
-        $book = Book::first();
+        /** @var Book $book */
+        $book = Book::query()->first();
         $bookPage = $book->pages->first();
         $bookChapter = $book->chapters->first();
 
         $bookUrl = $book->getUrl();
         $this->actingAs($this->user)
-            ->visit($bookUrl . '/edit')
-            ->see('Edit Book');
+            ->get($bookUrl . '/edit')
+            ->assertSee('Edit Book');
 
         $this->setRestrictionsForTestRoles($book, ['view', 'delete']);
 
-        $this->forceVisit($bookUrl . '/edit')
-            ->see('You do not have permission')->seePageIs('/');
-        $this->forceVisit($bookPage->getUrl() . '/edit')
-            ->see('You do not have permission')->seePageIs('/');
-        $this->forceVisit($bookChapter->getUrl() . '/edit')
-            ->see('You do not have permission')->seePageIs('/');
+        $this->get($bookUrl . '/edit')->assertRedirect('/');
+        $this->get('/')->assertSee('You do not have permission');
+        $this->get($bookPage->getUrl() . '/edit')->assertRedirect('/');
+        $this->get('/')->assertSee('You do not have permission');
+        $this->get($bookChapter->getUrl() . '/edit')->assertRedirect('/');
+        $this->get('/')->assertSee('You do not have permission');
 
         $this->setRestrictionsForTestRoles($book, ['view', 'update']);
 
-        $this->visit($bookUrl . '/edit')
-            ->seePageIs($bookUrl . '/edit');
-        $this->visit($bookPage->getUrl() . '/edit')
-            ->seePageIs($bookPage->getUrl() . '/edit');
-        $this->visit($bookChapter->getUrl() . '/edit')
-            ->see('Edit Chapter');
+        $this->get($bookUrl . '/edit')->assertOk();
+        $this->get($bookPage->getUrl() . '/edit')->assertOk();
+        $this->get($bookChapter->getUrl() . '/edit')->assertSee('Edit Chapter');
     }
 
     public function test_book_delete_restriction()
     {
-        $book = Book::first();
+        /** @var Book $book */
+        $book = Book::query()->first();
         $bookPage = $book->pages->first();
         $bookChapter = $book->chapters->first();
 
         $bookUrl = $book->getUrl();
-        $this->actingAs($this->user)
-            ->visit($bookUrl . '/delete')
-            ->see('Delete Book');
+        $this->actingAs($this->user)->get($bookUrl . '/delete')
+            ->assertSee('Delete Book');
 
         $this->setRestrictionsForTestRoles($book, ['view', 'update']);
 
-        $this->forceVisit($bookUrl . '/delete')
-            ->see('You do not have permission')->seePageIs('/');
-        $this->forceVisit($bookPage->getUrl() . '/delete')
-            ->see('You do not have permission')->seePageIs('/');
-        $this->forceVisit($bookChapter->getUrl() . '/delete')
-            ->see('You do not have permission')->seePageIs('/');
+        $this->get($bookUrl . '/delete')->assertRedirect('/');
+        $this->get('/')->assertSee('You do not have permission');
+        $this->get($bookPage->getUrl() . '/delete')->assertRedirect('/');
+        $this->get('/')->assertSee('You do not have permission');
+        $this->get($bookChapter->getUrl() . '/delete')->assertRedirect('/');
+        $this->get('/')->assertSee('You do not have permission');
 
         $this->setRestrictionsForTestRoles($book, ['view', 'delete']);
 
-        $this->visit($bookUrl . '/delete')
-            ->seePageIs($bookUrl . '/delete')->see('Delete Book');
-        $this->visit($bookPage->getUrl() . '/delete')
-            ->seePageIs($bookPage->getUrl() . '/delete')->see('Delete Page');
-        $this->visit($bookChapter->getUrl() . '/delete')
-            ->see('Delete Chapter');
+        $this->get($bookUrl . '/delete')->assertOk()->assertSee('Delete Book');
+        $this->get($bookPage->getUrl('/delete'))->assertOk()->assertSee('Delete Page');
+        $this->get($bookChapter->getUrl('/delete'))->assertSee('Delete Chapter');
     }
 
     public function test_chapter_view_restriction()
     {
-        $chapter = Chapter::first();
+        /** @var Chapter $chapter */
+        $chapter = Chapter::query()->first();
         $chapterPage = $chapter->pages->first();
 
         $chapterUrl = $chapter->getUrl();
-        $this->actingAs($this->user)
-            ->visit($chapterUrl)
-            ->seePageIs($chapterUrl);
+        $this->actingAs($this->user)->get($chapterUrl)->assertOk();
 
         $this->setRestrictionsForTestRoles($chapter, []);
 
-        $this->forceVisit($chapterUrl)
-            ->see('Chapter not found');
-        $this->forceVisit($chapterPage->getUrl())
-            ->see('Page not found');
+        $this->followingRedirects()->get($chapterUrl)->assertSee('Chapter not found');
+        $this->followingRedirects()->get($chapterPage->getUrl())->assertSee('Page not found');
 
         $this->setRestrictionsForTestRoles($chapter, ['view']);
 
-        $this->visit($chapterUrl)
-            ->see($chapter->name);
-        $this->visit($chapterPage->getUrl())
-            ->see($chapterPage->name);
+        $this->get($chapterUrl)->assertSee($chapter->name);
+        $this->get($chapterPage->getUrl())->assertSee($chapterPage->name);
     }
 
     public function test_chapter_create_restriction()
     {
-        $chapter = Chapter::first();
+        /** @var Chapter $chapter */
+        $chapter = Chapter::query()->first();
 
         $chapterUrl = $chapter->getUrl();
         $this->actingAs($this->user)
-            ->visit($chapterUrl)
-            ->seeInElement('.actions', 'New Page');
+            ->get($chapterUrl)
+            ->assertElementContains('.actions', 'New Page');
 
         $this->setRestrictionsForTestRoles($chapter, ['view', 'delete', 'update']);
 
-        $this->forceVisit($chapterUrl . '/create-page')
-            ->see('You do not have permission')->seePageIs('/');
-        $this->visit($chapterUrl)->dontSeeInElement('.actions', 'New Page');
+        $this->get($chapterUrl . '/create-page')->assertRedirect('/');
+        $this->get('/')->assertSee('You do not have permission');
+        $this->get($chapterUrl)->assertElementNotContains('.actions', 'New Page');
 
         $this->setRestrictionsForTestRoles($chapter, ['view', 'create']);
 
-        $this->visit($chapterUrl . '/create-page')
-            ->type('test page', 'name')
-            ->type('test content', 'html')
-            ->press('Save Page')
-            ->seePageIs($chapter->book->getUrl() . '/page/test-page');
+        $this->get($chapter->getUrl('/create-page'));
+        /** @var Page $page */
+        $page = Page::query()->where('draft', '=', true)->orderBy('id', 'desc')->first();
+        $resp = $this->post($page->getUrl(), [
+            'name' => 'test page',
+            'html' => 'test content',
+        ]);
+        $resp->assertRedirect($chapter->book->getUrl('/page/test-page'));
 
-        $this->visit($chapterUrl)->seeInElement('.actions', 'New Page');
+        $this->get($chapterUrl)->assertElementContains('.actions', 'New Page');
     }
 
     public function test_chapter_update_restriction()
     {
-        $chapter = Chapter::first();
+        /** @var Chapter $chapter */
+        $chapter = Chapter::query()->first();
         $chapterPage = $chapter->pages->first();
 
         $chapterUrl = $chapter->getUrl();
-        $this->actingAs($this->user)
-            ->visit($chapterUrl . '/edit')
-            ->see('Edit Chapter');
+        $this->actingAs($this->user)->get($chapterUrl . '/edit')
+            ->assertSee('Edit Chapter');
 
         $this->setRestrictionsForTestRoles($chapter, ['view', 'delete']);
 
-        $this->forceVisit($chapterUrl . '/edit')
-            ->see('You do not have permission')->seePageIs('/');
-        $this->forceVisit($chapterPage->getUrl() . '/edit')
-            ->see('You do not have permission')->seePageIs('/');
+        $this->get($chapterUrl . '/edit')->assertRedirect('/');
+        $this->get('/')->assertSee('You do not have permission');
+        $this->get($chapterPage->getUrl() . '/edit')->assertRedirect('/');
+        $this->get('/')->assertSee('You do not have permission');
 
         $this->setRestrictionsForTestRoles($chapter, ['view', 'update']);
 
-        $this->visit($chapterUrl . '/edit')
-            ->seePageIs($chapterUrl . '/edit')->see('Edit Chapter');
-        $this->visit($chapterPage->getUrl() . '/edit')
-            ->seePageIs($chapterPage->getUrl() . '/edit');
+        $this->get($chapterUrl . '/edit')->assertOk()->assertSee('Edit Chapter');
+        $this->get($chapterPage->getUrl() . '/edit')->assertOk();
     }
 
     public function test_chapter_delete_restriction()
     {
-        $chapter = Chapter::first();
+        /** @var Chapter $chapter */
+        $chapter = Chapter::query()->first();
         $chapterPage = $chapter->pages->first();
 
         $chapterUrl = $chapter->getUrl();
         $this->actingAs($this->user)
-            ->visit($chapterUrl . '/delete')
-            ->see('Delete Chapter');
+            ->get($chapterUrl . '/delete')
+            ->assertSee('Delete Chapter');
 
         $this->setRestrictionsForTestRoles($chapter, ['view', 'update']);
 
-        $this->forceVisit($chapterUrl . '/delete')
-            ->see('You do not have permission')->seePageIs('/');
-        $this->forceVisit($chapterPage->getUrl() . '/delete')
-            ->see('You do not have permission')->seePageIs('/');
+        $this->get($chapterUrl . '/delete')->assertRedirect('/');
+        $this->get('/')->assertSee('You do not have permission');
+        $this->get($chapterPage->getUrl() . '/delete')->assertRedirect('/');
+        $this->get('/')->assertSee('You do not have permission');
 
         $this->setRestrictionsForTestRoles($chapter, ['view', 'delete']);
 
-        $this->visit($chapterUrl . '/delete')
-            ->seePageIs($chapterUrl . '/delete')->see('Delete Chapter');
-        $this->visit($chapterPage->getUrl() . '/delete')
-            ->seePageIs($chapterPage->getUrl() . '/delete')->see('Delete Page');
+        $this->get($chapterUrl . '/delete')->assertOk()->assertSee('Delete Chapter');
+        $this->get($chapterPage->getUrl() . '/delete')->assertOk()->assertSee('Delete Page');
     }
 
     public function test_page_view_restriction()
     {
-        $page = Page::first();
+        /** @var Page $page */
+        $page = Page::query()->first();
 
         $pageUrl = $page->getUrl();
-        $this->actingAs($this->user)
-            ->visit($pageUrl)
-            ->seePageIs($pageUrl);
+        $this->actingAs($this->user)->get($pageUrl)->assertOk();
 
         $this->setRestrictionsForTestRoles($page, ['update', 'delete']);
 
-        $this->forceVisit($pageUrl)
-            ->see('Page not found');
+        $this->get($pageUrl)->assertSee('Page not found');
 
         $this->setRestrictionsForTestRoles($page, ['view']);
 
-        $this->visit($pageUrl)
-            ->see($page->name);
+        $this->get($pageUrl)->assertSee($page->name);
     }
 
     public function test_page_update_restriction()
     {
-        $page = Chapter::first();
+        /** @var Page $page */
+        $page = Page::query()->first();
 
         $pageUrl = $page->getUrl();
         $this->actingAs($this->user)
-            ->visit($pageUrl . '/edit')
-            ->seeInField('name', $page->name);
+            ->get($pageUrl . '/edit')
+            ->assertElementExists('input[name="name"][value="' . $page->name . '"]');
 
         $this->setRestrictionsForTestRoles($page, ['view', 'delete']);
 
-        $this->forceVisit($pageUrl . '/edit')
-            ->see('You do not have permission')->seePageIs('/');
+        $this->get($pageUrl . '/edit')->assertRedirect('/');
+        $this->get('/')->assertSee('You do not have permission');
 
         $this->setRestrictionsForTestRoles($page, ['view', 'update']);
 
-        $this->visit($pageUrl . '/edit')
-            ->seePageIs($pageUrl . '/edit')->seeInField('name', $page->name);
+        $this->get($pageUrl . '/edit')
+            ->assertOk()
+            ->assertElementExists('input[name="name"][value="' . $page->name . '"]');
     }
 
     public function test_page_delete_restriction()
     {
-        $page = Page::first();
+        /** @var Page $page */
+        $page = Page::query()->first();
 
         $pageUrl = $page->getUrl();
         $this->actingAs($this->user)
-            ->visit($pageUrl . '/delete')
-            ->see('Delete Page');
+            ->get($pageUrl . '/delete')
+            ->assertSee('Delete Page');
 
         $this->setRestrictionsForTestRoles($page, ['view', 'update']);
 
-        $this->forceVisit($pageUrl . '/delete')
-            ->see('You do not have permission')->seePageIs('/');
+        $this->get($pageUrl . '/delete')->assertRedirect('/');
+        $this->get('/')->assertSee('You do not have permission');
 
         $this->setRestrictionsForTestRoles($page, ['view', 'delete']);
 
-        $this->visit($pageUrl . '/delete')
-            ->seePageIs($pageUrl . '/delete')->see('Delete Page');
+        $this->get($pageUrl . '/delete')->assertOk()->assertSee('Delete Page');
+    }
+
+    protected function entityRestrictionFormTest(string $model, string $title, string $permission, string $roleId)
+    {
+        /** @var Entity $modelInstance */
+        $modelInstance = $model::query()->first();
+        $this->asAdmin()->get($modelInstance->getUrl('/permissions'))
+            ->assertSee($title);
+
+        $this->put($modelInstance->getUrl('/permissions'), [
+            'restricted'   => 'true',
+            'restrictions' => [
+                $roleId => [
+                    $permission => 'true',
+                ],
+            ],
+        ]);
+
+        $this->assertDatabaseHas($modelInstance->getTable(), ['id' => $modelInstance->id, 'restricted' => true]);
+        $this->assertDatabaseHas('entity_permissions', [
+            'restrictable_id'   => $modelInstance->id,
+            'restrictable_type' => $modelInstance->getMorphClass(),
+            'role_id'           => $roleId,
+            'action'            => $permission,
+        ]);
     }
 
     public function test_bookshelf_restriction_form()
     {
-        $shelf = Bookshelf::first();
-        $this->asAdmin()->visit($shelf->getUrl('/permissions'))
-            ->see('Bookshelf Permissions')
-            ->check('restricted')
-            ->check('restrictions[2][view]')
-            ->press('Save Permissions')
-            ->seeInDatabase('bookshelves', ['id' => $shelf->id, 'restricted' => true])
-            ->seeInDatabase('entity_permissions', [
-                'restrictable_id'   => $shelf->id,
-                'restrictable_type' => Bookshelf::newModelInstance()->getMorphClass(),
-                'role_id'           => '2',
-                'action'            => 'view',
-            ]);
+        $this->entityRestrictionFormTest(Bookshelf::class, 'Bookshelf Permissions', 'view', '2');
     }
 
     public function test_book_restriction_form()
     {
-        $book = Book::first();
-        $this->asAdmin()->visit($book->getUrl() . '/permissions')
-            ->see('Book Permissions')
-            ->check('restricted')
-            ->check('restrictions[2][view]')
-            ->press('Save Permissions')
-            ->seeInDatabase('books', ['id' => $book->id, 'restricted' => true])
-            ->seeInDatabase('entity_permissions', [
-                'restrictable_id'   => $book->id,
-                'restrictable_type' => Book::newModelInstance()->getMorphClass(),
-                'role_id'           => '2',
-                'action'            => 'view',
-            ]);
+        $this->entityRestrictionFormTest(Book::class, 'Book Permissions', 'view', '2');
     }
 
     public function test_chapter_restriction_form()
     {
-        $chapter = Chapter::first();
-        $this->asAdmin()->visit($chapter->getUrl() . '/permissions')
-            ->see('Chapter Permissions')
-            ->check('restricted')
-            ->check('restrictions[2][update]')
-            ->press('Save Permissions')
-            ->seeInDatabase('chapters', ['id' => $chapter->id, 'restricted' => true])
-            ->seeInDatabase('entity_permissions', [
-                'restrictable_id'   => $chapter->id,
-                'restrictable_type' => Chapter::newModelInstance()->getMorphClass(),
-                'role_id'           => '2',
-                'action'            => 'update',
-            ]);
+        $this->entityRestrictionFormTest(Chapter::class, 'Chapter Permissions', 'update', '2');
     }
 
     public function test_page_restriction_form()
     {
-        $page = Page::first();
-        $this->asAdmin()->visit($page->getUrl() . '/permissions')
-            ->see('Page Permissions')
-            ->check('restricted')
-            ->check('restrictions[2][delete]')
-            ->press('Save Permissions')
-            ->seeInDatabase('pages', ['id' => $page->id, 'restricted' => true])
-            ->seeInDatabase('entity_permissions', [
-                'restrictable_id'   => $page->id,
-                'restrictable_type' => Page::newModelInstance()->getMorphClass(),
-                'role_id'           => '2',
-                'action'            => 'delete',
-            ]);
+        $this->entityRestrictionFormTest(Page::class, 'Page Permissions', 'delete', '2');
     }
 
     public function test_restricted_pages_not_visible_in_book_navigation_on_pages()
     {
-        $chapter = Chapter::first();
+        /** @var Chapter $chapter */
+        $chapter = Chapter::query()->first();
         $page = $chapter->pages->first();
         $page2 = $chapter->pages[2];
 
         $this->setRestrictionsForTestRoles($page, []);
 
         $this->actingAs($this->user)
-            ->visit($page2->getUrl())
-            ->dontSeeInElement('.sidebar-page-list', $page->name);
+            ->get($page2->getUrl())
+            ->assertElementNotContains('.sidebar-page-list', $page->name);
     }
 
     public function test_restricted_pages_not_visible_in_book_navigation_on_chapters()
     {
-        $chapter = Chapter::first();
+        /** @var Chapter $chapter */
+        $chapter = Chapter::query()->first();
         $page = $chapter->pages->first();
 
         $this->setRestrictionsForTestRoles($page, []);
 
         $this->actingAs($this->user)
-            ->visit($chapter->getUrl())
-            ->dontSeeInElement('.sidebar-page-list', $page->name);
+            ->get($chapter->getUrl())
+            ->assertElementNotContains('.sidebar-page-list', $page->name);
     }
 
     public function test_restricted_pages_not_visible_on_chapter_pages()
     {
-        $chapter = Chapter::first();
+        /** @var Chapter $chapter */
+        $chapter = Chapter::query()->first();
         $page = $chapter->pages->first();
 
         $this->setRestrictionsForTestRoles($page, []);
 
         $this->actingAs($this->user)
-            ->visit($chapter->getUrl())
-            ->dontSee($page->name);
+            ->get($chapter->getUrl())
+            ->assertDontSee($page->name);
     }
 
     public function test_restricted_chapter_pages_not_visible_on_book_page()
     {
+        /** @var Chapter $chapter */
         $chapter = Chapter::query()->first();
         $this->actingAs($this->user)
-            ->visit($chapter->book->getUrl())
-            ->see($chapter->pages->first()->name);
+            ->get($chapter->book->getUrl())
+            ->assertSee($chapter->pages->first()->name);
 
         foreach ($chapter->pages as $page) {
             $this->setRestrictionsForTestRoles($page, []);
         }
 
         $this->actingAs($this->user)
-            ->visit($chapter->book->getUrl())
-            ->dontSee($chapter->pages->first()->name);
+            ->get($chapter->book->getUrl())
+            ->assertDontSee($chapter->pages->first()->name);
     }
 
     public function test_bookshelf_update_restriction_override()
     {
-        $shelf = Bookshelf::first();
+        /** @var Bookshelf $shelf */
+        $shelf = Bookshelf::query()->first();
 
         $this->actingAs($this->viewer)
-            ->visit($shelf->getUrl('/edit'))
-            ->dontSee('Edit Book');
+            ->get($shelf->getUrl('/edit'))
+            ->assertDontSee('Edit Book');
 
         $this->setRestrictionsForTestRoles($shelf, ['view', 'delete']);
 
-        $this->forceVisit($shelf->getUrl('/edit'))
-            ->see('You do not have permission')->seePageIs('/');
+        $this->get($shelf->getUrl('/edit'))->assertRedirect('/');
+        $this->get('/')->assertSee('You do not have permission');
 
         $this->setRestrictionsForTestRoles($shelf, ['view', 'update']);
 
-        $this->visit($shelf->getUrl('/edit'))
-            ->seePageIs($shelf->getUrl('/edit'));
+        $this->get($shelf->getUrl('/edit'))->assertOk();
     }
 
     public function test_bookshelf_delete_restriction_override()
     {
-        $shelf = Bookshelf::first();
+        /** @var Bookshelf $shelf */
+        $shelf = Bookshelf::query()->first();
 
         $this->actingAs($this->viewer)
-            ->visit($shelf->getUrl('/delete'))
-            ->dontSee('Delete Book');
+            ->get($shelf->getUrl('/delete'))
+            ->assertDontSee('Delete Book');
 
         $this->setRestrictionsForTestRoles($shelf, ['view', 'update']);
 
-        $this->forceVisit($shelf->getUrl('/delete'))
-            ->see('You do not have permission')->seePageIs('/');
+        $this->get($shelf->getUrl('/delete'))->assertRedirect('/');
+        $this->get('/')->assertSee('You do not have permission');
 
         $this->setRestrictionsForTestRoles($shelf, ['view', 'delete']);
 
-        $this->visit($shelf->getUrl('/delete'))
-            ->seePageIs($shelf->getUrl('/delete'))->see('Delete Book');
+        $this->get($shelf->getUrl('/delete'))->assertOk()->assertSee('Delete Book');
     }
 
     public function test_book_create_restriction_override()
     {
-        $book = Book::first();
+        /** @var Book $book */
+        $book = Book::query()->first();
 
         $bookUrl = $book->getUrl();
         $this->actingAs($this->viewer)
-            ->visit($bookUrl)
-            ->dontSeeInElement('.actions', 'New Page')
-            ->dontSeeInElement('.actions', 'New Chapter');
+            ->get($bookUrl)
+            ->assertElementNotContains('.actions', 'New Page')
+            ->assertElementNotContains('.actions', 'New Chapter');
 
         $this->setRestrictionsForTestRoles($book, ['view', 'delete', 'update']);
 
-        $this->forceVisit($bookUrl . '/create-chapter')
-            ->see('You do not have permission')->seePageIs('/');
-        $this->forceVisit($bookUrl . '/create-page')
-            ->see('You do not have permission')->seePageIs('/');
-        $this->visit($bookUrl)->dontSeeInElement('.actions', 'New Page')
-            ->dontSeeInElement('.actions', 'New Chapter');
+        $this->get($bookUrl . '/create-chapter')->assertRedirect('/');
+        $this->get('/')->assertSee('You do not have permission');
+        $this->get($bookUrl . '/create-page')->assertRedirect('/');
+        $this->get('/')->assertSee('You do not have permission');
+        $this->get($bookUrl)->assertElementNotContains('.actions', 'New Page')
+            ->assertElementNotContains('.actions', 'New Chapter');
 
         $this->setRestrictionsForTestRoles($book, ['view', 'create']);
 
-        $this->visit($bookUrl . '/create-chapter')
-            ->type('test chapter', 'name')
-            ->type('test description for chapter', 'description')
-            ->press('Save Chapter')
-            ->seePageIs($bookUrl . '/chapter/test-chapter');
-        $this->visit($bookUrl . '/create-page')
-            ->type('test page', 'name')
-            ->type('test content', 'html')
-            ->press('Save Page')
-            ->seePageIs($bookUrl . '/page/test-page');
-        $this->visit($bookUrl)->seeInElement('.actions', 'New Page')
-            ->seeInElement('.actions', 'New Chapter');
+        $resp = $this->post($book->getUrl('/create-chapter'), [
+            'name'        => 'test chapter',
+            'description' => 'test desc',
+        ]);
+        $resp->assertRedirect($book->getUrl('/chapter/test-chapter'));
+
+        $this->get($book->getUrl('/create-page'));
+        /** @var Page $page */
+        $page = Page::query()->where('draft', '=', true)->orderByDesc('id')->first();
+        $resp = $this->post($page->getUrl(), [
+            'name' => 'test page',
+            'html' => 'test desc',
+        ]);
+        $resp->assertRedirect($book->getUrl('/page/test-page'));
+
+        $this->get($bookUrl)
+            ->assertElementContains('.actions', 'New Page')
+            ->assertElementContains('.actions', 'New Chapter');
     }
 
     public function test_book_update_restriction_override()
     {
-        $book = Book::first();
+        /** @var Book $book */
+        $book = Book::query()->first();
         $bookPage = $book->pages->first();
         $bookChapter = $book->chapters->first();
 
         $bookUrl = $book->getUrl();
-        $this->actingAs($this->viewer)
-            ->visit($bookUrl . '/edit')
-            ->dontSee('Edit Book');
+        $this->actingAs($this->viewer)->get($bookUrl . '/edit')
+            ->assertDontSee('Edit Book');
 
         $this->setRestrictionsForTestRoles($book, ['view', 'delete']);
 
-        $this->forceVisit($bookUrl . '/edit')
-            ->see('You do not have permission')->seePageIs('/');
-        $this->forceVisit($bookPage->getUrl() . '/edit')
-            ->see('You do not have permission')->seePageIs('/');
-        $this->forceVisit($bookChapter->getUrl() . '/edit')
-            ->see('You do not have permission')->seePageIs('/');
+        $this->get($bookUrl . '/edit')->assertRedirect('/');
+        $this->get('/')->assertSee('You do not have permission');
+        $this->get($bookPage->getUrl() . '/edit')->assertRedirect('/');
+        $this->get('/')->assertSee('You do not have permission');
+        $this->get($bookChapter->getUrl() . '/edit')->assertRedirect('/');
+        $this->get('/')->assertSee('You do not have permission');
 
         $this->setRestrictionsForTestRoles($book, ['view', 'update']);
 
-        $this->visit($bookUrl . '/edit')
-            ->seePageIs($bookUrl . '/edit');
-        $this->visit($bookPage->getUrl() . '/edit')
-            ->seePageIs($bookPage->getUrl() . '/edit');
-        $this->visit($bookChapter->getUrl() . '/edit')
-            ->see('Edit Chapter');
+        $this->get($bookUrl . '/edit')->assertOk();
+        $this->get($bookPage->getUrl() . '/edit')->assertOk();
+        $this->get($bookChapter->getUrl() . '/edit')->assertSee('Edit Chapter');
     }
 
     public function test_book_delete_restriction_override()
     {
-        $book = Book::first();
+        /** @var Book $book */
+        $book = Book::query()->first();
         $bookPage = $book->pages->first();
         $bookChapter = $book->chapters->first();
 
         $bookUrl = $book->getUrl();
         $this->actingAs($this->viewer)
-            ->visit($bookUrl . '/delete')
-            ->dontSee('Delete Book');
+            ->get($bookUrl . '/delete')
+            ->assertDontSee('Delete Book');
 
         $this->setRestrictionsForTestRoles($book, ['view', 'update']);
 
-        $this->forceVisit($bookUrl . '/delete')
-            ->see('You do not have permission')->seePageIs('/');
-        $this->forceVisit($bookPage->getUrl() . '/delete')
-            ->see('You do not have permission')->seePageIs('/');
-        $this->forceVisit($bookChapter->getUrl() . '/delete')
-            ->see('You do not have permission')->seePageIs('/');
+        $this->get($bookUrl . '/delete')->assertRedirect('/');
+        $this->get('/')->assertSee('You do not have permission');
+        $this->get($bookPage->getUrl() . '/delete')->assertRedirect('/');
+        $this->get('/')->assertSee('You do not have permission');
+        $this->get($bookChapter->getUrl() . '/delete')->assertRedirect('/');
+        $this->get('/')->assertSee('You do not have permission');
 
         $this->setRestrictionsForTestRoles($book, ['view', 'delete']);
 
-        $this->visit($bookUrl . '/delete')
-            ->seePageIs($bookUrl . '/delete')->see('Delete Book');
-        $this->visit($bookPage->getUrl() . '/delete')
-            ->seePageIs($bookPage->getUrl() . '/delete')->see('Delete Page');
-        $this->visit($bookChapter->getUrl() . '/delete')
-            ->see('Delete Chapter');
+        $this->get($bookUrl . '/delete')->assertOk()->assertSee('Delete Book');
+        $this->get($bookPage->getUrl() . '/delete')->assertOk()->assertSee('Delete Page');
+        $this->get($bookChapter->getUrl() . '/delete')->assertSee('Delete Chapter');
     }
 
     public function test_page_visible_if_has_permissions_when_book_not_visible()
     {
-        $book = Book::first();
+        /** @var Book $book */
+        $book = Book::query()->first();
         $bookChapter = $book->chapters->first();
         $bookPage = $bookChapter->pages->first();
 
@@ -655,34 +645,37 @@ class EntityPermissionsTest extends BrowserKitTest
         $this->setRestrictionsForTestRoles($bookPage, ['view']);
 
         $this->actingAs($this->viewer);
-        $this->get($bookPage->getUrl());
-        $this->assertResponseOk();
-        $this->see($bookPage->name);
-        $this->dontSee(substr($book->name, 0, 15));
-        $this->dontSee(substr($bookChapter->name, 0, 15));
+        $resp = $this->get($bookPage->getUrl());
+        $resp->assertOk();
+        $resp->assertSee($bookPage->name);
+        $resp->assertDontSee(substr($book->name, 0, 15));
+        $resp->assertDontSee(substr($bookChapter->name, 0, 15));
     }
 
     public function test_book_sort_view_permission()
     {
-        $firstBook = Book::first();
-        $secondBook = Book::find(2);
+        /** @var Book $firstBook */
+        $firstBook = Book::query()->first();
+        /** @var Book $secondBook */
+        $secondBook = Book::query()->find(2);
 
         $this->setRestrictionsForTestRoles($firstBook, ['view', 'update']);
         $this->setRestrictionsForTestRoles($secondBook, ['view']);
 
         // Test sort page visibility
-        $this->actingAs($this->user)->visit($secondBook->getUrl() . '/sort')
-                ->see('You do not have permission')
-                ->seePageIs('/');
+        $this->actingAs($this->user)->get($secondBook->getUrl('/sort'))->assertRedirect('/');
+        $this->get('/')->assertSee('You do not have permission');
 
         // Check sort page on first book
-        $this->actingAs($this->user)->visit($firstBook->getUrl() . '/sort');
+        $this->actingAs($this->user)->get($firstBook->getUrl('/sort'));
     }
 
     public function test_book_sort_permission()
     {
-        $firstBook = Book::first();
-        $secondBook = Book::find(2);
+        /** @var Book $firstBook */
+        $firstBook = Book::query()->first();
+        /** @var Book $secondBook */
+        $secondBook = Book::query()->find(2);
 
         $this->setRestrictionsForTestRoles($firstBook, ['view', 'update']);
         $this->setRestrictionsForTestRoles($secondBook, ['view']);
@@ -703,9 +696,8 @@ class EntityPermissionsTest extends BrowserKitTest
 
         // Move chapter from first book to a second book
         $this->actingAs($this->user)->put($firstBook->getUrl() . '/sort', ['sort-tree' => json_encode($reqData)])
-                ->followRedirects()
-                ->see('You do not have permission')
-                ->seePageIs('/');
+            ->assertRedirect('/');
+        $this->get('/')->assertSee('You do not have permission');
 
         $reqData = [
             [
@@ -719,30 +711,30 @@ class EntityPermissionsTest extends BrowserKitTest
 
         // Move chapter from second book to first book
         $this->actingAs($this->user)->put($firstBook->getUrl() . '/sort', ['sort-tree' => json_encode($reqData)])
-                ->followRedirects()
-                ->see('You do not have permission')
-                ->seePageIs('/');
+                ->assertRedirect('/');
+        $this->get('/')->assertSee('You do not have permission');
     }
 
     public function test_can_create_page_if_chapter_has_permissions_when_book_not_visible()
     {
-        $book = Book::first();
+        /** @var Book $book */
+        $book = Book::query()->first();
         $this->setRestrictionsForTestRoles($book, []);
         $bookChapter = $book->chapters->first();
         $this->setRestrictionsForTestRoles($bookChapter, ['view']);
 
-        $this->actingAs($this->user)->visit($bookChapter->getUrl())
-            ->dontSee('New Page');
+        $this->actingAs($this->user)->get($bookChapter->getUrl())
+            ->assertDontSee('New Page');
 
         $this->setRestrictionsForTestRoles($bookChapter, ['view', 'create']);
 
-        $this->actingAs($this->user)->visit($bookChapter->getUrl())
-            ->click('New Page')
-            ->seeStatusCode(200)
-            ->type('test page', 'name')
-            ->type('test content', 'html')
-            ->press('Save Page')
-            ->seePageIs($book->getUrl('/page/test-page'))
-            ->seeStatusCode(200);
+        $this->get($bookChapter->getUrl('/create-page'));
+        /** @var Page $page */
+        $page = Page::query()->where('draft', '=', true)->orderByDesc('id')->first();
+        $resp = $this->post($page->getUrl(), [
+            'name' => 'test page',
+            'html' => 'test content',
+        ]);
+        $resp->assertRedirect($book->getUrl('/page/test-page'));
     }
 }
index 09c3233e38c3dd77a6d758be55d068b637e15b2c..5248ae1528ffbb509d62c0a2691b9f88aca06c85 100644 (file)
@@ -2,18 +2,20 @@
 
 namespace Tests\Permissions;
 
+use BookStack\Actions\ActivityType;
 use BookStack\Actions\Comment;
 use BookStack\Auth\Role;
 use BookStack\Auth\User;
 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\Uploads\Image;
-use Laravel\BrowserKitTesting\HttpException;
-use Tests\BrowserKitTest;
+use Tests\TestCase;
+use Tests\TestResponse;
 
-class RolesTest extends BrowserKitTest
+class RolesTest extends TestCase
 {
     protected $user;
 
@@ -25,17 +27,17 @@ class RolesTest extends BrowserKitTest
 
     public function test_admin_can_see_settings()
     {
-        $this->asAdmin()->visit('/settings')->see('Settings');
+        $this->asAdmin()->get('/settings')->assertSee('Settings');
     }
 
     public function test_cannot_delete_admin_role()
     {
         $adminRole = Role::getRole('admin');
         $deletePageUrl = '/settings/roles/delete/' . $adminRole->id;
-        $this->asAdmin()->visit($deletePageUrl)
-            ->press('Confirm')
-            ->seePageIs($deletePageUrl)
-            ->see('cannot be deleted');
+
+        $this->asAdmin()->get($deletePageUrl);
+        $this->delete($deletePageUrl)->assertRedirect($deletePageUrl);
+        $this->get($deletePageUrl)->assertSee('cannot be deleted');
     }
 
     public function test_role_cannot_be_deleted_if_default()
@@ -44,10 +46,9 @@ class RolesTest extends BrowserKitTest
         $this->setSettings(['registration-role' => $newRole->id]);
 
         $deletePageUrl = '/settings/roles/delete/' . $newRole->id;
-        $this->asAdmin()->visit($deletePageUrl)
-            ->press('Confirm')
-            ->seePageIs($deletePageUrl)
-            ->see('cannot be deleted');
+        $this->asAdmin()->get($deletePageUrl);
+        $this->delete($deletePageUrl)->assertRedirect($deletePageUrl);
+        $this->get($deletePageUrl)->assertSee('cannot be deleted');
     }
 
     public function test_role_create_update_delete_flow()
@@ -57,67 +58,104 @@ class RolesTest extends BrowserKitTest
         $testRoleUpdateName = 'An Super Updated role';
 
         // Creation
-        $this->asAdmin()->visit('/settings')
-            ->click('Roles')
-            ->seePageIs('/settings/roles')
-            ->click('Create New Role')
-            ->type('Test Role', 'display_name')
-            ->type('A little test description', 'description')
-            ->press('Save Role')
-            ->seeInDatabase('roles', ['display_name' => $testRoleName, 'description' => $testRoleDesc])
-            ->seePageIs('/settings/roles');
+        $resp = $this->asAdmin()->get('/settings');
+        $resp->assertElementContains('a[href="' . url('/settings/roles') . '"]', 'Roles');
+
+        $resp = $this->get('/settings/roles');
+        $resp->assertElementContains('a[href="' . url('/settings/roles/new') . '"]', 'Create New Role');
+
+        $resp = $this->get('/settings/roles/new');
+        $resp->assertElementContains('form[action="' . url('/settings/roles/new') . '"]', 'Save Role');
+
+        $resp = $this->post('/settings/roles/new', [
+            'display_name' => $testRoleName,
+            'description'  => $testRoleDesc,
+        ]);
+        $resp->assertRedirect('/settings/roles');
+
+        $resp = $this->get('/settings/roles');
+        $resp->assertSee($testRoleName);
+        $resp->assertSee($testRoleDesc);
+        $this->assertDatabaseHas('roles', [
+            'display_name' => $testRoleName,
+            'description'  => $testRoleDesc,
+            'mfa_enforced' => false,
+        ]);
+
+        /** @var Role $role */
+        $role = Role::query()->where('display_name', '=', $testRoleName)->first();
+
         // Updating
-        $this->asAdmin()->visit('/settings/roles')
-            ->see($testRoleDesc)
-            ->click($testRoleName)
-            ->type($testRoleUpdateName, '#display_name')
-            ->press('Save Role')
-            ->seeInDatabase('roles', ['display_name' => $testRoleUpdateName, 'description' => $testRoleDesc])
-            ->seePageIs('/settings/roles');
+        $resp = $this->get('/settings/roles/' . $role->id);
+        $resp->assertSee($testRoleName);
+        $resp->assertSee($testRoleDesc);
+        $resp->assertElementContains('form[action="' . url('/settings/roles/' . $role->id) . '"]', 'Save Role');
+
+        $resp = $this->put('/settings/roles/' . $role->id, [
+            'display_name' => $testRoleUpdateName,
+            'description'  => $testRoleDesc,
+            'mfa_enforced' => 'true',
+        ]);
+        $resp->assertRedirect('/settings/roles');
+        $this->assertDatabaseHas('roles', [
+            'display_name' => $testRoleUpdateName,
+            'description'  => $testRoleDesc,
+            'mfa_enforced' => true,
+        ]);
+
         // Deleting
-        $this->asAdmin()->visit('/settings/roles')
-            ->click($testRoleUpdateName)
-            ->click('Delete Role')
-            ->see($testRoleUpdateName)
-            ->press('Confirm')
-            ->seePageIs('/settings/roles')
-            ->dontSee($testRoleUpdateName);
+        $resp = $this->get('/settings/roles/' . $role->id);
+        $resp->assertElementContains('a[href="' . url("/settings/roles/delete/$role->id") . '"]', 'Delete Role');
+
+        $resp = $this->get("/settings/roles/delete/$role->id");
+        $resp->assertSee($testRoleUpdateName);
+        $resp->assertElementContains('form[action="' . url("/settings/roles/delete/$role->id") . '"]', 'Confirm');
+
+        $resp = $this->delete("/settings/roles/delete/$role->id");
+        $resp->assertRedirect('/settings/roles');
+        $this->get('/settings/roles')->assertSee('Role successfully deleted');
+        $this->assertActivityExists(ActivityType::ROLE_DELETE);
     }
 
-    public function test_admin_role_cannot_be_removed_if_last_admin()
+    public function test_admin_role_cannot_be_removed_if_user_last_admin()
     {
-        $adminRole = Role::where('system_name', '=', 'admin')->first();
+        /** @var Role $adminRole */
+        $adminRole = Role::query()->where('system_name', '=', 'admin')->first();
         $adminUser = $this->getAdmin();
         $adminRole->users()->where('id', '!=', $adminUser->id)->delete();
-        $this->assertEquals($adminRole->users()->count(), 1);
+        $this->assertEquals(1, $adminRole->users()->count());
 
         $viewerRole = $this->getViewer()->roles()->first();
 
         $editUrl = '/settings/users/' . $adminUser->id;
-        $this->actingAs($adminUser)->put($editUrl, [
+        $resp = $this->actingAs($adminUser)->put($editUrl, [
             'name'  => $adminUser->name,
             'email' => $adminUser->email,
             'roles' => [
                 'viewer' => strval($viewerRole->id),
             ],
-        ])->followRedirects();
+        ]);
+
+        $resp->assertRedirect($editUrl);
 
-        $this->seePageIs($editUrl);
-        $this->see('This user is the only user assigned to the administrator role');
+        $resp = $this->get($editUrl);
+        $resp->assertSee('This user is the only user assigned to the administrator role');
     }
 
     public function test_migrate_users_on_delete_works()
     {
+        /** @var Role $roleA */
         $roleA = Role::query()->create(['display_name' => 'Delete Test A']);
+        /** @var Role $roleB */
         $roleB = Role::query()->create(['display_name' => 'Delete Test B']);
         $this->user->attachRole($roleB);
 
         $this->assertCount(0, $roleA->users()->get());
         $this->assertCount(1, $roleB->users()->get());
 
-        $deletePage = $this->asAdmin()->get("/settings/roles/delete/{$roleB->id}");
-        $deletePage->seeElement('select[name=migrate_role_id]');
-        $this->asAdmin()->delete("/settings/roles/delete/{$roleB->id}", [
+        $deletePage = $this->asAdmin()->get("/settings/roles/delete/$roleB->id");
+        $deletePage->assertElementExists('select[name=migrate_role_id]');
+        $this->asAdmin()->delete("/settings/roles/delete/$roleB->id", [
             'migrate_role_id' => $roleA->id,
         ]);
 
@@ -127,21 +165,19 @@ class RolesTest extends BrowserKitTest
 
     public function test_manage_user_permission()
     {
-        $this->actingAs($this->user)->visit('/settings/users')
-            ->seePageIs('/');
+        $this->actingAs($this->user)->get('/settings/users')->assertRedirect('/');
         $this->giveUserPermissions($this->user, ['users-manage']);
-        $this->actingAs($this->user)->visit('/settings/users')
-            ->seePageIs('/settings/users');
+        $this->actingAs($this->user)->get('/settings/users')->assertOk();
     }
 
     public function test_manage_users_permission_shows_link_in_header_if_does_not_have_settings_manage_permision()
     {
         $usersLink = 'href="' . url('/settings/users') . '"';
-        $this->actingAs($this->user)->visit('/')->dontSee($usersLink);
+        $this->actingAs($this->user)->get('/')->assertDontSee($usersLink);
         $this->giveUserPermissions($this->user, ['users-manage']);
-        $this->actingAs($this->user)->visit('/')->see($usersLink);
+        $this->actingAs($this->user)->get('/')->assertSee($usersLink);
         $this->giveUserPermissions($this->user, ['settings-manage', 'users-manage']);
-        $this->actingAs($this->user)->visit('/')->dontSee($usersLink);
+        $this->actingAs($this->user)->get('/')->assertDontSee($usersLink);
     }
 
     public function test_user_cannot_change_email_unless_they_have_manage_users_permission()
@@ -150,14 +186,14 @@ class RolesTest extends BrowserKitTest
         $originalEmail = $this->user->email;
         $this->actingAs($this->user);
 
-        $this->visit($userProfileUrl)
-            ->assertResponseOk()
-            ->seeElement('input[name=email][disabled]');
+        $this->get($userProfileUrl)
+            ->assertOk()
+            ->assertElementExists('input[name=email][disabled]');
         $this->put($userProfileUrl, [
             'name'  => 'my_new_name',
             'email' => '[email protected]',
         ]);
-        $this->seeInDatabase('users', [
+        $this->assertDatabaseHas('users', [
             'id'    => $this->user->id,
             'email' => $originalEmail,
             'name'  => 'my_new_name',
@@ -165,16 +201,16 @@ class RolesTest extends BrowserKitTest
 
         $this->giveUserPermissions($this->user, ['users-manage']);
 
-        $this->visit($userProfileUrl)
-            ->assertResponseOk()
-            ->dontSeeElement('input[name=email][disabled]')
-            ->seeElement('input[name=email]');
+        $this->get($userProfileUrl)
+            ->assertOk()
+            ->assertElementNotExists('input[name=email][disabled]')
+            ->assertElementExists('input[name=email]');
         $this->put($userProfileUrl, [
             'name'  => 'my_new_name_2',
             'email' => '[email protected]',
         ]);
 
-        $this->seeInDatabase('users', [
+        $this->assertDatabaseHas('users', [
             'id'    => $this->user->id,
             'email' => '[email protected]',
             'name'  => 'my_new_name_2',
@@ -183,40 +219,47 @@ class RolesTest extends BrowserKitTest
 
     public function test_user_roles_manage_permission()
     {
-        $this->actingAs($this->user)->visit('/settings/roles')
-            ->seePageIs('/')->visit('/settings/roles/1')->seePageIs('/');
+        $this->actingAs($this->user)->get('/settings/roles')->assertRedirect('/');
+        $this->get('/settings/roles/1')->assertRedirect('/');
         $this->giveUserPermissions($this->user, ['user-roles-manage']);
-        $this->actingAs($this->user)->visit('/settings/roles')
-            ->seePageIs('/settings/roles')->click('Admin')
-            ->see('Edit Role');
+        $this->actingAs($this->user)->get('/settings/roles')->assertOk();
+        $this->get('/settings/roles/1')
+            ->assertOk()
+            ->assertSee('Admin');
     }
 
     public function test_settings_manage_permission()
     {
-        $this->actingAs($this->user)->visit('/settings')
-            ->seePageIs('/');
+        $this->actingAs($this->user)->get('/settings')->assertRedirect('/');
         $this->giveUserPermissions($this->user, ['settings-manage']);
-        $this->actingAs($this->user)->visit('/settings')
-            ->seePageIs('/settings')->press('Save Settings')->see('Settings Saved');
+        $this->get('/settings')->assertOk();
+
+        $resp = $this->post('/settings', []);
+        $resp->assertRedirect('/settings');
+        $resp = $this->get('/settings');
+        $resp->assertSee('Settings saved');
     }
 
     public function test_restrictions_manage_all_permission()
     {
-        $page = Page::take(1)->get()->first();
-        $this->actingAs($this->user)->visit($page->getUrl())
-            ->dontSee('Permissions')
-            ->visit($page->getUrl() . '/permissions')
-            ->seePageIs('/');
+        $page = Page::query()->get()->first();
+
+        $this->actingAs($this->user)->get($page->getUrl())->assertDontSee('Permissions');
+        $this->get($page->getUrl('/permissions'))->assertRedirect('/');
+
         $this->giveUserPermissions($this->user, ['restrictions-manage-all']);
-        $this->actingAs($this->user)->visit($page->getUrl())
-            ->see('Permissions')
-            ->click('Permissions')
-            ->see('Page Permissions')->seePageIs($page->getUrl() . '/permissions');
+
+        $this->actingAs($this->user)->get($page->getUrl())->assertSee('Permissions');
+
+        $this->get($page->getUrl('/permissions'))
+            ->assertOk()
+            ->assertSee('Page Permissions');
     }
 
     public function test_restrictions_manage_own_permission()
     {
-        $otherUsersPage = Page::first();
+        /** @var Page $otherUsersPage */
+        $otherUsersPage = Page::query()->first();
         $content = $this->createEntityChainBelongingToUser($this->user);
 
         // Set a different creator on the page we're checking to ensure
@@ -227,57 +270,45 @@ class RolesTest extends BrowserKitTest
         $page->save();
 
         // Check can't restrict other's content
-        $this->actingAs($this->user)->visit($otherUsersPage->getUrl())
-            ->dontSee('Permissions')
-            ->visit($otherUsersPage->getUrl() . '/permissions')
-            ->seePageIs('/');
+        $this->actingAs($this->user)->get($otherUsersPage->getUrl())->assertDontSee('Permissions');
+        $this->get($otherUsersPage->getUrl('/permissions'))->assertRedirect('/');
+
         // Check can't restrict own content
-        $this->actingAs($this->user)->visit($page->getUrl())
-            ->dontSee('Permissions')
-            ->visit($page->getUrl() . '/permissions')
-            ->seePageIs('/');
+        $this->actingAs($this->user)->get($page->getUrl())->assertDontSee('Permissions');
+        $this->get($page->getUrl('/permissions'))->assertRedirect('/');
 
         $this->giveUserPermissions($this->user, ['restrictions-manage-own']);
 
         // Check can't restrict other's content
-        $this->actingAs($this->user)->visit($otherUsersPage->getUrl())
-            ->dontSee('Permissions')
-            ->visit($otherUsersPage->getUrl() . '/permissions')
-            ->seePageIs('/');
+        $this->actingAs($this->user)->get($otherUsersPage->getUrl())->assertDontSee('Permissions');
+        $this->get($otherUsersPage->getUrl('/permissions'))->assertRedirect();
+
         // Check can restrict own content
-        $this->actingAs($this->user)->visit($page->getUrl())
-            ->see('Permissions')
-            ->click('Permissions')
-            ->seePageIs($page->getUrl() . '/permissions');
+        $this->actingAs($this->user)->get($page->getUrl())->assertSee('Permissions');
+        $this->get($page->getUrl('/permissions'))->assertOk();
     }
 
     /**
      * Check a standard entity access permission.
-     *
-     * @param string $permission
-     * @param array  $accessUrls Urls that are only accessible after having the permission
-     * @param array  $visibles   Check this text, In the buttons toolbar, is only visible with the permission
      */
-    private function checkAccessPermission($permission, $accessUrls = [], $visibles = [])
+    private function checkAccessPermission(string $permission, array $accessUrls = [], array $visibles = [])
     {
         foreach ($accessUrls as $url) {
-            $this->actingAs($this->user)->visit($url)
-                ->seePageIs('/');
+            $this->actingAs($this->user)->get($url)->assertRedirect('/');
         }
+
         foreach ($visibles as $url => $text) {
-            $this->actingAs($this->user)->visit($url)
-                ->dontSeeInElement('.action-buttons', $text);
+            $this->actingAs($this->user)->get($url)
+                ->assertElementNotContains('.action-buttons', $text);
         }
 
         $this->giveUserPermissions($this->user, [$permission]);
 
         foreach ($accessUrls as $url) {
-            $this->actingAs($this->user)->visit($url)
-                ->seePageIs($url);
+            $this->actingAs($this->user)->get($url)->assertOk();
         }
         foreach ($visibles as $url => $text) {
-            $this->actingAs($this->user)->visit($url)
-                ->see($text);
+            $this->actingAs($this->user)->get($url)->assertSee($text);
         }
     }
 
@@ -289,16 +320,16 @@ class RolesTest extends BrowserKitTest
             '/shelves' => 'New Shelf',
         ]);
 
-        $this->visit('/create-shelf')
-            ->type('test shelf', 'name')
-            ->type('shelf desc', 'description')
-            ->press('Save Shelf')
-            ->seePageIs('/shelves/test-shelf');
+        $this->post('/shelves', [
+            'name'        => 'test shelf',
+            'description' => 'shelf desc',
+        ])->assertRedirect('/shelves/test-shelf');
     }
 
     public function test_bookshelves_edit_own_permission()
     {
-        $otherShelf = Bookshelf::first();
+        /** @var Bookshelf $otherShelf */
+        $otherShelf = Bookshelf::query()->first();
         $ownShelf = $this->newShelf(['name' => 'test-shelf', 'slug' => 'test-shelf']);
         $ownShelf->forceFill(['owned_by' => $this->user->id, 'updated_by' => $this->user->id])->save();
         $this->regenEntityPermissions($ownShelf);
@@ -309,15 +340,14 @@ class RolesTest extends BrowserKitTest
             $ownShelf->getUrl() => 'Edit',
         ]);
 
-        $this->visit($otherShelf->getUrl())
-            ->dontSeeInElement('.action-buttons', 'Edit')
-            ->visit($otherShelf->getUrl('/edit'))
-            ->seePageIs('/');
+        $this->get($otherShelf->getUrl())->assertElementNotContains('.action-buttons', 'Edit');
+        $this->get($otherShelf->getUrl('/edit'))->assertRedirect('/');
     }
 
     public function test_bookshelves_edit_all_permission()
     {
-        $otherShelf = Bookshelf::first();
+        /** @var Bookshelf $otherShelf */
+        $otherShelf = Bookshelf::query()->first();
         $this->checkAccessPermission('bookshelf-update-all', [
             $otherShelf->getUrl('/edit'),
         ], [
@@ -328,7 +358,8 @@ class RolesTest extends BrowserKitTest
     public function test_bookshelves_delete_own_permission()
     {
         $this->giveUserPermissions($this->user, ['bookshelf-update-all']);
-        $otherShelf = Bookshelf::first();
+        /** @var Bookshelf $otherShelf */
+        $otherShelf = Bookshelf::query()->first();
         $ownShelf = $this->newShelf(['name' => 'test-shelf', 'slug' => 'test-shelf']);
         $ownShelf->forceFill(['owned_by' => $this->user->id, 'updated_by' => $this->user->id])->save();
         $this->regenEntityPermissions($ownShelf);
@@ -339,30 +370,27 @@ class RolesTest extends BrowserKitTest
             $ownShelf->getUrl() => 'Delete',
         ]);
 
-        $this->visit($otherShelf->getUrl())
-            ->dontSeeInElement('.action-buttons', 'Delete')
-            ->visit($otherShelf->getUrl('/delete'))
-            ->seePageIs('/');
-        $this->visit($ownShelf->getUrl())->visit($ownShelf->getUrl('/delete'))
-            ->press('Confirm')
-            ->seePageIs('/shelves')
-            ->dontSee($ownShelf->name);
+        $this->get($otherShelf->getUrl())->assertElementNotContains('.action-buttons', 'Delete');
+        $this->get($otherShelf->getUrl('/delete'))->assertRedirect('/');
+
+        $this->get($ownShelf->getUrl());
+        $this->delete($ownShelf->getUrl())->assertRedirect('/shelves');
+        $this->get('/shelves')->assertDontSee($ownShelf->name);
     }
 
     public function test_bookshelves_delete_all_permission()
     {
         $this->giveUserPermissions($this->user, ['bookshelf-update-all']);
-        $otherShelf = Bookshelf::first();
+        /** @var Bookshelf $otherShelf */
+        $otherShelf = Bookshelf::query()->first();
         $this->checkAccessPermission('bookshelf-delete-all', [
             $otherShelf->getUrl('/delete'),
         ], [
             $otherShelf->getUrl() => 'Delete',
         ]);
 
-        $this->visit($otherShelf->getUrl())->visit($otherShelf->getUrl('/delete'))
-            ->press('Confirm')
-            ->seePageIs('/shelves')
-            ->dontSee($otherShelf->name);
+        $this->delete($otherShelf->getUrl())->assertRedirect('/shelves');
+        $this->get('/shelves')->assertDontSee($otherShelf->name);
     }
 
     public function test_books_create_all_permissions()
@@ -373,16 +401,16 @@ class RolesTest extends BrowserKitTest
             '/books' => 'Create New Book',
         ]);
 
-        $this->visit('/create-book')
-            ->type('test book', 'name')
-            ->type('book desc', 'description')
-            ->press('Save Book')
-            ->seePageIs('/books/test-book');
+        $this->post('/books', [
+            'name'        => 'test book',
+            'description' => 'book desc',
+        ])->assertRedirect('/books/test-book');
     }
 
     public function test_books_edit_own_permission()
     {
-        $otherBook = Book::take(1)->get()->first();
+        /** @var Book $otherBook */
+        $otherBook = Book::query()->take(1)->get()->first();
         $ownBook = $this->createEntityChainBelongingToUser($this->user)['book'];
         $this->checkAccessPermission('book-update-own', [
             $ownBook->getUrl() . '/edit',
@@ -390,15 +418,14 @@ class RolesTest extends BrowserKitTest
             $ownBook->getUrl() => 'Edit',
         ]);
 
-        $this->visit($otherBook->getUrl())
-            ->dontSeeInElement('.action-buttons', 'Edit')
-            ->visit($otherBook->getUrl() . '/edit')
-            ->seePageIs('/');
+        $this->get($otherBook->getUrl())->assertElementNotContains('.action-buttons', 'Edit');
+        $this->get($otherBook->getUrl('/edit'))->assertRedirect('/');
     }
 
     public function test_books_edit_all_permission()
     {
-        $otherBook = Book::take(1)->get()->first();
+        /** @var Book $otherBook */
+        $otherBook = Book::query()->take(1)->get()->first();
         $this->checkAccessPermission('book-update-all', [
             $otherBook->getUrl() . '/edit',
         ], [
@@ -409,7 +436,8 @@ class RolesTest extends BrowserKitTest
     public function test_books_delete_own_permission()
     {
         $this->giveUserPermissions($this->user, ['book-update-all']);
-        $otherBook = Book::take(1)->get()->first();
+        /** @var Book $otherBook */
+        $otherBook = Book::query()->take(1)->get()->first();
         $ownBook = $this->createEntityChainBelongingToUser($this->user)['book'];
         $this->checkAccessPermission('book-delete-own', [
             $ownBook->getUrl() . '/delete',
@@ -417,35 +445,33 @@ class RolesTest extends BrowserKitTest
             $ownBook->getUrl() => 'Delete',
         ]);
 
-        $this->visit($otherBook->getUrl())
-            ->dontSeeInElement('.action-buttons', 'Delete')
-            ->visit($otherBook->getUrl() . '/delete')
-            ->seePageIs('/');
-        $this->visit($ownBook->getUrl())->visit($ownBook->getUrl() . '/delete')
-            ->press('Confirm')
-            ->seePageIs('/books')
-            ->dontSee($ownBook->name);
+        $this->get($otherBook->getUrl())->assertElementNotContains('.action-buttons', 'Delete');
+        $this->get($otherBook->getUrl('/delete'))->assertRedirect('/');
+        $this->get($ownBook->getUrl());
+        $this->delete($ownBook->getUrl())->assertRedirect('/books');
+        $this->get('/books')->assertDontSee($ownBook->name);
     }
 
     public function test_books_delete_all_permission()
     {
         $this->giveUserPermissions($this->user, ['book-update-all']);
-        $otherBook = Book::take(1)->get()->first();
+        /** @var Book $otherBook */
+        $otherBook = Book::query()->take(1)->get()->first();
         $this->checkAccessPermission('book-delete-all', [
             $otherBook->getUrl() . '/delete',
         ], [
             $otherBook->getUrl() => 'Delete',
         ]);
 
-        $this->visit($otherBook->getUrl())->visit($otherBook->getUrl() . '/delete')
-            ->press('Confirm')
-            ->seePageIs('/books')
-            ->dontSee($otherBook->name);
+        $this->get($otherBook->getUrl());
+        $this->delete($otherBook->getUrl())->assertRedirect('/books');
+        $this->get('/books')->assertDontSee($otherBook->name);
     }
 
     public function test_chapter_create_own_permissions()
     {
-        $book = Book::take(1)->get()->first();
+        /** @var Book $book */
+        $book = Book::query()->take(1)->get()->first();
         $ownBook = $this->createEntityChainBelongingToUser($this->user)['book'];
         $this->checkAccessPermission('chapter-create-own', [
             $ownBook->getUrl('/create-chapter'),
@@ -453,37 +479,35 @@ class RolesTest extends BrowserKitTest
             $ownBook->getUrl() => 'New Chapter',
         ]);
 
-        $this->visit($ownBook->getUrl('/create-chapter'))
-            ->type('test chapter', 'name')
-            ->type('chapter desc', 'description')
-            ->press('Save Chapter')
-            ->seePageIs($ownBook->getUrl('/chapter/test-chapter'));
+        $this->post($ownBook->getUrl('/create-chapter'), [
+            'name'        => 'test chapter',
+            'description' => 'chapter desc',
+        ])->assertRedirect($ownBook->getUrl('/chapter/test-chapter'));
 
-        $this->visit($book->getUrl())
-            ->dontSeeInElement('.action-buttons', 'New Chapter')
-            ->visit($book->getUrl('/create-chapter'))
-            ->seePageIs('/');
+        $this->get($book->getUrl())->assertElementNotContains('.action-buttons', 'New Chapter');
+        $this->get($book->getUrl('/create-chapter'))->assertRedirect('/');
     }
 
     public function test_chapter_create_all_permissions()
     {
-        $book = Book::take(1)->get()->first();
+        /** @var Book $book */
+        $book = Book::query()->first();
         $this->checkAccessPermission('chapter-create-all', [
             $book->getUrl('/create-chapter'),
         ], [
             $book->getUrl() => 'New Chapter',
         ]);
 
-        $this->visit($book->getUrl('/create-chapter'))
-            ->type('test chapter', 'name')
-            ->type('chapter desc', 'description')
-            ->press('Save Chapter')
-            ->seePageIs($book->getUrl('/chapter/test-chapter'));
+        $this->post($book->getUrl('/create-chapter'), [
+            'name'        => 'test chapter',
+            'description' => 'chapter desc',
+        ])->assertRedirect($book->getUrl('/chapter/test-chapter'));
     }
 
     public function test_chapter_edit_own_permission()
     {
-        $otherChapter = Chapter::take(1)->get()->first();
+        /** @var Chapter $otherChapter */
+        $otherChapter = Chapter::query()->first();
         $ownChapter = $this->createEntityChainBelongingToUser($this->user)['chapter'];
         $this->checkAccessPermission('chapter-update-own', [
             $ownChapter->getUrl() . '/edit',
@@ -491,15 +515,14 @@ class RolesTest extends BrowserKitTest
             $ownChapter->getUrl() => 'Edit',
         ]);
 
-        $this->visit($otherChapter->getUrl())
-            ->dontSeeInElement('.action-buttons', 'Edit')
-            ->visit($otherChapter->getUrl() . '/edit')
-            ->seePageIs('/');
+        $this->get($otherChapter->getUrl())->assertElementNotContains('.action-buttons', 'Edit');
+        $this->get($otherChapter->getUrl('/edit'))->assertRedirect('/');
     }
 
     public function test_chapter_edit_all_permission()
     {
-        $otherChapter = Chapter::take(1)->get()->first();
+        /** @var Chapter $otherChapter */
+        $otherChapter = Chapter::query()->take(1)->get()->first();
         $this->checkAccessPermission('chapter-update-all', [
             $otherChapter->getUrl() . '/edit',
         ], [
@@ -510,7 +533,8 @@ class RolesTest extends BrowserKitTest
     public function test_chapter_delete_own_permission()
     {
         $this->giveUserPermissions($this->user, ['chapter-update-all']);
-        $otherChapter = Chapter::take(1)->get()->first();
+        /** @var Chapter $otherChapter */
+        $otherChapter = Chapter::query()->first();
         $ownChapter = $this->createEntityChainBelongingToUser($this->user)['chapter'];
         $this->checkAccessPermission('chapter-delete-own', [
             $ownChapter->getUrl() . '/delete',
@@ -519,20 +543,18 @@ class RolesTest extends BrowserKitTest
         ]);
 
         $bookUrl = $ownChapter->book->getUrl();
-        $this->visit($otherChapter->getUrl())
-            ->dontSeeInElement('.action-buttons', 'Delete')
-            ->visit($otherChapter->getUrl() . '/delete')
-            ->seePageIs('/');
-        $this->visit($ownChapter->getUrl())->visit($ownChapter->getUrl() . '/delete')
-            ->press('Confirm')
-            ->seePageIs($bookUrl)
-            ->dontSeeInElement('.book-content', $ownChapter->name);
+        $this->get($otherChapter->getUrl())->assertElementNotContains('.action-buttons', 'Delete');
+        $this->get($otherChapter->getUrl('/delete'))->assertRedirect('/');
+        $this->get($ownChapter->getUrl());
+        $this->delete($ownChapter->getUrl())->assertRedirect($bookUrl);
+        $this->get($bookUrl)->assertElementNotContains('.book-content', $ownChapter->name);
     }
 
     public function test_chapter_delete_all_permission()
     {
         $this->giveUserPermissions($this->user, ['chapter-update-all']);
-        $otherChapter = Chapter::take(1)->get()->first();
+        /** @var Chapter $otherChapter */
+        $otherChapter = Chapter::query()->first();
         $this->checkAccessPermission('chapter-delete-all', [
             $otherChapter->getUrl() . '/delete',
         ], [
@@ -540,16 +562,17 @@ class RolesTest extends BrowserKitTest
         ]);
 
         $bookUrl = $otherChapter->book->getUrl();
-        $this->visit($otherChapter->getUrl())->visit($otherChapter->getUrl() . '/delete')
-            ->press('Confirm')
-            ->seePageIs($bookUrl)
-            ->dontSeeInElement('.book-content', $otherChapter->name);
+        $this->get($otherChapter->getUrl());
+        $this->delete($otherChapter->getUrl())->assertRedirect($bookUrl);
+        $this->get($bookUrl)->assertElementNotContains('.book-content', $otherChapter->name);
     }
 
     public function test_page_create_own_permissions()
     {
-        $book = Book::first();
-        $chapter = Chapter::first();
+        /** @var Book $book */
+        $book = Book::query()->first();
+        /** @var Chapter $chapter */
+        $chapter = Chapter::query()->first();
 
         $entities = $this->createEntityChainBelongingToUser($this->user);
         $ownBook = $entities['book'];
@@ -560,8 +583,7 @@ class RolesTest extends BrowserKitTest
         $accessUrls = [$createUrl, $createUrlChapter];
 
         foreach ($accessUrls as $url) {
-            $this->actingAs($this->user)->visit($url)
-                ->seePageIs('/');
+            $this->actingAs($this->user)->get($url)->assertRedirect('/');
         }
 
         $this->checkAccessPermission('page-create-own', [], [
@@ -572,40 +594,39 @@ class RolesTest extends BrowserKitTest
         $this->giveUserPermissions($this->user, ['page-create-own']);
 
         foreach ($accessUrls as $index => $url) {
-            $this->actingAs($this->user)->visit($url);
-            $expectedUrl = Page::where('draft', '=', true)->orderBy('id', 'desc')->first()->getUrl();
-            $this->seePageIs($expectedUrl);
+            $resp = $this->actingAs($this->user)->get($url);
+            $expectedUrl = Page::query()->where('draft', '=', true)->orderBy('id', 'desc')->first()->getUrl();
+            $resp->assertRedirect($expectedUrl);
         }
 
-        $this->visit($createUrl)
-            ->type('test page', 'name')
-            ->type('page desc', 'html')
-            ->press('Save Page')
-            ->seePageIs($ownBook->getUrl('/page/test-page'));
+        $this->get($createUrl);
+        /** @var Page $draft */
+        $draft = Page::query()->where('draft', '=', true)->orderBy('id', 'desc')->first();
+        $this->post($draft->getUrl(), [
+            'name' => 'test page',
+            'html' => 'page desc',
+        ])->assertRedirect($ownBook->getUrl('/page/test-page'));
+
+        $this->get($book->getUrl())->assertElementNotContains('.action-buttons', 'New Page');
+        $this->get($book->getUrl('/create-page'))->assertRedirect('/');
 
-        $this->visit($book->getUrl())
-            ->dontSeeInElement('.action-buttons', 'New Page')
-            ->visit($book->getUrl() . '/create-page')
-            ->seePageIs('/');
-        $this->visit($chapter->getUrl())
-            ->dontSeeInElement('.action-buttons', 'New Page')
-            ->visit($chapter->getUrl() . '/create-page')
-            ->seePageIs('/');
+        $this->get($chapter->getUrl())->assertElementNotContains('.action-buttons', 'New Page');
+        $this->get($chapter->getUrl('/create-page'))->assertRedirect('/');
     }
 
     public function test_page_create_all_permissions()
     {
-        $book = Book::take(1)->get()->first();
-        $chapter = Chapter::take(1)->get()->first();
-        $baseUrl = $book->getUrl() . '/page';
+        /** @var Book $book */
+        $book = Book::query()->first();
+        /** @var Chapter $chapter */
+        $chapter = Chapter::query()->first();
         $createUrl = $book->getUrl('/create-page');
 
         $createUrlChapter = $chapter->getUrl('/create-page');
         $accessUrls = [$createUrl, $createUrlChapter];
 
         foreach ($accessUrls as $url) {
-            $this->actingAs($this->user)->visit($url)
-                ->seePageIs('/');
+            $this->actingAs($this->user)->get($url)->assertRedirect('/');
         }
 
         $this->checkAccessPermission('page-create-all', [], [
@@ -616,27 +637,32 @@ class RolesTest extends BrowserKitTest
         $this->giveUserPermissions($this->user, ['page-create-all']);
 
         foreach ($accessUrls as $index => $url) {
-            $this->actingAs($this->user)->visit($url);
-            $expectedUrl = Page::where('draft', '=', true)->orderBy('id', 'desc')->first()->getUrl();
-            $this->seePageIs($expectedUrl);
+            $resp = $this->actingAs($this->user)->get($url);
+            $expectedUrl = Page::query()->where('draft', '=', true)->orderBy('id', 'desc')->first()->getUrl();
+            $resp->assertRedirect($expectedUrl);
         }
 
-        $this->visit($createUrl)
-            ->type('test page', 'name')
-            ->type('page desc', 'html')
-            ->press('Save Page')
-            ->seePageIs($book->getUrl('/page/test-page'));
+        $this->get($createUrl);
+        /** @var Page $draft */
+        $draft = Page::query()->where('draft', '=', true)->orderByDesc('id')->first();
+        $this->post($draft->getUrl(), [
+            'name' => 'test page',
+            'html' => 'page desc',
+        ])->assertRedirect($book->getUrl('/page/test-page'));
 
-        $this->visit($chapter->getUrl('/create-page'))
-            ->type('new test page', 'name')
-            ->type('page desc', 'html')
-            ->press('Save Page')
-            ->seePageIs($book->getUrl('/page/new-test-page'));
+        $this->get($chapter->getUrl('/create-page'));
+        /** @var Page $draft */
+        $draft = Page::query()->where('draft', '=', true)->orderByDesc('id')->first();
+        $this->post($draft->getUrl(), [
+            'name' => 'new test page',
+            'html' => 'page desc',
+        ])->assertRedirect($book->getUrl('/page/new-test-page'));
     }
 
     public function test_page_edit_own_permission()
     {
-        $otherPage = Page::take(1)->get()->first();
+        /** @var Page $otherPage */
+        $otherPage = Page::query()->first();
         $ownPage = $this->createEntityChainBelongingToUser($this->user)['page'];
         $this->checkAccessPermission('page-update-own', [
             $ownPage->getUrl() . '/edit',
@@ -644,17 +670,16 @@ class RolesTest extends BrowserKitTest
             $ownPage->getUrl() => 'Edit',
         ]);
 
-        $this->visit($otherPage->getUrl())
-            ->dontSeeInElement('.action-buttons', 'Edit')
-            ->visit($otherPage->getUrl() . '/edit')
-            ->seePageIs('/');
+        $this->get($otherPage->getUrl())->assertElementNotContains('.action-buttons', 'Edit');
+        $this->get($otherPage->getUrl() . '/edit')->assertRedirect('/');
     }
 
     public function test_page_edit_all_permission()
     {
-        $otherPage = Page::take(1)->get()->first();
+        /** @var Page $otherPage */
+        $otherPage = Page::query()->first();
         $this->checkAccessPermission('page-update-all', [
-            $otherPage->getUrl() . '/edit',
+            $otherPage->getUrl('/edit'),
         ], [
             $otherPage->getUrl() => 'Edit',
         ]);
@@ -663,7 +688,8 @@ class RolesTest extends BrowserKitTest
     public function test_page_delete_own_permission()
     {
         $this->giveUserPermissions($this->user, ['page-update-all']);
-        $otherPage = Page::take(1)->get()->first();
+        /** @var Page $otherPage */
+        $otherPage = Page::query()->first();
         $ownPage = $this->createEntityChainBelongingToUser($this->user)['page'];
         $this->checkAccessPermission('page-delete-own', [
             $ownPage->getUrl() . '/delete',
@@ -672,122 +698,127 @@ class RolesTest extends BrowserKitTest
         ]);
 
         $parent = $ownPage->chapter ?? $ownPage->book;
-        $this->visit($otherPage->getUrl())
-            ->dontSeeInElement('.action-buttons', 'Delete')
-            ->visit($otherPage->getUrl() . '/delete')
-            ->seePageIs('/');
-        $this->visit($ownPage->getUrl())->visit($ownPage->getUrl() . '/delete')
-            ->press('Confirm')
-            ->seePageIs($parent->getUrl())
-            ->dontSeeInElement('.book-content', $ownPage->name);
+        $this->get($otherPage->getUrl())->assertElementNotContains('.action-buttons', 'Delete');
+        $this->get($otherPage->getUrl('/delete'))->assertRedirect('/');
+        $this->get($ownPage->getUrl());
+        $this->delete($ownPage->getUrl())->assertRedirect($parent->getUrl());
+        $this->get($parent->getUrl())->assertElementNotContains('.book-content', $ownPage->name);
     }
 
     public function test_page_delete_all_permission()
     {
         $this->giveUserPermissions($this->user, ['page-update-all']);
-        $otherPage = Page::take(1)->get()->first();
+        /** @var Page $otherPage */
+        $otherPage = Page::query()->first();
+
         $this->checkAccessPermission('page-delete-all', [
             $otherPage->getUrl() . '/delete',
         ], [
             $otherPage->getUrl() => 'Delete',
         ]);
 
+        /** @var Entity $parent */
         $parent = $otherPage->chapter ?? $otherPage->book;
-        $this->visit($otherPage->getUrl())->visit($otherPage->getUrl() . '/delete')
-            ->press('Confirm')
-            ->seePageIs($parent->getUrl())
-            ->dontSeeInElement('.book-content', $otherPage->name);
+        $this->get($otherPage->getUrl());
+
+        $this->delete($otherPage->getUrl())->assertRedirect($parent->getUrl());
+        $this->get($parent->getUrl())->assertDontSee($otherPage->name);
     }
 
     public function test_public_role_visible_in_user_edit_screen()
     {
-        $user = User::first();
+        /** @var User $user */
+        $user = User::query()->first();
         $adminRole = Role::getSystemRole('admin');
         $publicRole = Role::getSystemRole('public');
-        $this->asAdmin()->visit('/settings/users/' . $user->id)
-            ->seeElement('[name="roles[' . $adminRole->id . ']"]')
-            ->seeElement('[name="roles[' . $publicRole->id . ']"]');
+        $this->asAdmin()->get('/settings/users/' . $user->id)
+            ->assertElementExists('[name="roles[' . $adminRole->id . ']"]')
+            ->assertElementExists('[name="roles[' . $publicRole->id . ']"]');
     }
 
     public function test_public_role_visible_in_role_listing()
     {
-        $this->asAdmin()->visit('/settings/roles')
-            ->see('Admin')
-            ->see('Public');
+        $this->asAdmin()->get('/settings/roles')
+            ->assertSee('Admin')
+            ->assertSee('Public');
     }
 
     public function test_public_role_visible_in_default_role_setting()
     {
-        $this->asAdmin()->visit('/settings')
-            ->seeElement('[data-system-role-name="admin"]')
-            ->seeElement('[data-system-role-name="public"]');
+        $this->asAdmin()->get('/settings')
+            ->assertElementExists('[data-system-role-name="admin"]')
+            ->assertElementExists('[data-system-role-name="public"]');
     }
 
-    public function test_public_role_not_deleteable()
+    public function test_public_role_not_deletable()
     {
-        $this->asAdmin()->visit('/settings/roles')
-            ->click('Public')
-            ->see('Edit Role')
-            ->click('Delete Role')
-            ->press('Confirm')
-            ->see('Delete Role')
-            ->see('Cannot be deleted');
+        /** @var Role $publicRole */
+        $publicRole = Role::getSystemRole('public');
+        $resp = $this->asAdmin()->delete('/settings/roles/delete/' . $publicRole->id);
+        $resp->assertRedirect('/');
+
+        $this->get('/settings/roles/delete/' . $publicRole->id);
+        $resp = $this->delete('/settings/roles/delete/' . $publicRole->id);
+        $resp->assertRedirect('/settings/roles/delete/' . $publicRole->id);
+        $resp = $this->get('/settings/roles/delete/' . $publicRole->id);
+        $resp->assertSee('This role is a system role and cannot be deleted');
     }
 
     public function test_image_delete_own_permission()
     {
         $this->giveUserPermissions($this->user, ['image-update-all']);
-        $page = Page::first();
-        $image = factory(Image::class)->create(['uploaded_to' => $page->id, 'created_by' => $this->user->id, 'updated_by' => $this->user->id]);
+        /** @var Page $page */
+        $page = Page::query()->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);
+        $this->actingAs($this->user)->json('delete', '/images/' . $image->id)->assertStatus(403);
 
         $this->giveUserPermissions($this->user, ['image-delete-own']);
 
-        $this->actingAs($this->user)->json('delete', '/images/' . $image->id)
-            ->seeStatusCode(200)
-            ->dontSeeInDatabase('images', ['id' => $image->id]);
+        $this->actingAs($this->user)->json('delete', '/images/' . $image->id)->assertOk();
+        $this->assertDatabaseMissing('images', ['id' => $image->id]);
     }
 
     public function test_image_delete_all_permission()
     {
         $this->giveUserPermissions($this->user, ['image-update-all']);
         $admin = $this->getAdmin();
-        $page = Page::first();
+        /** @var Page $page */
+        $page = Page::query()->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);
+        $this->actingAs($this->user)->json('delete', '/images/' . $image->id)->assertStatus(403);
 
         $this->giveUserPermissions($this->user, ['image-delete-own']);
 
-        $this->actingAs($this->user)->json('delete', '/images/' . $image->id)
-            ->seeStatusCode(403);
+        $this->actingAs($this->user)->json('delete', '/images/' . $image->id)->assertStatus(403);
 
         $this->giveUserPermissions($this->user, ['image-delete-all']);
 
-        $this->actingAs($this->user)->json('delete', '/images/' . $image->id)
-            ->seeStatusCode(200)
-            ->dontSeeInDatabase('images', ['id' => $image->id]);
+        $this->actingAs($this->user)->json('delete', '/images/' . $image->id)->assertOk();
+        $this->assertDatabaseMissing('images', ['id' => $image->id]);
     }
 
     public function test_role_permission_removal()
     {
         // To cover issue fixed in f99c8ff99aee9beb8c692f36d4b84dc6e651e50a.
-        $page = Page::first();
+        /** @var Page $page */
+        $page = Page::query()->first();
         $viewerRole = Role::getRole('viewer');
         $viewer = $this->getViewer();
-        $this->actingAs($viewer)->visit($page->getUrl())->assertResponseStatus(200);
+        $this->actingAs($viewer)->get($page->getUrl())->assertOk();
 
         $this->asAdmin()->put('/settings/roles/' . $viewerRole->id, [
             'display_name' => $viewerRole->display_name,
             'description'  => $viewerRole->description,
             'permission'   => [],
-        ])->assertResponseStatus(302);
+        ])->assertStatus(302);
 
-        $this->expectException(HttpException::class);
-        $this->actingAs($viewer)->visit($page->getUrl())->assertResponseStatus(404);
+        $this->actingAs($viewer)->get($page->getUrl())->assertStatus(404);
     }
 
     public function test_empty_state_actions_not_visible_without_permission()
@@ -795,130 +826,120 @@ class RolesTest extends BrowserKitTest
         $admin = $this->getAdmin();
         // Book links
         $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');
+        $this->regenEntityPermissions($book);
+        $this->actingAs($this->getViewer())->get($book->getUrl())
+            ->assertDontSee('Create a new page')
+            ->assertDontSee('Add a chapter');
 
         // Chapter links
         $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')
-            ->dontSee('Sort the current book');
+        $this->regenEntityPermissions($chapter);
+        $this->actingAs($this->getViewer())->get($chapter->getUrl())
+            ->assertDontSee('Create a new page')
+            ->assertDontSee('Sort the current book');
     }
 
     public function test_comment_create_permission()
     {
         $ownPage = $this->createEntityChainBelongingToUser($this->user)['page'];
 
-        $this->actingAs($this->user)->addComment($ownPage);
-
-        $this->assertResponseStatus(403);
+        $this->actingAs($this->user)
+            ->addComment($ownPage)
+            ->assertStatus(403);
 
         $this->giveUserPermissions($this->user, ['comment-create-all']);
 
-        $this->actingAs($this->user)->addComment($ownPage);
-        $this->assertResponseStatus(200);
+        $this->actingAs($this->user)
+            ->addComment($ownPage)
+            ->assertOk();
     }
 
     public function test_comment_update_own_permission()
     {
         $ownPage = $this->createEntityChainBelongingToUser($this->user)['page'];
         $this->giveUserPermissions($this->user, ['comment-create-all']);
-        $commentId = $this->actingAs($this->user)->addComment($ownPage);
+        $this->actingAs($this->user)->addComment($ownPage);
+        /** @var Comment $comment */
+        $comment = $ownPage->comments()->latest()->first();
 
         // no comment-update-own
-        $this->actingAs($this->user)->updateComment($commentId);
-        $this->assertResponseStatus(403);
+        $this->actingAs($this->user)->updateComment($comment)->assertStatus(403);
 
         $this->giveUserPermissions($this->user, ['comment-update-own']);
 
         // now has comment-update-own
-        $this->actingAs($this->user)->updateComment($commentId);
-        $this->assertResponseStatus(200);
+        $this->actingAs($this->user)->updateComment($comment)->assertOk();
     }
 
     public function test_comment_update_all_permission()
     {
+        /** @var Page $ownPage */
         $ownPage = $this->createEntityChainBelongingToUser($this->user)['page'];
-        $commentId = $this->asAdmin()->addComment($ownPage);
+        $this->asAdmin()->addComment($ownPage);
+        /** @var Comment $comment */
+        $comment = $ownPage->comments()->latest()->first();
 
         // no comment-update-all
-        $this->actingAs($this->user)->updateComment($commentId);
-        $this->assertResponseStatus(403);
+        $this->actingAs($this->user)->updateComment($comment)->assertStatus(403);
 
         $this->giveUserPermissions($this->user, ['comment-update-all']);
 
         // now has comment-update-all
-        $this->actingAs($this->user)->updateComment($commentId);
-        $this->assertResponseStatus(200);
+        $this->actingAs($this->user)->updateComment($comment)->assertOk();
     }
 
     public function test_comment_delete_own_permission()
     {
+        /** @var Page $ownPage */
         $ownPage = $this->createEntityChainBelongingToUser($this->user)['page'];
         $this->giveUserPermissions($this->user, ['comment-create-all']);
-        $commentId = $this->actingAs($this->user)->addComment($ownPage);
+        $this->actingAs($this->user)->addComment($ownPage);
+
+        /** @var Comment $comment */
+        $comment = $ownPage->comments()->latest()->first();
 
         // no comment-delete-own
-        $this->actingAs($this->user)->deleteComment($commentId);
-        $this->assertResponseStatus(403);
+        $this->actingAs($this->user)->deleteComment($comment)->assertStatus(403);
 
         $this->giveUserPermissions($this->user, ['comment-delete-own']);
 
         // now has comment-update-own
-        $this->actingAs($this->user)->deleteComment($commentId);
-        $this->assertResponseStatus(200);
+        $this->actingAs($this->user)->deleteComment($comment)->assertOk();
     }
 
     public function test_comment_delete_all_permission()
     {
+        /** @var Page $ownPage */
         $ownPage = $this->createEntityChainBelongingToUser($this->user)['page'];
-        $commentId = $this->asAdmin()->addComment($ownPage);
+        $this->asAdmin()->addComment($ownPage);
+        /** @var Comment $comment */
+        $comment = $ownPage->comments()->latest()->first();
 
         // no comment-delete-all
-        $this->actingAs($this->user)->deleteComment($commentId);
-        $this->assertResponseStatus(403);
+        $this->actingAs($this->user)->deleteComment($comment)->assertStatus(403);
 
         $this->giveUserPermissions($this->user, ['comment-delete-all']);
 
         // now has comment-delete-all
-        $this->actingAs($this->user)->deleteComment($commentId);
-        $this->assertResponseStatus(200);
+        $this->actingAs($this->user)->deleteComment($comment)->assertOk();
     }
 
-    private function addComment($page)
+    private function addComment(Page $page): TestResponse
     {
         $comment = factory(Comment::class)->make();
-        $url = "/comment/$page->id";
-        $request = [
-            'text' => $comment->text,
-            'html' => $comment->html,
-        ];
-
-        $this->postJson($url, $request);
-        $comment = $page->comments()->first();
 
-        return $comment === null ? null : $comment->id;
+        return $this->postJson("/comment/$page->id", $comment->only('text', 'html'));
     }
 
-    private function updateComment($commentId)
+    private function updateComment(Comment $comment): TestResponse
     {
-        $comment = factory(Comment::class)->make();
-        $url = "/comment/$commentId";
-        $request = [
-            'text' => $comment->text,
-            'html' => $comment->html,
-        ];
+        $commentData = factory(Comment::class)->make();
 
-        return $this->putJson($url, $request);
+        return $this->putJson("/comment/{$comment->id}", $commentData->only('text', 'html'));
     }
 
-    private function deleteComment($commentId)
+    private function deleteComment(Comment $comment): TestResponse
     {
-        $url = '/comment/' . $commentId;
-
-        return $this->json('DELETE', $url);
+        return $this->json('DELETE', '/comment/' . $comment->id);
     }
 }
index ae0c0ff95c6d454f649f0fe535b374a775a4d459..499c0c9f9710ab0bbd4c6f7401743c325dc2c6be 100644 (file)
@@ -2,7 +2,6 @@
 
 namespace Tests;
 
-use Auth;
 use BookStack\Auth\Permissions\PermissionService;
 use BookStack\Auth\Permissions\RolePermission;
 use BookStack\Auth\Role;
@@ -10,6 +9,7 @@ use BookStack\Auth\User;
 use BookStack\Entities\Models\Book;
 use BookStack\Entities\Models\Chapter;
 use BookStack\Entities\Models\Page;
+use Illuminate\Support\Facades\Auth;
 use Illuminate\Support\Facades\View;
 
 class PublicActionTest extends TestCase
index 1c54452124fbb088631edb961931739377244270..f3e30c0d07d442f894347f7599e97e3e1c9218fc 100644 (file)
@@ -8,8 +8,8 @@ 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;
+use Illuminate\Support\Facades\DB;
 
 class RecycleBinTest extends TestCase
 {
index 888dac8106af4b8088ecf2f28fb8d82bd96d12f6..2bde890ad58139ef0bc4d33416e3d4cfe68fa462 100644 (file)
@@ -2,7 +2,7 @@
 
 namespace Tests;
 
-use Illuminate\Support\Str;
+use BookStack\Util\CspService;
 
 class SecurityHeaderTest extends TestCase
 {
@@ -44,26 +44,90 @@ class SecurityHeaderTest extends TestCase
     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');
-        });
+        $frameHeader = $this->getCspHeader($resp, 'frame-ancestors');
 
-        $this->assertTrue($frameHeaders->count() === 1);
-        $this->assertEquals('frame-ancestors \'self\'', $frameHeaders->first());
+        $this->assertEquals('frame-ancestors \'self\'', $frameHeader);
     }
 
     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');
-            });
+            $frameHeader = $this->getCspHeader($resp, 'frame-ancestors');
 
-            $this->assertTrue($frameHeaders->count() === 1);
-            $this->assertEquals('frame-ancestors \'self\' https://a.example.com https://b.example.com', $frameHeaders->first());
+            $this->assertNotEmpty($frameHeader);
+            $this->assertEquals('frame-ancestors \'self\' https://a.example.com https://b.example.com', $frameHeader);
         });
     }
+
+    public function test_script_csp_set_on_responses()
+    {
+        $resp = $this->get('/');
+        $scriptHeader = $this->getCspHeader($resp, 'script-src');
+        $this->assertStringContainsString('\'strict-dynamic\'', $scriptHeader);
+        $this->assertStringContainsString('\'nonce-', $scriptHeader);
+    }
+
+    public function test_script_csp_nonce_matches_nonce_used_in_custom_head()
+    {
+        $this->setSettings(['app-custom-head' => '<script>console.log("cat");</script>']);
+        $resp = $this->get('/login');
+        $scriptHeader = $this->getCspHeader($resp, 'script-src');
+
+        $nonce = app()->make(CspService::class)->getNonce();
+        $this->assertStringContainsString('nonce-' . $nonce, $scriptHeader);
+        $resp->assertSee('<script nonce="' . $nonce . '">console.log("cat");</script>');
+    }
+
+    public function test_script_csp_nonce_changes_per_request()
+    {
+        $resp = $this->get('/');
+        $firstHeader = $this->getCspHeader($resp, 'script-src');
+
+        $this->refreshApplication();
+
+        $resp = $this->get('/');
+        $secondHeader = $this->getCspHeader($resp, 'script-src');
+
+        $this->assertNotEquals($firstHeader, $secondHeader);
+    }
+
+    public function test_allow_content_scripts_settings_controls_csp_script_headers()
+    {
+        config()->set('app.allow_content_scripts', true);
+        $resp = $this->get('/');
+        $scriptHeader = $this->getCspHeader($resp, 'script-src');
+        $this->assertEmpty($scriptHeader);
+
+        config()->set('app.allow_content_scripts', false);
+        $resp = $this->get('/');
+        $scriptHeader = $this->getCspHeader($resp, 'script-src');
+        $this->assertNotEmpty($scriptHeader);
+    }
+
+    public function test_object_src_csp_header_set()
+    {
+        $resp = $this->get('/');
+        $scriptHeader = $this->getCspHeader($resp, 'object-src');
+        $this->assertEquals('object-src \'self\'', $scriptHeader);
+    }
+
+    public function test_base_uri_csp_header_set()
+    {
+        $resp = $this->get('/');
+        $scriptHeader = $this->getCspHeader($resp, 'base-uri');
+        $this->assertEquals('base-uri \'self\'', $scriptHeader);
+    }
+
+    /**
+     * Get the value of the first CSP header of the given type.
+     */
+    protected function getCspHeader(TestResponse $resp, string $type): string
+    {
+        $cspHeaders = collect($resp->headers->all('Content-Security-Policy'));
+
+        return $cspHeaders->filter(function ($val) use ($type) {
+            return strpos($val, $type) === 0;
+        })->first() ?? '';
+    }
 }
diff --git a/tests/Settings/CustomHeadContentTest.php b/tests/Settings/CustomHeadContentTest.php
new file mode 100644 (file)
index 0000000..94ef471
--- /dev/null
@@ -0,0 +1,66 @@
+<?php
+
+namespace Tests\Settings;
+
+use BookStack\Util\CspService;
+use Tests\TestCase;
+
+class CustomHeadContentTest extends TestCase
+{
+    public function test_configured_content_shows_on_pages()
+    {
+        $this->setSettings(['app-custom-head' => '<script>console.log("cat");</script>']);
+        $resp = $this->get('/login');
+        $resp->assertSee('console.log("cat")');
+    }
+
+    public function test_configured_content_does_not_show_on_settings_page()
+    {
+        $this->setSettings(['app-custom-head' => '<script>console.log("cat");</script>']);
+        $resp = $this->asAdmin()->get('/settings');
+        $resp->assertDontSee('console.log("cat")');
+    }
+
+    public function test_divs_in_js_preserved_in_configured_content()
+    {
+        $this->setSettings(['app-custom-head' => '<script><div id="hello">cat</div></script>']);
+        $resp = $this->get('/login');
+        $resp->assertSee('<div id="hello">cat</div>');
+    }
+
+    public function test_nonce_application_handles_edge_cases()
+    {
+        $mockCSP = $this->mock(CspService::class);
+        $mockCSP->shouldReceive('getNonce')->andReturn('abc123');
+
+        $content = trim('
+<script>console.log("cat");</script>
+<script type="text/html"><\script>const a = `<div></div>`<\/\script></script>
+<script >const a = `<div></div>`;</script>
+<script type="<script text>test">const c = `<div></div>`;</script>
+<script
+    type="text/html"
+>
+const a = `<\script><\/script>`;
+const b = `<script`;
+</script>
+<SCRIPT>const b = `↗️£`;</SCRIPT>
+        ');
+
+        $expectedOutput = trim('
+<script nonce="abc123">console.log("cat");</script>
+<script type="text/html" nonce="abc123"><\script>const a = `<div></div>`<\/\script></script>
+<script nonce="abc123">const a = `<div></div>`;</script>
+<script type="&lt;script text&gt;test" nonce="abc123">const c = `<div></div>`;</script>
+<script type="text/html" nonce="abc123">
+const a = `<\script><\/script>`;
+const b = `<script`;
+</script>
+<script nonce="abc123">const b = `↗️£`;</script>
+        ');
+
+        $this->setSettings(['app-custom-head' => $content]);
+        $resp = $this->get('/login');
+        $resp->assertSee($expectedOutput);
+    }
+}
similarity index 98%
rename from tests/FooterLinksTest.php
rename to tests/Settings/FooterLinksTest.php
index cb2959411cf49be89ef99a104bf781799e6a3cb7..55c3e107d5ff4b071e98cf2cd50f7475a148d018 100644 (file)
@@ -1,5 +1,7 @@
 <?php
 
+namespace Tests\Settings;
+
 use Tests\TestCase;
 
 class FooterLinksTest extends TestCase
index 0bc80924e499024a7435aeacc353b48891f115dc..e4d27c849e7a993ea34d3a2333fea62d6f260e51 100644 (file)
@@ -4,6 +4,7 @@ namespace Tests;
 
 use BookStack\Auth\Permissions\PermissionService;
 use BookStack\Auth\Permissions\PermissionsRepo;
+use BookStack\Auth\Permissions\RolePermission;
 use BookStack\Auth\Role;
 use BookStack\Auth\User;
 use BookStack\Entities\Models\Book;
@@ -18,6 +19,7 @@ use BookStack\Entities\Repos\PageRepo;
 use BookStack\Settings\SettingService;
 use BookStack\Uploads\HttpFetcher;
 use Illuminate\Foundation\Testing\Assert as PHPUnit;
+use Illuminate\Http\JsonResponse;
 use Illuminate\Support\Env;
 use Illuminate\Support\Facades\Log;
 use Mockery;
@@ -87,7 +89,7 @@ trait SharedTestHelpers
     /**
      * Get a user that's not a system user such as the guest user.
      */
-    public function getNormalUser()
+    public function getNormalUser(): User
     {
         return User::query()->where('system_name', '=', null)->get()->last();
     }
@@ -184,6 +186,19 @@ trait SharedTestHelpers
         $user->clearPermissionCache();
     }
 
+    /**
+     * Completely remove the given permission name from the given user.
+     */
+    protected function removePermissionFromUser(User $user, string $permission)
+    {
+        $permission = RolePermission::query()->where('name', '=', $permission)->first();
+        /** @var Role $role */
+        foreach ($user->roles as $role) {
+            $role->detachPermission($permission);
+        }
+        $user->clearPermissionCache();
+    }
+
     /**
      * Create a new basic role for testing purposes.
      */
@@ -196,6 +211,27 @@ trait SharedTestHelpers
         return $permissionRepo->saveNewRole($roleData);
     }
 
+    /**
+     * Create a group of entities that belong to a specific user.
+     *
+     * @return array{book: Book, chapter: Chapter, page: Page}
+     */
+    protected function createEntityChainBelongingToUser(User $creatorUser, ?User $updaterUser = null): array
+    {
+        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 compact('book', 'chapter', 'page');
+    }
+
     /**
      * Mock the HttpFetcher service and return the given data on fetch.
      */
@@ -274,8 +310,17 @@ trait SharedTestHelpers
     private function isPermissionError($response): bool
     {
         return $response->status() === 302
-            && $response->headers->get('Location') === url('/')
-            && strpos(session()->pull('error', ''), 'You do not have permission to access') === 0;
+            && (
+                (
+                    $response->headers->get('Location') === url('/')
+                    && strpos(session()->pull('error', ''), 'You do not have permission to access') === 0
+                )
+                ||
+                (
+                    $response instanceof JsonResponse &&
+                    $response->json(['error' => 'You do not have permission to perform the requested action.'])
+                )
+            );
     }
 
     /**
index 080515173d67cdd6cdf3605dc9e66c7ad1de42a4..98e0dfbacf4c4cfb6321a55d9aa44fe9e491c6ef 100644 (file)
@@ -62,7 +62,7 @@ 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 $type, Entity $entity = null)
+    protected function assertActivityExists(string $type, ?Entity $entity = null, string $detail = '')
     {
         $detailsToCheck = ['type' => $type];
 
@@ -71,6 +71,10 @@ abstract class TestCase extends BaseTestCase
             $detailsToCheck['entity_id'] = $entity->id;
         }
 
+        if ($detail) {
+            $detailsToCheck['detail'] = $detail;
+        }
+
         $this->assertDatabaseHas('activities', $detailsToCheck);
     }
 }
index 39a9d796b12a218085e092235ffe21f553d91bd6..79f173c9b1bee136dee5552093c418eb26cb5488 100644 (file)
@@ -26,6 +26,14 @@ class TestResponse extends BaseTestResponse
         return $this->crawlerInstance;
     }
 
+    /**
+     * Get the HTML of the first element at the given selector.
+     */
+    public function getElementHtml(string $selector): string
+    {
+        return $this->crawler()->filter($selector)->first()->outerHtml();
+    }
+
     /**
      * Assert the response contains the specified element.
      *
index bab85be7a5e4ff9f8c0dd8bff3abbc785cb54cbc..2cab765ae4345c6958d2a2e54988dffb8cccab4b 100644 (file)
@@ -7,9 +7,9 @@ use BookStack\Entities\Models\Page;
 use BookStack\Entities\Tools\PageContent;
 use BookStack\Facades\Theme;
 use BookStack\Theming\ThemeEvents;
-use File;
 use Illuminate\Http\Request;
 use Illuminate\Http\Response;
+use Illuminate\Support\Facades\File;
 use League\CommonMark\ConfigurableEnvironmentInterface;
 
 class ThemeTest extends TestCase
index f45d201363294274588d2e2ad318b8f970714a61..207fb7f59e3865aa607a6a36dc66696f1c63f835 100644 (file)
@@ -76,6 +76,12 @@ class ConfigTest extends TestCase
         );
     }
 
+    public function test_dompdf_remote_fetching_controlled_by_allow_untrusted_server_fetching_false()
+    {
+        $this->checkEnvConfigResult('ALLOW_UNTRUSTED_SERVER_FETCHING', 'false', 'dompdf.defines.enable_remote', false);
+        $this->checkEnvConfigResult('ALLOW_UNTRUSTED_SERVER_FETCHING', 'true', 'dompdf.defines.enable_remote', true);
+    }
+
     /**
      * Set an environment variable of the given name and value
      * then check the given config key to see if it matches the given result.
index 4fd7bacc7dc465fd648e6c25d64efd8284547d7b..ed2fb5f04808c5ebddfce6cc8165c0b09a8ded5e 100644 (file)
 namespace Tests\User;
 
 use BookStack\Actions\ActivityType;
+use BookStack\Auth\Role;
 use BookStack\Auth\User;
 use BookStack\Entities\Models\Page;
+use Illuminate\Support\Facades\Hash;
+use Illuminate\Support\Str;
 use Tests\TestCase;
 
 class UserManagementTest extends TestCase
 {
+    public function test_user_creation()
+    {
+        /** @var User $user */
+        $user = factory(User::class)->make();
+        $adminRole = Role::getRole('admin');
+
+        $resp = $this->asAdmin()->get('/settings/users');
+        $resp->assertElementContains('a[href="' . url('/settings/users/create') . '"]', 'Add New User');
+
+        $this->get('/settings/users/create')
+            ->assertElementContains('form[action="' . url('/settings/users/create') . '"]', 'Save');
+
+        $resp = $this->post('/settings/users/create', [
+            'name'                          => $user->name,
+            'email'                         => $user->email,
+            'password'                      => $user->password,
+            'password-confirm'              => $user->password,
+            'roles[' . $adminRole->id . ']' => 'true',
+        ]);
+        $resp->assertRedirect('/settings/users');
+
+        $resp = $this->get('/settings/users');
+        $resp->assertSee($user->name);
+
+        $this->assertDatabaseHas('users', $user->only('name', 'email'));
+
+        $user->refresh();
+        $this->assertStringStartsWith(Str::slug($user->name), $user->slug);
+    }
+
+    public function test_user_updating()
+    {
+        $user = $this->getNormalUser();
+        $password = $user->password;
+
+        $resp = $this->asAdmin()->get('/settings/users/' . $user->id);
+        $resp->assertSee($user->email);
+
+        $this->put($user->getEditUrl(), [
+            'name' => 'Barry Scott',
+        ])->assertRedirect('/settings/users');
+
+        $this->assertDatabaseHas('users', ['id' => $user->id, 'name' => 'Barry Scott', 'password' => $password]);
+        $this->assertDatabaseMissing('users', ['name' => $user->name]);
+
+        $user->refresh();
+        $this->assertStringStartsWith(Str::slug($user->name), $user->slug);
+    }
+
+    public function test_user_password_update()
+    {
+        $user = $this->getNormalUser();
+        $userProfilePage = '/settings/users/' . $user->id;
+
+        $this->asAdmin()->get($userProfilePage);
+        $this->put($userProfilePage, [
+            'password' => 'newpassword',
+        ])->assertRedirect($userProfilePage);
+
+        $this->get($userProfilePage)->assertSee('Password confirmation required');
+
+        $this->put($userProfilePage, [
+            'password'         => 'newpassword',
+            'password-confirm' => 'newpassword',
+        ])->assertRedirect('/settings/users');
+
+        $userPassword = User::query()->find($user->id)->password;
+        $this->assertTrue(Hash::check('newpassword', $userPassword));
+    }
+
+    public function test_user_cannot_be_deleted_if_last_admin()
+    {
+        $adminRole = Role::getRole('admin');
+
+        // Delete all but one admin user if there are more than one
+        $adminUsers = $adminRole->users;
+        if (count($adminUsers) > 1) {
+            /** @var User $user */
+            foreach ($adminUsers->splice(1) as $user) {
+                $user->delete();
+            }
+        }
+
+        // Ensure we currently only have 1 admin user
+        $this->assertEquals(1, $adminRole->users()->count());
+        /** @var User $user */
+        $user = $adminRole->users->first();
+
+        $resp = $this->asAdmin()->delete('/settings/users/' . $user->id);
+        $resp->assertRedirect('/settings/users/' . $user->id);
+
+        $resp = $this->get('/settings/users/' . $user->id);
+        $resp->assertSee('You cannot delete the only admin');
+
+        $this->assertDatabaseHas('users', ['id' => $user->id]);
+    }
+
     public function test_delete()
     {
         $editor = $this->getEditor();
@@ -42,4 +142,26 @@ class UserManagementTest extends TestCase
             'owned_by' => $newOwner->id,
         ]);
     }
+
+    public function test_guest_profile_shows_limited_form()
+    {
+        $guest = User::getDefault();
+        $resp = $this->asAdmin()->get('/settings/users/' . $guest->id);
+        $resp->assertSee('Guest');
+        $resp->assertElementNotExists('#password');
+    }
+
+    public function test_guest_profile_cannot_be_deleted()
+    {
+        $guestUser = User::getDefault();
+        $resp = $this->asAdmin()->get('/settings/users/' . $guestUser->id . '/delete');
+        $resp->assertSee('Delete User');
+        $resp->assertSee('Guest');
+        $resp->assertElementContains('form[action$="/settings/users/' . $guestUser->id . '"] button', 'Confirm');
+
+        $resp = $this->delete('/settings/users/' . $guestUser->id);
+        $resp->assertRedirect('/settings/users/' . $guestUser->id);
+        $resp = $this->followRedirects($resp);
+        $resp->assertSee('cannot delete the guest user');
+    }
 }
index 1d5d3e7297ef29b2c58a6d117a4a461324fc45db..b39c2c47c84bee8145b0340baade7a183d5bdac9 100644 (file)
@@ -2,6 +2,7 @@
 
 namespace Tests\User;
 
+use BookStack\Entities\Models\Bookshelf;
 use Tests\TestCase;
 
 class UserPreferencesTest extends TestCase
@@ -106,4 +107,44 @@ class UserPreferencesTest extends TestCase
         $home = $this->get('/login');
         $home->assertElementExists('.dark-mode');
     }
+
+    public function test_books_view_type_preferences_when_list()
+    {
+        $editor = $this->getEditor();
+        setting()->putUser($editor, 'books_view_type', 'list');
+
+        $this->actingAs($editor)->get('/books')
+            ->assertElementNotExists('.featured-image-container')
+            ->assertElementExists('.content-wrap .entity-list-item');
+    }
+
+    public function test_books_view_type_preferences_when_grid()
+    {
+        $editor = $this->getEditor();
+        setting()->putUser($editor, 'books_view_type', 'grid');
+
+        $this->actingAs($editor)->get('/books')
+            ->assertElementExists('.featured-image-container');
+    }
+
+    public function test_shelf_view_type_change()
+    {
+        $editor = $this->getEditor();
+        /** @var Bookshelf $shelf */
+        $shelf = Bookshelf::query()->first();
+        setting()->putUser($editor, 'bookshelf_view_type', 'list');
+
+        $this->actingAs($editor)->get($shelf->getUrl())
+            ->assertElementNotExists('.featured-image-container')
+            ->assertElementExists('.content-wrap .entity-list-item')
+            ->assertSee('Grid View');
+
+        $req = $this->patch("/settings/users/{$editor->id}/switch-shelf-view", ['view_type' => 'grid']);
+        $req->assertRedirect($shelf->getUrl());
+
+        $this->actingAs($editor)->get($shelf->getUrl())
+            ->assertElementExists('.featured-image-container')
+            ->assertElementNotExists('.content-wrap .entity-list-item')
+            ->assertSee('List View');
+    }
 }
index 859a036e0e4a9a06653aa2da6bfff2aac7fe18fc..3942efa8e3d095a1d1084594d4c0d9959e91fde9 100644 (file)
@@ -5,11 +5,13 @@ namespace Tests\User;
 use Activity;
 use BookStack\Actions\ActivityType;
 use BookStack\Auth\User;
-use BookStack\Entities\Models\Bookshelf;
-use Tests\BrowserKitTest;
+use Tests\TestCase;
 
-class UserProfileTest extends BrowserKitTest
+class UserProfileTest extends TestCase
 {
+    /**
+     * @var User
+     */
     protected $user;
 
     public function setUp(): void
@@ -21,74 +23,73 @@ class UserProfileTest extends BrowserKitTest
     public function test_profile_page_shows_name()
     {
         $this->asAdmin()
-            ->visit('/user/' . $this->user->slug)
-            ->see($this->user->name);
+            ->get('/user/' . $this->user->slug)
+            ->assertSee($this->user->name);
     }
 
     public function test_profile_page_shows_recent_entities()
     {
         $content = $this->createEntityChainBelongingToUser($this->user, $this->user);
 
-        $this->asAdmin()
-            ->visit('/user/' . $this->user->slug)
-            // Check the recently created page is shown
-            ->see($content['page']->name)
-            // Check the recently created chapter is shown
-            ->see($content['chapter']->name)
-            // Check the recently created book is shown
-            ->see($content['book']->name);
+        $resp = $this->asAdmin()->get('/user/' . $this->user->slug);
+        // Check the recently created page is shown
+        $resp->assertSee($content['page']->name);
+        // Check the recently created chapter is shown
+        $resp->assertSee($content['chapter']->name);
+        // Check the recently created book is shown
+        $resp->assertSee($content['book']->name);
     }
 
     public function test_profile_page_shows_created_content_counts()
     {
-        $newUser = $this->getNewBlankUser();
+        $newUser = factory(User::class)->create();
 
-        $this->asAdmin()->visit('/user/' . $newUser->slug)
-            ->see($newUser->name)
-            ->seeInElement('#content-counts', '0 Books')
-            ->seeInElement('#content-counts', '0 Chapters')
-            ->seeInElement('#content-counts', '0 Pages');
+        $this->asAdmin()->get('/user/' . $newUser->slug)
+            ->assertSee($newUser->name)
+            ->assertElementContains('#content-counts', '0 Books')
+            ->assertElementContains('#content-counts', '0 Chapters')
+            ->assertElementContains('#content-counts', '0 Pages');
 
         $this->createEntityChainBelongingToUser($newUser, $newUser);
 
-        $this->asAdmin()->visit('/user/' . $newUser->slug)
-            ->see($newUser->name)
-            ->seeInElement('#content-counts', '1 Book')
-            ->seeInElement('#content-counts', '1 Chapter')
-            ->seeInElement('#content-counts', '1 Page');
+        $this->asAdmin()->get('/user/' . $newUser->slug)
+            ->assertSee($newUser->name)
+            ->assertElementContains('#content-counts', '1 Book')
+            ->assertElementContains('#content-counts', '1 Chapter')
+            ->assertElementContains('#content-counts', '1 Page');
     }
 
     public function test_profile_page_shows_recent_activity()
     {
-        $newUser = $this->getNewBlankUser();
+        $newUser = factory(User::class)->create();
         $this->actingAs($newUser);
         $entities = $this->createEntityChainBelongingToUser($newUser, $newUser);
         Activity::addForEntity($entities['book'], ActivityType::BOOK_UPDATE);
         Activity::addForEntity($entities['page'], ActivityType::PAGE_CREATE);
 
-        $this->asAdmin()->visit('/user/' . $newUser->slug)
-            ->seeInElement('#recent-user-activity', 'updated book')
-            ->seeInElement('#recent-user-activity', 'created page')
-            ->seeInElement('#recent-user-activity', $entities['page']->name);
+        $this->asAdmin()->get('/user/' . $newUser->slug)
+            ->assertElementContains('#recent-user-activity', 'updated book')
+            ->assertElementContains('#recent-user-activity', 'created page')
+            ->assertElementContains('#recent-user-activity', $entities['page']->name);
     }
 
-    public function test_clicking_user_name_in_activity_leads_to_profile_page()
+    public function test_user_activity_has_link_leading_to_profile()
     {
-        $newUser = $this->getNewBlankUser();
+        $newUser = factory(User::class)->create();
         $this->actingAs($newUser);
         $entities = $this->createEntityChainBelongingToUser($newUser, $newUser);
         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->slug)
-            ->see($newUser->name);
+        $linkSelector = '#recent-activity a[href$="/user/' . $newUser->slug . '"]';
+        $this->asAdmin()->get('/')
+            ->assertElementContains($linkSelector, $newUser->name);
     }
 
     public function test_profile_has_search_links_in_created_entity_lists()
     {
         $user = $this->getEditor();
-        $resp = $this->actingAs($this->getAdmin())->visit('/user/' . $user->slug);
+        $resp = $this->actingAs($this->getAdmin())->get('/user/' . $user->slug);
 
         $expectedLinks = [
             '/search?term=%7Bcreated_by%3A' . $user->slug . '%7D+%7Btype%3Apage%7D',
@@ -98,66 +99,7 @@ class UserProfileTest extends BrowserKitTest
         ];
 
         foreach ($expectedLinks as $link) {
-            $resp->seeInElement('[href$="' . $link . '"]', 'View All');
+            $resp->assertElementContains('[href$="' . $link . '"]', 'View All');
         }
     }
-
-    public function test_guest_profile_shows_limited_form()
-    {
-        $this->asAdmin()
-            ->visit('/settings/users')
-            ->click('Guest')
-            ->dontSeeElement('#password');
-    }
-
-    public function test_guest_profile_cannot_be_deleted()
-    {
-        $guestUser = User::getDefault();
-        $this->asAdmin()->visit('/settings/users/' . $guestUser->id . '/delete')
-            ->see('Delete User')->see('Guest')
-            ->press('Confirm')
-            ->seePageIs('/settings/users/' . $guestUser->id)
-            ->see('cannot delete the guest user');
-    }
-
-    public function test_books_view_is_list()
-    {
-        $editor = $this->getEditor();
-        setting()->putUser($editor, 'books_view_type', 'list');
-
-        $this->actingAs($editor)
-            ->visit('/books')
-            ->pageNotHasElement('.featured-image-container')
-            ->pageHasElement('.content-wrap .entity-list-item');
-    }
-
-    public function test_books_view_is_grid()
-    {
-        $editor = $this->getEditor();
-        setting()->putUser($editor, 'books_view_type', 'grid');
-
-        $this->actingAs($editor)
-            ->visit('/books')
-            ->pageHasElement('.featured-image-container');
-    }
-
-    public function test_shelf_view_type_change()
-    {
-        $editor = $this->getEditor();
-        $shelf = Bookshelf::query()->first();
-        setting()->putUser($editor, 'bookshelf_view_type', 'list');
-
-        $this->actingAs($editor)->visit($shelf->getUrl())
-            ->pageNotHasElement('.featured-image-container')
-            ->pageHasElement('.content-wrap .entity-list-item')
-            ->see('Grid View');
-
-        $req = $this->patch("/settings/users/{$editor->id}/switch-shelf-view", ['view_type' => 'grid']);
-        $req->assertRedirectedTo($shelf->getUrl());
-
-        $this->actingAs($editor)->visit($shelf->getUrl())
-            ->pageHasElement('.featured-image-container')
-            ->pageNotHasElement('.content-wrap .entity-list-item')
-            ->see('List View');
-    }
 }