]> BookStack Code Mirror - bookstack/commitdiff
Merge branch 'patch-1' of git://github.com/DeftNerd/BookStack into DeftNerd-patch-1
authorDan Brown <redacted>
Thu, 17 Oct 2019 13:58:20 +0000 (14:58 +0100)
committerDan Brown <redacted>
Thu, 17 Oct 2019 13:58:20 +0000 (14:58 +0100)
519 files changed:
.browserslistrc [deleted file]
.env.example.complete
.github/workflows/phpunit.yml [new file with mode: 0644]
.gitignore
.travis.yml [deleted file]
app/Actions/Activity.php
app/Actions/ActivityService.php
app/Actions/ViewService.php
app/Application.php [new file with mode: 0644]
app/Auth/Access/EmailConfirmationService.php
app/Auth/Access/SocialAuthService.php
app/Auth/Access/UserInviteService.php [new file with mode: 0644]
app/Auth/Access/UserTokenService.php [new file with mode: 0644]
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 [moved from config/app.php with 96% similarity]
app/Config/auth.php [moved from config/auth.php with 98% similarity]
app/Config/broadcasting.php [moved from config/broadcasting.php with 80% similarity]
app/Config/cache.php [moved from config/cache.php with 86% similarity]
app/Config/database.php [moved from config/database.php with 75% similarity]
app/Config/debugbar.php [new file with mode: 0644]
app/Config/dompdf.php [moved from config/dompdf.php with 98% similarity]
app/Config/filesystems.php [moved from config/filesystems.php with 88% similarity]
app/Config/hashing.php [new file with mode: 0644]
app/Config/logging.php [new file with mode: 0644]
app/Config/mail.php [moved from config/mail.php with 77% similarity]
app/Config/queue.php [moved from config/queue.php with 52% similarity]
app/Config/services.php [moved from config/services.php with 89% similarity]
app/Config/session.php [moved from config/session.php with 91% similarity]
app/Config/setting-defaults.php [moved from config/setting-defaults.php with 84% similarity]
app/Config/snappy.php [moved from config/snappy.php with 100% similarity]
app/Config/view.php [moved from config/view.php with 100% similarity]
app/Console/Commands/CreateAdmin.php
app/Entities/Book.php
app/Entities/BookChild.php [new file with mode: 0644]
app/Entities/Bookshelf.php
app/Entities/BreadcrumbsViewComposer.php
app/Entities/Chapter.php
app/Entities/Entity.php
app/Entities/EntityProvider.php
app/Entities/ExportService.php
app/Entities/HasCoverImage.php [new file with mode: 0644]
app/Entities/Managers/BookContents.php [new file with mode: 0644]
app/Entities/Managers/EntityContext.php [moved from app/Entities/EntityContextManager.php with 53% similarity]
app/Entities/Managers/PageContent.php [new file with mode: 0644]
app/Entities/Managers/PageEditActivity.php [new file with mode: 0644]
app/Entities/Managers/TrashCan.php [new file with mode: 0644]
app/Entities/Page.php
app/Entities/PageRevision.php
app/Entities/Repos/BaseRepo.php [new file with mode: 0644]
app/Entities/Repos/BookRepo.php [new file with mode: 0644]
app/Entities/Repos/BookshelfRepo.php [new file with mode: 0644]
app/Entities/Repos/ChapterRepo.php [new file with mode: 0644]
app/Entities/Repos/EntityRepo.php [deleted file]
app/Entities/Repos/PageRepo.php
app/Entities/SearchService.php
app/Entities/SlugGenerator.php [new file with mode: 0644]
app/Exceptions/MoveOperationException.php [new file with mode: 0644]
app/Exceptions/SortOperationException.php [new file with mode: 0644]
app/Exceptions/UserTokenExpiredException.php [new file with mode: 0644]
app/Exceptions/UserTokenNotFoundException.php [new file with mode: 0644]
app/Facades/Permissions.php [new file with mode: 0644]
app/Http/Controllers/AttachmentController.php
app/Http/Controllers/Auth/ConfirmEmailController.php [new file with mode: 0644]
app/Http/Controllers/Auth/ForgotPasswordController.php
app/Http/Controllers/Auth/LoginController.php
app/Http/Controllers/Auth/RegisterController.php
app/Http/Controllers/Auth/ResetPasswordController.php
app/Http/Controllers/Auth/UserInviteController.php [new file with mode: 0644]
app/Http/Controllers/BookController.php
app/Http/Controllers/BookExportController.php [new file with mode: 0644]
app/Http/Controllers/BookSortController.php [new file with mode: 0644]
app/Http/Controllers/BookshelfController.php
app/Http/Controllers/ChapterController.php
app/Http/Controllers/ChapterExportController.php [new file with mode: 0644]
app/Http/Controllers/CommentController.php
app/Http/Controllers/Controller.php
app/Http/Controllers/HomeController.php
app/Http/Controllers/Images/ImageController.php
app/Http/Controllers/PageController.php
app/Http/Controllers/PageExportController.php [new file with mode: 0644]
app/Http/Controllers/PageRevisionController.php [new file with mode: 0644]
app/Http/Controllers/PageTemplateController.php [new file with mode: 0644]
app/Http/Controllers/PermissionController.php
app/Http/Controllers/SearchController.php
app/Http/Controllers/SettingController.php
app/Http/Controllers/UserController.php
app/Http/Kernel.php
app/Http/Middleware/Authenticate.php
app/Http/Middleware/CheckForMaintenanceMode.php [new file with mode: 0644]
app/Http/Middleware/GlobalViewData.php [new file with mode: 0644]
app/Http/Middleware/Localization.php
app/Http/Middleware/TrustProxies.php
app/Http/Middleware/VerifyCsrfToken.php
app/Http/Request.php [new file with mode: 0644]
app/Notifications/ConfirmEmail.php
app/Notifications/ResetPassword.php
app/Notifications/UserInvite.php [new file with mode: 0644]
app/Providers/AppServiceProvider.php
app/Providers/CustomFacadeProvider.php
app/Providers/PaginationServiceProvider.php
app/Uploads/Attachment.php
app/Uploads/AttachmentService.php
app/Uploads/ImageService.php
app/helpers.php
bootstrap/app.php
composer.json
composer.lock
database/factories/ModelFactory.php
database/migrations/2019_07_07_112515_add_template_support.php [new file with mode: 0644]
database/migrations/2019_08_17_140214_add_user_invites_table.php [new file with mode: 0644]
database/seeds/DatabaseSeeder.php
database/seeds/DummyContentSeeder.php
database/seeds/LargeContentSeeder.php
dev/docker/Dockerfile [new file with mode: 0644]
dev/docker/entrypoint.app.sh [new file with mode: 0755]
dev/docker/entrypoint.node.sh [new file with mode: 0755]
docker-compose.yml [new file with mode: 0644]
package-lock.json
package.json
phpunit.xml
public/.htaccess
public/index.php
public/libs/jq-color-picker/tiny-color-picker.min.js [deleted file]
public/libs/jquery-sortable/jquery-sortable.min.js [deleted file]
public/uploads/.gitignore
public/uploads/.htaccess [new file with mode: 0755]
public/web.config [new file with mode: 0644]
readme.md
resources/assets/js/components/chapter-toggle.js [deleted file]
resources/assets/js/components/dropdown.js [deleted file]
resources/assets/js/components/expand-toggle.js [deleted file]
resources/assets/js/components/page-display.js [deleted file]
resources/assets/js/components/shelf-sort.js [deleted file]
resources/assets/js/components/toggle-switch.js [deleted file]
resources/assets/js/services/dom-polyfills.js [deleted file]
resources/assets/js/services/events.js [deleted file]
resources/assets/js/services/global-ui.js [deleted file]
resources/assets/js/services/http.js [deleted file]
resources/assets/sass/_colors.scss [deleted file]
resources/assets/sass/print-styles.scss [deleted file]
resources/icons/add-circle.svg [moved from resources/assets/icons/add-circle.svg with 100% similarity]
resources/icons/add.svg [moved from resources/assets/icons/add.svg with 100% similarity]
resources/icons/attach.svg [moved from resources/assets/icons/attach.svg with 100% similarity]
resources/icons/auth/azure.svg [moved from resources/assets/icons/auth/azure.svg with 100% similarity]
resources/icons/auth/discord.svg [moved from resources/assets/icons/auth/discord.svg with 100% similarity]
resources/icons/auth/facebook.svg [moved from resources/assets/icons/auth/facebook.svg with 100% similarity]
resources/icons/auth/github.svg [moved from resources/assets/icons/auth/github.svg with 100% similarity]
resources/icons/auth/gitlab.svg [moved from resources/assets/icons/auth/gitlab.svg with 100% similarity]
resources/icons/auth/google.svg [moved from resources/assets/icons/auth/google.svg with 100% similarity]
resources/icons/auth/okta.svg [moved from resources/assets/icons/auth/okta.svg with 100% similarity]
resources/icons/auth/slack.svg [moved from resources/assets/icons/auth/slack.svg with 100% similarity]
resources/icons/auth/twitch.svg [moved from resources/assets/icons/auth/twitch.svg with 100% similarity]
resources/icons/auth/twitter.svg [moved from resources/assets/icons/auth/twitter.svg with 100% similarity]
resources/icons/back.svg [moved from resources/assets/icons/back.svg with 100% similarity]
resources/icons/book.svg [moved from resources/assets/icons/book.svg with 100% similarity]
resources/icons/books.svg [moved from resources/assets/icons/books.svg with 100% similarity]
resources/icons/bookshelf.svg [moved from resources/assets/icons/bookshelf.svg with 100% similarity]
resources/icons/cancel.svg [moved from resources/assets/icons/cancel.svg with 100% similarity]
resources/icons/caret-down.svg [moved from resources/assets/icons/caret-down.svg with 100% similarity]
resources/icons/caret-left-circle.svg [moved from resources/assets/icons/caret-left-circle.svg with 100% similarity]
resources/icons/caret-right-circle.svg [moved from resources/assets/icons/caret-right-circle.svg with 100% similarity]
resources/icons/caret-right.svg [moved from resources/assets/icons/caret-right.svg with 100% similarity]
resources/icons/chapter.svg [moved from resources/assets/icons/chapter.svg with 100% similarity]
resources/icons/check-circle.svg [moved from resources/assets/icons/check-circle.svg with 100% similarity]
resources/icons/check.svg [moved from resources/assets/icons/check.svg with 100% similarity]
resources/icons/chevron-down.svg [new file with mode: 0644]
resources/icons/chevron-right.svg [moved from resources/assets/icons/chevron-right.svg with 100% similarity]
resources/icons/chevron-up.svg [moved from resources/assets/icons/chevron-up.svg with 100% similarity]
resources/icons/close.svg [moved from resources/assets/icons/close.svg with 100% similarity]
resources/icons/comment.svg [moved from resources/assets/icons/comment.svg with 100% similarity]
resources/icons/copy.svg [moved from resources/assets/icons/copy.svg with 81% similarity]
resources/icons/danger.svg [moved from resources/assets/icons/danger.svg with 100% similarity]
resources/icons/delete.svg [moved from resources/assets/icons/delete.svg with 100% similarity]
resources/icons/drawing.svg [moved from resources/assets/icons/drawing.svg with 100% similarity]
resources/icons/edit.svg [moved from resources/assets/icons/edit.svg with 100% similarity]
resources/icons/expand-text.svg [moved from resources/assets/icons/expand-text.svg with 100% similarity]
resources/icons/export.svg [moved from resources/assets/icons/export.svg with 100% similarity]
resources/icons/file.svg [moved from resources/assets/icons/file.svg with 100% similarity]
resources/icons/folder.svg [moved from resources/assets/icons/folder.svg with 100% similarity]
resources/icons/grid.svg [moved from resources/assets/icons/grid.svg with 100% similarity]
resources/icons/grip.svg [moved from resources/assets/icons/grip.svg with 100% similarity]
resources/icons/history.svg [moved from resources/assets/icons/history.svg with 100% similarity]
resources/icons/image.svg [moved from resources/assets/icons/image.svg with 100% similarity]
resources/icons/images.svg [moved from resources/assets/icons/images.svg with 100% similarity]
resources/icons/include.svg [moved from resources/assets/icons/include.svg with 100% similarity]
resources/icons/info-filled.svg [moved from resources/assets/icons/info-filled.svg with 100% similarity]
resources/icons/info.svg [moved from resources/assets/icons/info.svg with 100% similarity]
resources/icons/link.svg [moved from resources/assets/icons/link.svg with 85% similarity]
resources/icons/list.svg [moved from resources/assets/icons/list.svg with 100% similarity]
resources/icons/lock-open.svg [moved from resources/assets/icons/lock-open.svg with 100% similarity]
resources/icons/lock.svg [moved from resources/assets/icons/lock.svg with 100% similarity]
resources/icons/login.svg [moved from resources/assets/icons/login.svg with 100% similarity]
resources/icons/logout.svg [moved from resources/assets/icons/logout.svg with 100% similarity]
resources/icons/more.svg [moved from resources/assets/icons/more.svg with 100% similarity]
resources/icons/new-user.svg [moved from resources/assets/icons/new-user.svg with 100% similarity]
resources/icons/open-book.svg [moved from resources/assets/icons/open-book.svg with 100% similarity]
resources/icons/page.svg [moved from resources/assets/icons/page.svg with 100% similarity]
resources/icons/permission.svg [moved from resources/assets/icons/permission.svg with 100% similarity]
resources/icons/popular.svg [moved from resources/assets/icons/popular.svg with 100% similarity]
resources/icons/reply.svg [moved from resources/assets/icons/reply.svg with 100% similarity]
resources/icons/save.svg [moved from resources/assets/icons/save.svg with 100% similarity]
resources/icons/search.svg [moved from resources/assets/icons/search.svg with 100% similarity]
resources/icons/settings.svg [moved from resources/assets/icons/settings.svg with 100% similarity]
resources/icons/sort-down.svg [moved from resources/assets/icons/sort-down.svg with 100% similarity]
resources/icons/sort-up.svg [moved from resources/assets/icons/sort-up.svg with 100% similarity]
resources/icons/sort.svg [moved from resources/assets/icons/sort.svg with 100% similarity]
resources/icons/spanner.svg [moved from resources/assets/icons/spanner.svg with 100% similarity]
resources/icons/star-circle.svg [moved from resources/assets/icons/star-circle.svg with 100% similarity]
resources/icons/star.svg [moved from resources/assets/icons/star.svg with 100% similarity]
resources/icons/swap-vertical.svg [moved from resources/assets/icons/swap-vertical.svg with 100% similarity]
resources/icons/tag.svg [moved from resources/assets/icons/tag.svg with 100% similarity]
resources/icons/template.svg [new file with mode: 0644]
resources/icons/time.svg [moved from resources/assets/icons/time.svg with 100% similarity]
resources/icons/user.svg [moved from resources/assets/icons/user.svg with 100% similarity]
resources/icons/users-add.svg [moved from resources/assets/icons/users-add.svg with 100% similarity]
resources/icons/users.svg [moved from resources/assets/icons/users.svg with 100% similarity]
resources/icons/view.svg [moved from resources/assets/icons/view.svg with 100% similarity]
resources/icons/warning.svg [moved from resources/assets/icons/warning.svg with 100% similarity]
resources/js/components/back-to-top.js [moved from resources/assets/js/components/back-to-top.js with 100% similarity]
resources/js/components/book-sort.js [new file with mode: 0644]
resources/js/components/breadcrumb-listing.js [moved from resources/assets/js/components/breadcrumb-listing.js with 86% similarity]
resources/js/components/chapter-toggle.js [new file with mode: 0644]
resources/js/components/collapsible.js [moved from resources/assets/js/components/collapsible.js with 70% similarity]
resources/js/components/custom-checkbox.js [new file with mode: 0644]
resources/js/components/dropdown.js [new file with mode: 0644]
resources/js/components/editor-toolbox.js [moved from resources/assets/js/components/editor-toolbox.js with 90% similarity]
resources/js/components/entity-permissions-editor.js [new file with mode: 0644]
resources/js/components/entity-selector-popup.js [moved from resources/assets/js/components/entity-selector-popup.js with 100% similarity]
resources/js/components/entity-selector.js [moved from resources/assets/js/components/entity-selector.js with 100% similarity]
resources/js/components/expand-toggle.js [new file with mode: 0644]
resources/js/components/header-mobile-toggle.js [moved from resources/assets/js/components/header-mobile-toggle.js with 100% similarity]
resources/js/components/homepage-control.js [moved from resources/assets/js/components/homepage-control.js with 100% similarity]
resources/js/components/image-picker.js [moved from resources/assets/js/components/image-picker.js with 100% similarity]
resources/js/components/index.js [moved from resources/assets/js/components/index.js with 85% similarity]
resources/js/components/list-sort-control.js [moved from resources/assets/js/components/list-sort-control.js with 100% similarity]
resources/js/components/markdown-editor.js [moved from resources/assets/js/components/markdown-editor.js with 81% similarity]
resources/js/components/new-user-password.js [new file with mode: 0644]
resources/js/components/notification.js [moved from resources/assets/js/components/notification.js with 100% similarity]
resources/js/components/overlay.js [moved from resources/assets/js/components/overlay.js with 73% similarity]
resources/js/components/page-comments.js [moved from resources/assets/js/components/page-comments.js with 96% similarity]
resources/js/components/page-display.js [new file with mode: 0644]
resources/js/components/page-picker.js [moved from resources/assets/js/components/page-picker.js with 100% similarity]
resources/js/components/permissions-table.js [moved from resources/assets/js/components/permissions-table.js with 100% similarity]
resources/js/components/setting-app-color-picker.js [new file with mode: 0644]
resources/js/components/shelf-sort.js [new file with mode: 0644]
resources/js/components/sidebar.js [moved from resources/assets/js/components/sidebar.js with 100% similarity]
resources/js/components/template-manager.js [new file with mode: 0644]
resources/js/components/toggle-switch.js [new file with mode: 0644]
resources/js/components/tri-layout.js [moved from resources/assets/js/components/tri-layout.js with 67% similarity]
resources/js/components/wysiwyg-editor.js [moved from resources/assets/js/components/wysiwyg-editor.js with 89% similarity]
resources/js/index.js [moved from resources/assets/js/index.js with 68% similarity]
resources/js/services/animations.js [new file with mode: 0644]
resources/js/services/code.js [moved from resources/assets/js/services/code.js with 95% similarity]
resources/js/services/dates.js [moved from resources/assets/js/services/dates.js with 100% similarity]
resources/js/services/dom.js [new file with mode: 0644]
resources/js/services/drawio.js [moved from resources/assets/js/services/drawio.js with 100% similarity]
resources/js/services/events.js [new file with mode: 0644]
resources/js/services/http.js [new file with mode: 0644]
resources/js/services/translations.js [moved from resources/assets/js/services/translations.js with 65% similarity]
resources/js/services/util.js [new file with mode: 0644]
resources/js/vues/attachment-manager.js [moved from resources/assets/js/vues/attachment-manager.js with 100% similarity]
resources/js/vues/code-editor.js [moved from resources/assets/js/vues/code-editor.js with 89% similarity]
resources/js/vues/components/autosuggest.js [moved from resources/assets/js/vues/components/autosuggest.js with 84% similarity]
resources/js/vues/components/dropzone.js [moved from resources/assets/js/vues/components/dropzone.js with 83% similarity]
resources/js/vues/entity-dashboard.js [moved from resources/assets/js/vues/entity-dashboard.js with 100% similarity]
resources/js/vues/image-manager.js [moved from resources/assets/js/vues/image-manager.js with 98% similarity]
resources/js/vues/page-editor.js [moved from resources/assets/js/vues/page-editor.js with 92% similarity]
resources/js/vues/search.js [moved from resources/assets/js/vues/search.js with 100% similarity]
resources/js/vues/tag-manager.js [moved from resources/assets/js/vues/tag-manager.js with 98% similarity]
resources/js/vues/vues.js [moved from resources/assets/js/vues/vues.js with 100% similarity]
resources/lang/ar/auth.php
resources/lang/cs/auth.php
resources/lang/de/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/en/auth.php
resources/lang/en/common.php
resources/lang/en/entities.php
resources/lang/en/errors.php
resources/lang/en/passwords.php
resources/lang/en/settings.php
resources/lang/en/validation.php
resources/lang/es/auth.php
resources/lang/es/common.php
resources/lang/es/entities.php
resources/lang/es/errors.php
resources/lang/es/settings.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/errors.php
resources/lang/es_AR/settings.php
resources/lang/es_AR/validation.php
resources/lang/fr/auth.php
resources/lang/fr/common.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/hu/activities.php [new file with mode: 0644]
resources/lang/hu/auth.php [new file with mode: 0644]
resources/lang/hu/common.php [new file with mode: 0644]
resources/lang/hu/components.php [new file with mode: 0644]
resources/lang/hu/entities.php [new file with mode: 0644]
resources/lang/hu/errors.php [new file with mode: 0644]
resources/lang/hu/pagination.php [new file with mode: 0644]
resources/lang/hu/passwords.php [new file with mode: 0644]
resources/lang/hu/settings.php [new file with mode: 0644]
resources/lang/hu/validation.php [new file with mode: 0644]
resources/lang/it/auth.php
resources/lang/ja/auth.php
resources/lang/kr/auth.php
resources/lang/nl/auth.php
resources/lang/pl/auth.php
resources/lang/pt_BR/activities.php
resources/lang/pt_BR/auth.php
resources/lang/pt_BR/common.php
resources/lang/pt_BR/components.php
resources/lang/pt_BR/entities.php
resources/lang/pt_BR/errors.php
resources/lang/pt_BR/pagination.php
resources/lang/pt_BR/passwords.php
resources/lang/pt_BR/settings.php
resources/lang/pt_BR/validation.php
resources/lang/ru/auth.php
resources/lang/ru/common.php
resources/lang/ru/components.php
resources/lang/ru/entities.php
resources/lang/ru/errors.php
resources/lang/ru/pagination.php
resources/lang/ru/passwords.php
resources/lang/ru/settings.php
resources/lang/ru/validation.php
resources/lang/sk/auth.php
resources/lang/sv/auth.php
resources/lang/tr/activities.php [new file with mode: 0644]
resources/lang/tr/auth.php [new file with mode: 0644]
resources/lang/tr/common.php [new file with mode: 0644]
resources/lang/tr/components.php [new file with mode: 0644]
resources/lang/tr/entities.php [new file with mode: 0644]
resources/lang/tr/errors.php [new file with mode: 0644]
resources/lang/tr/pagination.php [new file with mode: 0644]
resources/lang/tr/passwords.php [new file with mode: 0644]
resources/lang/tr/settings.php [new file with mode: 0755]
resources/lang/tr/validation.php [new file with mode: 0644]
resources/lang/uk/auth.php
resources/lang/zh_CN/auth.php
resources/lang/zh_TW/auth.php
resources/sass/_animations.scss [moved from resources/assets/sass/_animations.scss with 100% similarity]
resources/sass/_blocks.scss [moved from resources/assets/sass/_blocks.scss with 97% similarity]
resources/sass/_buttons.scss [moved from resources/assets/sass/_buttons.scss with 60% similarity]
resources/sass/_codemirror.scss [moved from resources/assets/sass/_codemirror.scss with 100% similarity]
resources/sass/_colors.scss [new file with mode: 0644]
resources/sass/_components.scss [moved from resources/assets/sass/_components.scss with 93% similarity]
resources/sass/_forms.scss [moved from resources/assets/sass/_forms.scss with 88% similarity]
resources/sass/_header.scss [moved from resources/assets/sass/_header.scss with 93% similarity]
resources/sass/_html.scss [moved from resources/assets/sass/_html.scss with 81% similarity]
resources/sass/_layout.scss [moved from resources/assets/sass/_layout.scss with 92% similarity]
resources/sass/_lists.scss [moved from resources/assets/sass/_lists.scss with 91% similarity]
resources/sass/_mixins.scss [moved from resources/assets/sass/_mixins.scss with 100% similarity]
resources/sass/_pages.scss [moved from resources/assets/sass/_pages.scss with 95% similarity]
resources/sass/_reset.scss [moved from resources/assets/sass/_reset.scss with 100% similarity]
resources/sass/_spacing.scss [moved from resources/assets/sass/_spacing.scss with 100% similarity]
resources/sass/_tables.scss [moved from resources/assets/sass/_tables.scss with 100% similarity]
resources/sass/_text.scss [moved from resources/assets/sass/_text.scss with 88% similarity]
resources/sass/_tinymce.scss [moved from resources/assets/sass/_tinymce.scss with 94% similarity]
resources/sass/_variables.scss [moved from resources/assets/sass/_variables.scss with 79% similarity]
resources/sass/export-styles.scss [moved from resources/assets/sass/export-styles.scss with 97% similarity]
resources/sass/print-styles.scss [new file with mode: 0644]
resources/sass/styles.scss [moved from resources/assets/sass/styles.scss with 88% similarity]
resources/views/auth/forms/login/ldap.blade.php
resources/views/auth/forms/login/standard.blade.php
resources/views/auth/invite-set-password.blade.php [new file with mode: 0644]
resources/views/auth/login.blade.php
resources/views/auth/passwords/email.blade.php
resources/views/auth/passwords/reset.blade.php
resources/views/auth/register.blade.php
resources/views/auth/user-unconfirmed.blade.php
resources/views/base.blade.php
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/form.blade.php
resources/views/books/index.blade.php
resources/views/books/list.blade.php
resources/views/books/permissions.blade.php
resources/views/books/show.blade.php
resources/views/books/sort.blade.php
resources/views/chapters/child-menu.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/form.blade.php
resources/views/chapters/list-item.blade.php
resources/views/chapters/move.blade.php
resources/views/chapters/permissions.blade.php
resources/views/chapters/show.blade.php
resources/views/comments/comment.blade.php
resources/views/comments/comments.blade.php
resources/views/comments/create.blade.php
resources/views/common/header.blade.php
resources/views/common/home-book.blade.php
resources/views/common/home-custom.blade.php
resources/views/common/home-shelves.blade.php
resources/views/common/home-sidebar.blade.php
resources/views/common/home.blade.php
resources/views/components/code-editor.blade.php
resources/views/components/custom-checkbox.blade.php
resources/views/components/entity-selector-popup.blade.php
resources/views/components/expand-toggle.blade.php
resources/views/components/image-manager.blade.php
resources/views/components/image-picker.blade.php
resources/views/components/page-picker.blade.php
resources/views/components/tag-list.blade.php
resources/views/components/tag-manager.blade.php
resources/views/components/toggle-switch.blade.php
resources/views/errors/404.blade.php
resources/views/errors/500.blade.php
resources/views/form/entity-permissions.blade.php
resources/views/form/password.blade.php
resources/views/form/text.blade.php
resources/views/pages/attachment-manager.blade.php [new file with mode: 0644]
resources/views/pages/copy.blade.php
resources/views/pages/delete.blade.php
resources/views/pages/detailed-listing.blade.php
resources/views/pages/edit.blade.php
resources/views/pages/editor-toolbox.blade.php [new file with mode: 0644]
resources/views/pages/export.blade.php
resources/views/pages/form-toolbox.blade.php [deleted file]
resources/views/pages/form.blade.php
resources/views/pages/guest-create.blade.php
resources/views/pages/markdown-editor.blade.php [new file with mode: 0644]
resources/views/pages/move.blade.php
resources/views/pages/permissions.blade.php
resources/views/pages/pointer.blade.php [new file with mode: 0644]
resources/views/pages/revision.blade.php
resources/views/pages/revisions.blade.php
resources/views/pages/show.blade.php
resources/views/pages/template-manager-list.blade.php [new file with mode: 0644]
resources/views/pages/template-manager.blade.php [new file with mode: 0644]
resources/views/pages/wysiwyg-editor.blade.php [new file with mode: 0644]
resources/views/partials/book-tree.blade.php
resources/views/partials/breadcrumb-listing.blade.php
resources/views/partials/breadcrumbs.blade.php
resources/views/partials/custom-styles.blade.php
resources/views/partials/entity-dashboard-search-box.blade.php
resources/views/partials/entity-export-menu.blade.php [new file with mode: 0644]
resources/views/partials/entity-list-item-basic.blade.php
resources/views/partials/notifications.blade.php
resources/views/partials/sort.blade.php
resources/views/partials/view-toggle.blade.php
resources/views/search/all.blade.php
resources/views/settings/index.blade.php
resources/views/settings/maintenance.blade.php
resources/views/settings/navbar.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/form.blade.php
resources/views/settings/roles/index.blade.php
resources/views/shelves/create.blade.php
resources/views/shelves/edit.blade.php
resources/views/shelves/form.blade.php
resources/views/shelves/index.blade.php
resources/views/shelves/list.blade.php
resources/views/shelves/show.blade.php
resources/views/tri-layout.blade.php
resources/views/users/create.blade.php
resources/views/users/delete.blade.php
resources/views/users/edit.blade.php
resources/views/users/form.blade.php
resources/views/users/index.blade.php
resources/views/users/profile.blade.php
resources/views/vendor/notifications/email.blade.php
routes/web.php
storage/framework/cache/.gitignore
tests/Auth/AuthTest.php
tests/Auth/LdapTest.php
tests/Auth/SocialAuthTest.php
tests/Auth/UserInviteTest.php [new file with mode: 0644]
tests/BrowserKitTest.php
tests/CommandsTest.php
tests/Entity/BookShelfTest.php
tests/Entity/CommentSettingTest.php
tests/Entity/EntityTest.php
tests/Entity/ExportTest.php
tests/Entity/MarkdownTest.php
tests/Entity/PageContentTest.php
tests/Entity/PageDraftTest.php
tests/Entity/PageRevisionTest.php
tests/Entity/PageTemplateTest.php [new file with mode: 0644]
tests/Entity/SortTest.php
tests/Entity/TagTest.php
tests/HomepageTest.php
tests/LanguageTest.php
tests/Permissions/RestrictionsTest.php
tests/Permissions/RolesTest.php
tests/SharedTestHelpers.php
tests/Unit/ConfigTest.php [new file with mode: 0644]
tests/Unit/HelpersTest.php [deleted file]
tests/Unit/PageRepoTest.php [deleted file]
tests/Unit/UrlTest.php [new file with mode: 0644]
tests/Uploads/AttachmentTest.php
tests/Uploads/ImageTest.php
tests/UserProfileTest.php
version
webpack.config.js

diff --git a/.browserslistrc b/.browserslistrc
deleted file mode 100644 (file)
index 47c6836..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
->0.25%
-not op_mini all
\ No newline at end of file
index 37421a419e5485c3f4e6d612bf08236e3a0f2159..c4c3f0b85ff1192839395d1cc7edb3b965802b8f 100644 (file)
@@ -89,12 +89,22 @@ REDIS_SERVERS=127.0.0.1:6379:0
 # Queue driver to use
 # Queue not really currently used but may be configurable in the future.
 # Would advise not to change this for now.
-QUEUE_DRIVER=sync
+QUEUE_CONNECTION=sync
 
 # Storage system to use
 # Can be 'local', 'local_secure' or 's3'
 STORAGE_TYPE=local
 
+# Image storage system to use
+# Defaults to the value of STORAGE_TYPE if unset.
+# Accepts the same values as STORAGE_TYPE.
+STORAGE_IMAGE_TYPE=local
+
+# Attachment storage system to use
+# Defaults to the value of STORAGE_TYPE if unset.
+# Accepts the same values as STORAGE_TYPE although 'local' will be forced to 'local_secure'.
+STORAGE_ATTACHMENT_TYPE=local_secure
+
 # Amazon S3 storage configuration
 STORAGE_S3_KEY=your-s3-key
 STORAGE_S3_SECRET=your-s3-secret
diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml
new file mode 100644 (file)
index 0000000..922aa50
--- /dev/null
@@ -0,0 +1,26 @@
+name: phpunit
+
+on: [push, pull_request]
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        php: [7.2, 7.3]
+    steps:
+    - uses: actions/checkout@v1
+    - name: Setup Database
+      run: |
+        mysql -uroot -proot -e 'CREATE DATABASE IF NOT EXISTS `bookstack-test`;'
+        mysql -uroot -proot -e "CREATE USER 'bookstack-test'@'localhost' IDENTIFIED BY 'bookstack-test';"
+        mysql -uroot -proot -e "GRANT ALL ON \`bookstack-test\`.* TO 'bookstack-test'@'localhost';"
+        mysql -uroot -proot -e 'FLUSH PRIVILEGES;'
+    - name: Install composer dependencies & Test
+      run: composer install --prefer-dist --no-interaction --ansi
+    - name: Migrate and seed the database
+      run: |
+        php${{ matrix.php }} artisan migrate --force -n --database=mysql_testing
+        php${{ matrix.php }} artisan db:seed --force -n --class=DummyContentSeeder --database=mysql_testing
+    - name: phpunit
+      run: php${{ matrix.php }} ./vendor/bin/phpunit
index 1b53cbe7ac52416e90f3844194aab4a262c48d20..e5579e4a62f21ca24e6f804fdee275b9655519b6 100644 (file)
@@ -21,4 +21,5 @@ nbproject
 .buildpath
 .project
 .settings/
-webpack-stats.json
\ No newline at end of file
+webpack-stats.json
+.phpunit.result.cache
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644 (file)
index 29727f4..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-dist: trusty
-sudo: false
-language: php
-php:
-  - 7.0.20
-  - 7.1.9
-
-cache:
-  directories:
-    - $HOME/.composer/cache
-
-before_script:
-  - mysql -u root -e 'create database `bookstack-test`;'
-  - mysql -u root -e "CREATE USER 'bookstack-test'@'localhost' IDENTIFIED BY 'bookstack-test';"
-  - mysql -u root -e "GRANT ALL ON \`bookstack-test\`.* TO 'bookstack-test'@'localhost';"
-  - mysql -u root -e "FLUSH PRIVILEGES;"
-  - phpenv config-rm xdebug.ini
-  - composer install --prefer-dist --no-interaction
-  - php artisan clear-compiled -n
-  - php artisan optimize -n
-  - php artisan migrate --force -n --database=mysql_testing
-  - php artisan db:seed --force -n --class=DummyContentSeeder --database=mysql_testing
-
-after_failure:
-  - cat storage/logs/laravel.log
-
-script:
-  - phpunit
index 1ae1811e11461dbd94674c791b0c75143596d8b2..05f0129ddff7c322f8f900a3f5bdf4e6dba65fda 100644 (file)
@@ -3,13 +3,18 @@
 namespace BookStack\Actions;
 
 use BookStack\Auth\User;
+use BookStack\Entities\Entity;
 use BookStack\Model;
 
 /**
- * @property string  key
- * @property \User   user
- * @property \Entity entity
- * @property string  extra
+ * @property string $key
+ * @property User $user
+ * @property Entity $entity
+ * @property string $extra
+ * @property string $entity_type
+ * @property int $entity_id
+ * @property int $user_id
+ * @property int $book_id
  */
 class Activity extends Model
 {
index f4f82a6f4dfbbac6b63c2fb42a15fad0631f77e9..f56f1ca57e03c304c4f4fdcd7201293afe45f8ba 100644 (file)
@@ -1,8 +1,8 @@
 <?php namespace BookStack\Actions;
 
 use BookStack\Auth\Permissions\PermissionService;
+use BookStack\Entities\Book;
 use BookStack\Entities\Entity;
-use Session;
 
 class ActivityService
 {
@@ -12,7 +12,7 @@ class ActivityService
 
     /**
      * ActivityService constructor.
-     * @param \BookStack\Actions\Activity $activity
+     * @param Activity $activity
      * @param PermissionService $permissionService
      */
     public function __construct(Activity $activity, PermissionService $permissionService)
@@ -24,52 +24,57 @@ class ActivityService
 
     /**
      * Add activity data to database.
-     * @param Entity $entity
-     * @param        $activityKey
+     * @param \BookStack\Entities\Entity $entity
+     * @param string $activityKey
      * @param int $bookId
-     * @param bool $extra
      */
-    public function add(Entity $entity, $activityKey, $bookId = 0, $extra = false)
+    public function add(Entity $entity, string $activityKey, int $bookId = null)
     {
-        $activity = $this->activity->newInstance();
-        $activity->user_id = $this->user->id;
-        $activity->book_id = $bookId;
-        $activity->key = strtolower($activityKey);
-        if ($extra !== false) {
-            $activity->extra = $extra;
-        }
+        $activity = $this->newActivityForUser($activityKey, $bookId);
         $entity->activity()->save($activity);
         $this->setNotification($activityKey);
     }
 
     /**
-     * Adds a activity history with a message & without binding to a entity.
-     * @param            $activityKey
+     * Adds a activity history with a message, without binding to a entity.
+     * @param string $activityKey
+     * @param string $message
      * @param int $bookId
-     * @param bool|false $extra
      */
-    public function addMessage($activityKey, $bookId = 0, $extra = false)
+    public function addMessage(string $activityKey, string $message, int $bookId = null)
     {
-        $this->activity->user_id = $this->user->id;
-        $this->activity->book_id = $bookId;
-        $this->activity->key = strtolower($activityKey);
-        if ($extra !== false) {
-            $this->activity->extra = $extra;
-        }
-        $this->activity->save();
+        $this->newActivityForUser($activityKey, $bookId)->forceFill([
+            'extra' => $message
+        ])->save();
+
         $this->setNotification($activityKey);
     }
 
+    /**
+     * Get a new activity instance for the current user.
+     * @param string $key
+     * @param int|null $bookId
+     * @return Activity
+     */
+    protected function newActivityForUser(string $key, int $bookId = null)
+    {
+        return $this->activity->newInstance()->forceFill([
+            'key' => strtolower($key),
+            'user_id' => $this->user->id,
+            'book_id' => $bookId ?? 0,
+        ]);
+    }
 
     /**
      * Removes the entity attachment from each of its activities
      * and instead uses the 'extra' field with the entities name.
      * Used when an entity is deleted.
-     * @param Entity $entity
+     * @param \BookStack\Entities\Entity $entity
      * @return mixed
      */
     public function removeEntity(Entity $entity)
     {
+        // TODO - Rewrite to db query.
         $activities = $entity->activity;
         foreach ($activities as $activity) {
             $activity->extra = $entity->name;
@@ -90,7 +95,11 @@ class ActivityService
     {
         $activityList = $this->permissionService
             ->filterRestrictedEntityRelations($this->activity, 'activities', 'entity_id', 'entity_type')
-            ->orderBy('created_at', 'desc')->with('user', 'entity')->skip($count * $page)->take($count)->get();
+            ->orderBy('created_at', 'desc')
+            ->with('user', 'entity')
+            ->skip($count * $page)
+            ->take($count)
+            ->get();
 
         return $this->filterSimilar($activityList);
     }
@@ -98,7 +107,7 @@ class ActivityService
     /**
      * Gets the latest activity for an entity, Filtering out similar
      * items to prevent a message activity list.
-     * @param Entity $entity
+     * @param \BookStack\Entities\Entity $entity
      * @param int $count
      * @param int $page
      * @return array
@@ -171,7 +180,7 @@ class ActivityService
         $notificationTextKey = 'activities.' . $activityKey . '_notification';
         if (trans()->has($notificationTextKey)) {
             $message = trans($notificationTextKey);
-            Session::flash('success', $message);
+            session()->flash('success', $message);
         }
     }
 }
index 532f31c423ca06ae026bb931f03deb84e16bfc87..324bfaa4ef9fb14c722148529c4bb895692a12e8 100644 (file)
@@ -1,8 +1,10 @@
 <?php namespace BookStack\Actions;
 
 use BookStack\Auth\Permissions\PermissionService;
+use BookStack\Entities\Book;
 use BookStack\Entities\Entity;
 use BookStack\Entities\EntityProvider;
+use DB;
 use Illuminate\Support\Collection;
 
 class ViewService
@@ -13,8 +15,8 @@ class ViewService
 
     /**
      * ViewService constructor.
-     * @param \BookStack\Actions\View $view
-     * @param \BookStack\Auth\Permissions\PermissionService $permissionService
+     * @param View $view
+     * @param PermissionService $permissionService
      * @param EntityProvider $entityProvider
      */
     public function __construct(View $view, PermissionService $permissionService, EntityProvider $entityProvider)
@@ -26,7 +28,7 @@ class ViewService
 
     /**
      * Add a view to the given entity.
-     * @param Entity $entity
+     * @param \BookStack\Entities\Entity $entity
      * @return int
      */
     public function add(Entity $entity)
@@ -43,7 +45,7 @@ class ViewService
         }
 
         // Otherwise create new view count
-        $entity->views()->save($this->view->create([
+        $entity->views()->save($this->view->newInstance([
             'user_id' => $user->id,
             'views' => 1
         ]));
@@ -59,12 +61,12 @@ class ViewService
      * @param string $action - used for permission checking
      * @return Collection
      */
-    public function getPopular(int $count = 10, int $page = 0, $filterModels = null, string $action = 'view')
+    public function getPopular(int $count = 10, int $page = 0, array $filterModels = null, string $action = 'view')
     {
         $skipCount = $count * $page;
         $query = $this->permissionService
             ->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type', $action)
-            ->select('*', 'viewable_id', 'viewable_type', \DB::raw('SUM(views) as view_count'))
+            ->select('*', 'viewable_id', 'viewable_type', DB::raw('SUM(views) as view_count'))
             ->groupBy('viewable_id', 'viewable_type')
             ->orderBy('view_count', 'desc');
 
diff --git a/app/Application.php b/app/Application.php
new file mode 100644 (file)
index 0000000..499fdea
--- /dev/null
@@ -0,0 +1,23 @@
+<?php
+
+namespace BookStack;
+
+class Application extends \Illuminate\Foundation\Application
+{
+
+    /**
+     * Get the path to the application configuration files.
+     *
+     * @param  string  $path Optionally, a path to append to the config path
+     * @return string
+     */
+    public function configPath($path = '')
+    {
+        return $this->basePath
+            . DIRECTORY_SEPARATOR
+            . 'app'
+            . DIRECTORY_SEPARATOR
+            . 'Config'
+            . ($path ? DIRECTORY_SEPARATOR.$path : $path);
+    }
+}
index 4df014116c5c3d1c393e6e4594923c0d65a5d519..9aa3b9b98b56cf0d56453ae9e2de18c48c4d4524 100644 (file)
@@ -1,33 +1,18 @@
 <?php namespace BookStack\Auth\Access;
 
 use BookStack\Auth\User;
-use BookStack\Auth\UserRepo;
 use BookStack\Exceptions\ConfirmationEmailException;
-use BookStack\Exceptions\UserRegistrationException;
 use BookStack\Notifications\ConfirmEmail;
-use Carbon\Carbon;
-use Illuminate\Database\Connection as Database;
 
-class EmailConfirmationService
+class EmailConfirmationService extends UserTokenService
 {
-    protected $db;
-    protected $users;
-
-    /**
-     * EmailConfirmationService constructor.
-     * @param Database $db
-     * @param \BookStack\Auth\UserRepo $users
-     */
-    public function __construct(Database $db, UserRepo $users)
-    {
-        $this->db = $db;
-        $this->users = $users;
-    }
+    protected $tokenTable = 'email_confirmations';
+    protected $expiryTime = 24;
 
     /**
      * Create new confirmation for a user,
      * Also removes any existing old ones.
-     * @param \BookStack\Auth\User $user
+     * @param User $user
      * @throws ConfirmationEmailException
      */
     public function sendConfirmation(User $user)
@@ -36,76 +21,19 @@ class EmailConfirmationService
             throw new ConfirmationEmailException(trans('errors.email_already_confirmed'), '/login');
         }
 
-        $this->deleteConfirmationsByUser($user);
-        $token = $this->createEmailConfirmation($user);
+        $this->deleteByUser($user);
+        $token = $this->createTokenForUser($user);
 
         $user->notify(new ConfirmEmail($token));
     }
 
     /**
-     * Creates a new email confirmation in the database and returns the token.
-     * @param User $user
-     * @return string
-     */
-    public function createEmailConfirmation(User $user)
-    {
-        $token = $this->getToken();
-        $this->db->table('email_confirmations')->insert([
-            'user_id' => $user->id,
-            'token' => $token,
-            'created_at' => Carbon::now(),
-            'updated_at' => Carbon::now()
-        ]);
-        return $token;
-    }
-
-    /**
-     * Gets an email confirmation by looking up the token,
-     * Ensures the token has not expired.
-     * @param string $token
-     * @return array|null|\stdClass
-     * @throws UserRegistrationException
-     */
-    public function getEmailConfirmationFromToken($token)
-    {
-        $emailConfirmation = $this->db->table('email_confirmations')->where('token', '=', $token)->first();
-
-        // If not found show error
-        if ($emailConfirmation === null) {
-            throw new UserRegistrationException(trans('errors.email_confirmation_invalid'), '/register');
-        }
-
-        // If more than a day old
-        if (Carbon::now()->subDay()->gt(new Carbon($emailConfirmation->created_at))) {
-            $user = $this->users->getById($emailConfirmation->user_id);
-            $this->sendConfirmation($user);
-            throw new UserRegistrationException(trans('errors.email_confirmation_expired'), '/register/confirm');
-        }
-
-        $emailConfirmation->user = $this->users->getById($emailConfirmation->user_id);
-        return $emailConfirmation;
-    }
-
-    /**
-     * Delete all email confirmations that belong to a user.
-     * @param \BookStack\Auth\User $user
-     * @return mixed
+     * Check if confirmation is required in this instance.
+     * @return bool
      */
-    public function deleteConfirmationsByUser(User $user)
+    public function confirmationRequired() : bool
     {
-        return $this->db->table('email_confirmations')->where('user_id', '=', $user->id)->delete();
-    }
-
-    /**
-     * Creates a unique token within the email confirmation database.
-     * @return string
-     */
-    protected function getToken()
-    {
-        $token = str_random(24);
-        while ($this->db->table('email_confirmations')->where('token', '=', $token)->exists()) {
-            $token = str_random(25);
-        }
-        return $token;
+        return setting('registration-confirmation')
+            || setting('registration-restrict');
     }
 }
index 0d46b9f882d5ebe6afe1d08fb0d7b4714c9eeb74..9c8d1a81f43ebb8f30d66807a14349cab2882a39 100644 (file)
@@ -5,6 +5,7 @@ use BookStack\Auth\UserRepo;
 use BookStack\Exceptions\SocialDriverNotConfigured;
 use BookStack\Exceptions\SocialSignInAccountNotUsed;
 use BookStack\Exceptions\UserRegistrationException;
+use Illuminate\Support\Str;
 use Laravel\Socialite\Contracts\Factory as Socialite;
 use Laravel\Socialite\Contracts\User as SocialUser;
 
@@ -104,6 +105,7 @@ class SocialAuthService
         $socialAccount = $this->socialAccount->where('driver_id', '=', $socialId)->first();
         $isLoggedIn = auth()->check();
         $currentUser = user();
+        $titleCaseDriver = Str::title($socialDriver);
 
         // When a user is not logged in and a matching SocialAccount exists,
         // Simply log the user into the application.
@@ -117,26 +119,26 @@ class SocialAuthService
         if ($isLoggedIn && $socialAccount === null) {
             $this->fillSocialAccount($socialDriver, $socialUser);
             $currentUser->socialAccounts()->save($this->socialAccount);
-            session()->flash('success', trans('settings.users_social_connected', ['socialAccount' => title_case($socialDriver)]));
+            session()->flash('success', trans('settings.users_social_connected', ['socialAccount' => $titleCaseDriver]));
             return redirect($currentUser->getEditUrl());
         }
 
         // When a user is logged in and the social account exists and is already linked to the current user.
         if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id === $currentUser->id) {
-            session()->flash('error', trans('errors.social_account_existing', ['socialAccount' => title_case($socialDriver)]));
+            session()->flash('error', trans('errors.social_account_existing', ['socialAccount' => $titleCaseDriver]));
             return redirect($currentUser->getEditUrl());
         }
 
         // When a user is logged in, A social account exists but the users do not match.
         if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id != $currentUser->id) {
-            session()->flash('error', trans('errors.social_account_already_used_existing', ['socialAccount' => title_case($socialDriver)]));
+            session()->flash('error', trans('errors.social_account_already_used_existing', ['socialAccount' => $titleCaseDriver]));
             return redirect($currentUser->getEditUrl());
         }
 
         // Otherwise let the user know this social account is not used by anyone.
-        $message = trans('errors.social_account_not_used', ['socialAccount' => title_case($socialDriver)]);
+        $message = trans('errors.social_account_not_used', ['socialAccount' => $titleCaseDriver]);
         if (setting('registration-enabled')) {
-            $message .= trans('errors.social_account_register_instructions', ['socialAccount' => title_case($socialDriver)]);
+            $message .= trans('errors.social_account_register_instructions', ['socialAccount' => $titleCaseDriver]);
         }
         
         throw new SocialSignInAccountNotUsed($message, '/login');
@@ -157,7 +159,7 @@ class SocialAuthService
             abort(404, trans('errors.social_driver_not_found'));
         }
         if (!$this->checkDriverConfigured($driver)) {
-            throw new SocialDriverNotConfigured(trans('errors.social_driver_not_configured', ['socialAccount' => title_case($socialDriver)]));
+            throw new SocialDriverNotConfigured(trans('errors.social_driver_not_configured', ['socialAccount' => Str::title($socialDriver)]));
         }
 
         return $driver;
@@ -244,7 +246,7 @@ class SocialAuthService
     public function detachSocialAccount($socialDriver)
     {
         user()->socialAccounts()->where('driver', '=', $socialDriver)->delete();
-        session()->flash('success', trans('settings.users_social_disconnected', ['socialAccount' => title_case($socialDriver)]));
+        session()->flash('success', trans('settings.users_social_disconnected', ['socialAccount' => Str::title($socialDriver)]));
         return redirect(user()->getEditUrl());
     }
 
diff --git a/app/Auth/Access/UserInviteService.php b/app/Auth/Access/UserInviteService.php
new file mode 100644 (file)
index 0000000..20519fc
--- /dev/null
@@ -0,0 +1,22 @@
+<?php namespace BookStack\Auth\Access;
+
+use BookStack\Auth\User;
+use BookStack\Notifications\UserInvite;
+
+class UserInviteService extends UserTokenService
+{
+    protected $tokenTable = 'user_invites';
+    protected $expiryTime = 336; // Two weeks
+
+    /**
+     * Send an invitation to a user to sign into BookStack
+     * Removes existing invitation tokens.
+     * @param User $user
+     */
+    public function sendInvitation(User $user)
+    {
+        $this->deleteByUser($user);
+        $token = $this->createTokenForUser($user);
+        $user->notify(new UserInvite($token));
+    }
+}
diff --git a/app/Auth/Access/UserTokenService.php b/app/Auth/Access/UserTokenService.php
new file mode 100644 (file)
index 0000000..a1defbf
--- /dev/null
@@ -0,0 +1,134 @@
+<?php namespace BookStack\Auth\Access;
+
+use BookStack\Auth\User;
+use BookStack\Exceptions\UserTokenExpiredException;
+use BookStack\Exceptions\UserTokenNotFoundException;
+use Carbon\Carbon;
+use Illuminate\Database\Connection as Database;
+use Illuminate\Support\Str;
+use stdClass;
+
+class UserTokenService
+{
+
+    /**
+     * Name of table where user tokens are stored.
+     * @var string
+     */
+    protected $tokenTable = 'user_tokens';
+
+    /**
+     * Token expiry time in hours.
+     * @var int
+     */
+    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.
+     * @param User $user
+     * @return mixed
+     */
+    public function deleteByUser(User $user)
+    {
+        return $this->db->table($this->tokenTable)
+            ->where('user_id', '=', $user->id)
+            ->delete();
+    }
+
+    /**
+     * Get the user id from a token, while check the token exists and has not expired.
+     * @param string $token
+     * @return int
+     * @throws UserTokenNotFoundException
+     * @throws UserTokenExpiredException
+     */
+    public function checkTokenAndGetUserId(string $token) : int
+    {
+        $entry = $this->getEntryByToken($token);
+
+        if (is_null($entry)) {
+            throw new UserTokenNotFoundException('Token "' . $token . '" not found');
+        }
+
+        if ($this->entryExpired($entry)) {
+            throw new UserTokenExpiredException("Token of id {$entry->id} has expired.", $entry->user_id);
+        }
+
+        return $entry->user_id;
+    }
+
+    /**
+     * Creates a unique token within the email confirmation database.
+     * @return string
+     */
+    protected function generateToken() : string
+    {
+        $token = Str::random(24);
+        while ($this->tokenExists($token)) {
+            $token = Str::random(25);
+        }
+        return $token;
+    }
+
+    /**
+     * Generate and store a token for the given user.
+     * @param User $user
+     * @return string
+     */
+    protected function createTokenForUser(User $user) : string
+    {
+        $token = $this->generateToken();
+        $this->db->table($this->tokenTable)->insert([
+            'user_id' => $user->id,
+            'token' => $token,
+            'created_at' => Carbon::now(),
+            'updated_at' => Carbon::now()
+        ]);
+        return $token;
+    }
+
+    /**
+     * Check if the given token exists.
+     * @param string $token
+     * @return bool
+     */
+    protected function tokenExists(string $token) : bool
+    {
+        return $this->db->table($this->tokenTable)
+            ->where('token', '=', $token)->exists();
+    }
+
+    /**
+     * Get a token entry for the given token.
+     * @param string $token
+     * @return object|null
+     */
+    protected function getEntryByToken(string $token)
+    {
+        return $this->db->table($this->tokenTable)
+            ->where('token', '=', $token)
+            ->first();
+    }
+
+    /**
+     * Check if the given token entry has expired.
+     * @param stdClass $tokenEntry
+     * @return bool
+     */
+    protected function entryExpired(stdClass $tokenEntry) : bool
+    {
+        return Carbon::now()->subHours($this->expiryTime)
+            ->gt(new Carbon($tokenEntry->created_at));
+    }
+}
index a5ab4ea9a8e51109c7225bc82ccca1799470205d..97cc1ca241e84209f235136550378f3cb8f43f81 100644 (file)
@@ -215,7 +215,6 @@ class PermissionService
      * @param Collection $books
      * @param array $roles
      * @param bool $deleteOld
-     * @throws \Throwable
      */
     protected function buildJointPermissionsForBooks($books, $roles, $deleteOld = false)
     {
@@ -634,42 +633,40 @@ class PermissionService
     }
 
     /**
-     * Get the children of a book in an efficient single query, Filtered by the permission system.
-     * @param integer $book_id
-     * @param bool $filterDrafts
-     * @param bool $fetchPageContent
-     * @return QueryBuilder
+     * Limited the given entity query so that the query will only
+     * return items that the user has permission for the given ability.
      */
-    public function bookChildrenQuery($book_id, $filterDrafts = false, $fetchPageContent = false)
+    public function restrictEntityQuery(Builder $query, string $ability = 'view'): Builder
     {
-        $entities = $this->entityProvider;
-        $pageSelect = $this->db->table('pages')->selectRaw($entities->page->entityRawQuery($fetchPageContent))
-            ->where('book_id', '=', $book_id)->where(function ($query) use ($filterDrafts) {
-                $query->where('draft', '=', 0);
-                if (!$filterDrafts) {
-                    $query->orWhere(function ($query) {
-                        $query->where('draft', '=', 1)->where('created_by', '=', $this->currentUser()->id);
+        $this->clean();
+        return $query->where(function (Builder $parentQuery) use ($ability) {
+            $parentQuery->whereHas('jointPermissions', function (Builder $permissionQuery) use ($ability) {
+                $permissionQuery->whereIn('role_id', $this->getRoles())
+                    ->where('action', '=', $ability)
+                    ->where(function (Builder $query) {
+                        $query->where('has_permission', '=', true)
+                            ->orWhere(function (Builder $query) {
+                                $query->where('has_permission_own', '=', true)
+                                    ->where('created_by', '=', $this->currentUser()->id);
+                            });
                     });
-                }
-            });
-        $chapterSelect = $this->db->table('chapters')->selectRaw($entities->chapter->entityRawQuery())->where('book_id', '=', $book_id);
-        $query = $this->db->query()->select('*')->from($this->db->raw("({$pageSelect->toSql()} UNION {$chapterSelect->toSql()}) AS U"))
-            ->mergeBindings($pageSelect)->mergeBindings($chapterSelect);
-
-        // Add joint permission filter
-        $whereQuery = $this->db->table('joint_permissions as jp')->selectRaw('COUNT(*)')
-            ->whereRaw('jp.entity_id=U.id')->whereRaw('jp.entity_type=U.entity_type')
-            ->where('jp.action', '=', 'view')->whereIn('jp.role_id', $this->getRoles())
-            ->where(function ($query) {
-                $query->where('jp.has_permission', '=', 1)->orWhere(function ($query) {
-                    $query->where('jp.has_permission_own', '=', 1)->where('jp.created_by', '=', $this->currentUser()->id);
-                });
             });
-        $query->whereRaw("({$whereQuery->toSql()}) > 0")->mergeBindings($whereQuery);
+        });
+    }
 
-        $query->orderBy('draft', 'desc')->orderBy('priority', 'asc');
-        $this->clean();
-        return  $query;
+    /**
+     * Extend the given page query to ensure draft items are not visible
+     * unless created by the given user.
+     */
+    public function enforceDraftVisiblityOnQuery(Builder $query): Builder
+    {
+        return $query->where(function (Builder $query) {
+            $query->where('draft', '=', false)
+                ->orWhere(function (Builder $query) {
+                    $query->where('draft', '=', true)
+                        ->where('created_by', '=', $this->currentUser()->id);
+                });
+        });
     }
 
     /**
@@ -684,12 +681,11 @@ class PermissionService
         if (strtolower($entityType) === 'page') {
             // Prevent drafts being visible to others.
             $query = $query->where(function ($query) {
-                $query->where('draft', '=', false);
-                if ($this->currentUser()) {
-                    $query->orWhere(function ($query) {
-                        $query->where('draft', '=', true)->where('created_by', '=', $this->currentUser()->id);
+                $query->where('draft', '=', false)
+                    ->orWhere(function ($query) {
+                        $query->where('draft', '=', true)
+                            ->where('created_by', '=', $this->currentUser()->id);
                     });
-                }
             });
         }
 
index 18d5089bec2f89dcaea0d9258a448d9463041b1b..56ef193015f7103a98bb440ec60498e5bf765c25 100644 (file)
@@ -3,6 +3,7 @@
 use BookStack\Auth\Permissions;
 use BookStack\Auth\Role;
 use BookStack\Exceptions\PermissionsException;
+use Illuminate\Support\Str;
 
 class PermissionsRepo
 {
@@ -66,7 +67,7 @@ class PermissionsRepo
         $role->name = str_replace(' ', '-', strtolower($roleData['display_name']));
         // Prevent duplicate names
         while ($this->role->where('name', '=', $role->name)->count() > 0) {
-            $role->name .= strtolower(str_random(2));
+            $role->name .= strtolower(Str::random(2));
         }
         $role->save();
 
@@ -136,7 +137,7 @@ class PermissionsRepo
         // Prevent deleting admin role or default registration role.
         if ($role->system_name && in_array($role->system_name, $this->systemRoles)) {
             throw new PermissionsException(trans('errors.role_system_cannot_be_deleted'));
-        } else if ($role->id == setting('registration-role')) {
+        } else if ($role->id === intval(setting('registration-role'))) {
             throw new PermissionsException(trans('errors.role_registration_default_cannot_delete'));
         }
 
index 917d8aa26192c0388fac58fa5970267f3e25969c..712f5299b7b1d2ff5485179621aeddf9a1c5efbd 100644 (file)
@@ -75,7 +75,7 @@ class Role extends Model
      */
     public static function getRole($roleName)
     {
-        return static::where('name', '=', $roleName)->first();
+        return static::query()->where('name', '=', $roleName)->first();
     }
 
     /**
@@ -85,7 +85,7 @@ class Role extends Model
      */
     public static function getSystemRole($roleName)
     {
-        return static::where('system_name', '=', $roleName)->first();
+        return static::query()->where('system_name', '=', $roleName)->first();
     }
 
     /**
@@ -94,6 +94,15 @@ class Role extends Model
      */
     public static function visible()
     {
-        return static::where('hidden', '=', false)->orderBy('name')->get();
+        return static::query()->where('hidden', '=', false)->orderBy('name')->get();
+    }
+
+    /**
+     * Get the roles that can be restricted.
+     * @return \Illuminate\Database\Eloquent\Builder[]|\Illuminate\Database\Eloquent\Collection
+     */
+    public static function restrictable()
+    {
+        return static::query()->where('system_name', '!=', 'admin')->get();
     }
 }
index 12f022b0668491653479e9151ea10373f2fa32eb..bce418a7421fb40ddeb34b0e0dacd192d7048e2a 100644 (file)
@@ -3,6 +3,7 @@
 use BookStack\Model;
 use BookStack\Notifications\ResetPassword;
 use BookStack\Uploads\Image;
+use Carbon\Carbon;
 use Illuminate\Auth\Authenticatable;
 use Illuminate\Auth\Passwords\CanResetPassword;
 use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
@@ -10,6 +11,20 @@ use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
 use Illuminate\Database\Eloquent\Relations\BelongsToMany;
 use Illuminate\Notifications\Notifiable;
 
+/**
+ * Class User
+ * @package BookStack\Auth
+ * @property string $id
+ * @property string $name
+ * @property string $email
+ * @property string $password
+ * @property Carbon $created_at
+ * @property Carbon $updated_at
+ * @property bool $email_confirmed
+ * @property int $image_id
+ * @property string $external_auth_id
+ * @property string $system_name
+ */
 class User extends Model implements AuthenticatableContract, CanResetPasswordContract
 {
     use Authenticatable, CanResetPassword, Notifiable;
@@ -38,13 +53,24 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
      */
     protected $permissions;
 
+    /**
+     * This holds the default user when loaded.
+     * @var null|User
+     */
+    protected static $defaultUser = null;
+
     /**
      * Returns the default public user.
      * @return User
      */
     public static function getDefault()
     {
-        return static::where('system_name', '=', 'public')->first();
+        if (!is_null(static::$defaultUser)) {
+            return static::$defaultUser;
+        }
+        
+        static::$defaultUser = static::where('system_name', '=', 'public')->first();
+        return static::$defaultUser;
     }
 
     /**
@@ -168,14 +194,14 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
      */
     public function getAvatar($size = 50)
     {
-        $default = baseUrl('/user_avatar.png');
+        $default = url('/user_avatar.png');
         $imageId = $this->image_id;
         if ($imageId === 0 || $imageId === '0' || $imageId === null) {
             return $default;
         }
 
         try {
-            $avatar = $this->avatar ? baseUrl($this->avatar->getThumb($size, $size, false)) : $default;
+            $avatar = $this->avatar ? url($this->avatar->getThumb($size, $size, false)) : $default;
         } catch (\Exception $err) {
             $avatar = $default;
         }
@@ -197,7 +223,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
      */
     public function getEditUrl()
     {
-        return baseUrl('/settings/users/' . $this->id);
+        return url('/settings/users/' . $this->id);
     }
 
     /**
@@ -206,7 +232,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
      */
     public function getProfileUrl()
     {
-        return baseUrl('/user/' . $this->id);
+        return url('/user/' . $this->id);
     }
 
     /**
@@ -216,12 +242,12 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
      */
     public function getShortName($chars = 8)
     {
-        if (strlen($this->name) <= $chars) {
+        if (mb_strlen($this->name) <= $chars) {
             return $this->name;
         }
 
         $splitName = explode(' ', $this->name);
-        if (strlen($splitName[0]) <= $chars) {
+        if (mb_strlen($splitName[0]) <= $chars) {
             return $splitName[0];
         }
 
index dec973f6c7acf67721e4dd31b20516c798714f48..a903e2c38324e27c1d646655d9758d8e1febb471 100644 (file)
@@ -1,32 +1,31 @@
 <?php namespace BookStack\Auth;
 
 use Activity;
-use BookStack\Entities\Repos\EntityRepo;
+use BookStack\Entities\Book;
+use BookStack\Entities\Bookshelf;
+use BookStack\Entities\Chapter;
+use BookStack\Entities\Page;
 use BookStack\Exceptions\NotFoundException;
 use BookStack\Exceptions\UserUpdateException;
 use BookStack\Uploads\Image;
 use Exception;
 use Illuminate\Database\Eloquent\Builder;
 use Images;
+use Log;
 
 class UserRepo
 {
 
     protected $user;
     protected $role;
-    protected $entityRepo;
 
     /**
      * UserRepo constructor.
-     * @param User $user
-     * @param Role $role
-     * @param EntityRepo $entityRepo
      */
-    public function __construct(User $user, Role $role, EntityRepo $entityRepo)
+    public function __construct(User $user, Role $role)
     {
         $this->user = $user;
         $this->role = $role;
-        $this->entityRepo = $entityRepo;
     }
 
     /**
@@ -81,7 +80,7 @@ class UserRepo
      * Creates a new user and attaches a role to them.
      * @param array $data
      * @param boolean $verifyEmail
-     * @return \BookStack\Auth\User
+     * @return User
      */
     public function registerNew(array $data, $verifyEmail = false)
     {
@@ -121,7 +120,7 @@ class UserRepo
 
     /**
      * Checks if the give user is the only admin.
-     * @param \BookStack\Auth\User $user
+     * @param User $user
      * @return bool
      */
     public function isOnlyAdmin(User $user)
@@ -175,7 +174,7 @@ class UserRepo
      * Create a new basic instance of user.
      * @param array $data
      * @param boolean $verifyEmail
-     * @return \BookStack\Auth\User
+     * @return User
      */
     public function create(array $data, $verifyEmail = false)
     {
@@ -189,7 +188,7 @@ class UserRepo
 
     /**
      * Remove the given user from storage, Delete all related content.
-     * @param \BookStack\Auth\User $user
+     * @param User $user
      * @throws Exception
      */
     public function destroy(User $user)
@@ -206,7 +205,7 @@ class UserRepo
 
     /**
      * Get the latest activity for a user.
-     * @param \BookStack\Auth\User $user
+     * @param User $user
      * @param int $count
      * @param int $page
      * @return array
@@ -218,36 +217,35 @@ class UserRepo
 
     /**
      * Get the recently created content for this given user.
-     * @param \BookStack\Auth\User $user
-     * @param int $count
-     * @return mixed
      */
-    public function getRecentlyCreated(User $user, $count = 20)
+    public function getRecentlyCreated(User $user, int $count = 20): array
     {
-        $createdByUserQuery = function (Builder $query) use ($user) {
-            $query->where('created_by', '=', $user->id);
+        $query = function (Builder $query) use ($user, $count) {
+            return $query->orderBy('created_at', 'desc')
+                ->where('created_by', '=', $user->id)
+                ->take($count)
+                ->get();
         };
 
         return [
-            'pages'    => $this->entityRepo->getRecentlyCreated('page', $count, 0, $createdByUserQuery),
-            'chapters' => $this->entityRepo->getRecentlyCreated('chapter', $count, 0, $createdByUserQuery),
-            'books'    => $this->entityRepo->getRecentlyCreated('book', $count, 0, $createdByUserQuery),
-            'shelves'  => $this->entityRepo->getRecentlyCreated('bookshelf', $count, 0, $createdByUserQuery)
+            'pages'    => $query(Page::visible()->where('draft', '=', false)),
+            'chapters' => $query(Chapter::visible()),
+            'books'    => $query(Book::visible()),
+            'shelves'  => $query(Bookshelf::visible()),
         ];
     }
 
     /**
      * Get asset created counts for the give user.
-     * @param \BookStack\Auth\User $user
-     * @return array
      */
-    public function getAssetCounts(User $user)
+    public function getAssetCounts(User $user): array
     {
+        $createdBy = ['created_by' => $user->id];
         return [
-            'pages'    => $this->entityRepo->getUserTotalCreated('page', $user),
-            'chapters' => $this->entityRepo->getUserTotalCreated('chapter', $user),
-            'books'    => $this->entityRepo->getUserTotalCreated('book', $user),
-            'shelves'    => $this->entityRepo->getUserTotalCreated('bookshelf', $user),
+            'pages'    =>  Page::visible()->where($createdBy)->count(),
+            'chapters'    =>  Chapter::visible()->where($createdBy)->count(),
+            'books'    =>  Book::visible()->where($createdBy)->count(),
+            'shelves'    =>  Bookshelf::visible()->where($createdBy)->count(),
         ];
     }
 
@@ -260,16 +258,6 @@ class UserRepo
         return $this->role->newQuery()->orderBy('name', 'asc')->get();
     }
 
-    /**
-     * Get all the roles which can be given restricted access to
-     * other entities in the system.
-     * @return mixed
-     */
-    public function getRestrictableRoles()
-    {
-        return $this->role->where('system_name', '!=', 'admin')->get();
-    }
-
     /**
      * Get an avatar image for a user and set it as their avatar.
      * Returns early if avatars disabled or not set in config.
@@ -288,7 +276,7 @@ class UserRepo
             $user->save();
             return true;
         } catch (Exception $e) {
-            \Log::error('Failed to save user avatar image');
+            Log::error('Failed to save user avatar image');
             return false;
         }
     }
similarity index 96%
rename from config/app.php
rename to app/Config/app.php
index aaeafb98df291e6925d6ade1d283d1fcb9804379..b5bf891b2b672dd6547d4b15b00585e465ee068f 100755 (executable)
@@ -52,11 +52,14 @@ return [
     'locale' => env('APP_LANG', 'en'),
 
     // Locales available
-    'locales' => ['en', 'ar', 'de', 'de_informal', 'es', 'es_AR', 'fr', 'nl', 'pt_BR', 'sk', 'cs', 'sv', 'kr', 'ja', 'pl', 'it', 'ru', 'uk', 'zh_CN', 'zh_TW'],
+    'locales' => ['en', 'ar', 'de', 'de_informal', 'es', 'es_AR', 'fr', 'hu', 'nl', 'pt_BR', 'sk', 'cs', 'sv', 'kr', 'ja', 'pl', 'it', 'ru', 'uk', 'zh_CN', 'zh_TW', 'tr'],
 
     //  Application Fallback Locale
     'fallback_locale' => 'en',
 
+    // Faker Locale
+    'faker_locale' => 'en_GB',
+
     // Enable right-to-left text control.
     'rtl' => false,
 
@@ -72,10 +75,6 @@ return [
     // Encryption cipher
     'cipher' => 'AES-256-CBC',
 
-    // Logging configuration
-    // Options: single, daily, syslog, errorlog
-    'log' => env('APP_LOGGING', 'single'),
-
     // Application Services Provides
     'providers' => [
 
@@ -137,6 +136,7 @@ return [
 
         // Laravel
         'App'       => Illuminate\Support\Facades\App::class,
+        'Arr'       => Illuminate\Support\Arr::class,
         'Artisan'   => Illuminate\Support\Facades\Artisan::class,
         'Auth'      => Illuminate\Support\Facades\Auth::class,
         'Blade'     => Illuminate\Support\Facades\Blade::class,
@@ -166,6 +166,7 @@ return [
         'Schema'    => Illuminate\Support\Facades\Schema::class,
         'Session'   => Illuminate\Support\Facades\Session::class,
         'Storage'   => Illuminate\Support\Facades\Storage::class,
+        'Str'       => Illuminate\Support\Str::class,
         'URL'       => Illuminate\Support\Facades\URL::class,
         'Validator' => Illuminate\Support\Facades\Validator::class,
         'View'      => Illuminate\Support\Facades\View::class,
@@ -181,6 +182,7 @@ return [
         'Setting'  => BookStack\Facades\Setting::class,
         'Views'    => BookStack\Facades\Views::class,
         'Images'   => BookStack\Facades\Images::class,
+        'Permissions' => BookStack\Facades\Permissions::class,
 
     ],
 
similarity index 98%
rename from config/auth.php
rename to app/Config/auth.php
index 7bf1ae7722c958cfd8940c4d30d43af78a76dbf6..5535a6f9ce88b1c5ea6fd972d212f00337dfdb24 100644 (file)
@@ -36,6 +36,7 @@ return [
         'api' => [
             'driver' => 'token',
             'provider' => 'users',
+            'hash' => false,
         ],
     ],
 
@@ -69,4 +70,4 @@ return [
         ],
     ],
 
-];
\ No newline at end of file
+];
similarity index 80%
rename from config/broadcasting.php
rename to app/Config/broadcasting.php
index 3d9eb78f95abd18bdb79d785a693de3f174641a6..7aaaa5693fe1cf907e07da54880710f802c9694a 100644 (file)
@@ -24,9 +24,13 @@ return [
 
         'pusher' => [
             'driver' => 'pusher',
-            'key' => env('PUSHER_KEY'),
-            'secret' => env('PUSHER_SECRET'),
+            'key' => env('PUSHER_APP_KEY'),
+            'secret' => env('PUSHER_APP_SECRET'),
             'app_id' => env('PUSHER_APP_ID'),
+            'options' => [
+                'cluster' => env('PUSHER_APP_CLUSTER'),
+                'useTLS' => true,
+            ],
         ],
 
         'redis' => [
@@ -38,6 +42,11 @@ return [
             'driver' => 'log',
         ],
 
+        'null' => [
+            'driver' => 'null',
+        ],
+
+
     ],
 
 ];
similarity index 86%
rename from config/cache.php
rename to app/Config/cache.php
index 43f420457eba43f7b757e48bf0960b6e5a55a2b8..33d3a1a0bb7b02e6471f1088492fa7b5078e0519 100644 (file)
@@ -14,8 +14,12 @@ if (env('CACHE_DRIVER') === 'memcached') {
     $memcachedServers = explode(',', trim(env('MEMCACHED_SERVERS', '127.0.0.1:11211:100'), ','));
     foreach ($memcachedServers as $index => $memcachedServer) {
         $memcachedServerDetails = explode(':', $memcachedServer);
-        if (count($memcachedServerDetails) < 2) $memcachedServerDetails[] = '11211';
-        if (count($memcachedServerDetails) < 3) $memcachedServerDetails[] = '100';
+        if (count($memcachedServerDetails) < 2) {
+            $memcachedServerDetails[] = '11211';
+        }
+        if (count($memcachedServerDetails) < 3) {
+            $memcachedServerDetails[] = '100';
+        }
         $memcachedServers[$index] = array_combine($memcachedServerKeys, $memcachedServerDetails);
     }
 }
@@ -62,6 +66,6 @@ return [
 
     // Cache key prefix
     // Used to prevent collisions in shared cache systems.
-    'prefix' => env('CACHE_PREFIX', 'bookstack'),
+    'prefix' => env('CACHE_PREFIX', 'bookstack_cache'),
 
 ];
similarity index 75%
rename from config/database.php
rename to app/Config/database.php
index 93a44854f092a8d166b9ce994a41da9dacb67a99..ed654ffb9172b4789a62c922d971adb8f550976d 100644 (file)
 // REDIS
 // Split out configuration into an array
 if (env('REDIS_SERVERS', false)) {
-
     $redisDefaults = ['host' => '127.0.0.1', 'port' => '6379', 'database' => '0', 'password' => null];
     $redisServers = explode(',', trim(env('REDIS_SERVERS', '127.0.0.1:6379:0'), ','));
-    $redisConfig = [];
+    $redisConfig = ['client' => 'predis'];
     $cluster = count($redisServers) > 1;
 
     if ($cluster) {
@@ -59,14 +58,9 @@ return [
     // Many of those shown here are unsupported by BookStack.
     'connections' => [
 
-        'sqlite' => [
-            'driver'   => 'sqlite',
-            'database' => storage_path('database.sqlite'),
-            'prefix'   => '',
-        ],
-
         'mysql' => [
             'driver'    => 'mysql',
+            'url' => env('DATABASE_URL'),
             'host'      => $mysql_host,
             'database'  => env('DB_DATABASE', 'forge'),
             'username'  => env('DB_USERNAME', 'forge'),
@@ -76,43 +70,28 @@ return [
             'charset'   => 'utf8mb4',
             'collation' => 'utf8mb4_unicode_ci',
             'prefix'    => '',
+            'prefix_indexes' => true,
             'strict'    => false,
             'engine' => null,
+            'options' => extension_loaded('pdo_mysql') ? array_filter([
+                PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
+            ]) : [],
         ],
 
         'mysql_testing' => [
             'driver'    => 'mysql',
+            'url' => env('TEST_DATABASE_URL'),
             'host'      => '127.0.0.1',
             'database'  => 'bookstack-test',
             'username'  => env('MYSQL_USER', 'bookstack-test'),
             'password'  => env('MYSQL_PASSWORD', 'bookstack-test'),
-            'charset'   => 'utf8',
-            'collation' => 'utf8_unicode_ci',
+            'charset'   => 'utf8mb4',
+            'collation' => 'utf8mb4_unicode_ci',
             'prefix'    => '',
+            'prefix_indexes' => true,
             'strict'    => false,
         ],
 
-        'pgsql' => [
-            'driver'   => 'pgsql',
-            'host'     => env('DB_HOST', 'localhost'),
-            'database' => env('DB_DATABASE', 'forge'),
-            'username' => env('DB_USERNAME', 'forge'),
-            'password' => env('DB_PASSWORD', ''),
-            'charset'  => 'utf8',
-            'prefix'   => '',
-            'schema'   => 'public',
-        ],
-
-        'sqlsrv' => [
-            'driver'   => 'sqlsrv',
-            'host'     => env('DB_HOST', 'localhost'),
-            'database' => env('DB_DATABASE', 'forge'),
-            'username' => env('DB_USERNAME', 'forge'),
-            'password' => env('DB_PASSWORD', ''),
-            'charset'  => 'utf8',
-            'prefix'   => '',
-        ],
-
     ],
 
     // Migration Repository Table
diff --git a/app/Config/debugbar.php b/app/Config/debugbar.php
new file mode 100644 (file)
index 0000000..fe624eb
--- /dev/null
@@ -0,0 +1,133 @@
+<?php
+
+/**
+ * Debugbar Configuration Options
+ *
+ * Changes to these config files are not supported by BookStack and may break upon updates.
+ * Configuration should be altered via the `.env` file or environment variables.
+ * Do not edit this file unless you're happy to maintain any changes yourself.
+ */
+
+return [
+
+     // Debugbar is enabled by default, when debug is set to true in app.php.
+     // You can override the value by setting enable to true or false instead of null.
+     //
+     // You can provide an array of URI's that must be ignored (eg. 'api/*')
+    'enabled' => env('DEBUGBAR_ENABLED', false),
+    'except' => [
+        'telescope*'
+    ],
+
+
+     // DebugBar stores data for session/ajax requests.
+     // You can disable this, so the debugbar stores data in headers/session,
+     // but this can cause problems with large data collectors.
+     // By default, file storage (in the storage folder) is used. Redis and PDO
+     // can also be used. For PDO, run the package migrations first.
+    'storage' => [
+        'enabled'    => true,
+        'driver'     => 'file', // redis, file, pdo, custom
+        'path'       => storage_path('debugbar'), // For file driver
+        'connection' => null,   // Leave null for default connection (Redis/PDO)
+        'provider'   => '' // Instance of StorageInterface for custom driver
+    ],
+
+     // Vendor files are included by default, but can be set to false.
+     // This can also be set to 'js' or 'css', to only include javascript or css vendor files.
+     // Vendor files are for css: font-awesome (including fonts) and highlight.js (css files)
+     // and for js: jquery and and highlight.js
+     // So if you want syntax highlighting, set it to true.
+     // jQuery is set to not conflict with existing jQuery scripts.
+    'include_vendors' => true,
+
+     // The Debugbar can capture Ajax requests and display them. If you don't want this (ie. because of errors),
+     // you can use this option to disable sending the data through the headers.
+     // Optionally, you can also send ServerTiming headers on ajax requests for the Chrome DevTools.
+
+    'capture_ajax' => true,
+    'add_ajax_timing' => false,
+
+     // When enabled, the Debugbar shows deprecated warnings for Symfony components
+     // in the Messages tab.
+    'error_handler' => false,
+
+     // The Debugbar can emulate the Clockwork headers, so you can use the Chrome
+     // Extension, without the server-side code. It uses Debugbar collectors instead.
+    'clockwork' => false,
+
+     // Enable/disable DataCollectors
+    'collectors' => [
+        'phpinfo'         => true,  // Php version
+        'messages'        => true,  // Messages
+        'time'            => true,  // Time Datalogger
+        'memory'          => true,  // Memory usage
+        'exceptions'      => true,  // Exception displayer
+        'log'             => true,  // Logs from Monolog (merged in messages if enabled)
+        'db'              => true,  // Show database (PDO) queries and bindings
+        'views'           => true,  // Views with their data
+        'route'           => true,  // Current route information
+        'auth'            => true, // Display Laravel authentication status
+        'gate'            => true, // Display Laravel Gate checks
+        'session'         => true,  // Display session data
+        'symfony_request' => true,  // Only one can be enabled..
+        'mail'            => true,  // Catch mail messages
+        'laravel'         => false, // Laravel version and environment
+        'events'          => false, // All events fired
+        'default_request' => false, // Regular or special Symfony request logger
+        'logs'            => false, // Add the latest log messages
+        'files'           => false, // Show the included files
+        'config'          => false, // Display config settings
+        'cache'           => false, // Display cache events
+        'models'          => true, // Display models
+    ],
+
+     // Configure some DataCollectors
+    'options' => [
+        'auth' => [
+            'show_name' => true,   // Also show the users name/email in the debugbar
+        ],
+        'db' => [
+            'with_params'       => true,   // Render SQL with the parameters substituted
+            'backtrace'         => true,   // Use a backtrace to find the origin of the query in your files.
+            'timeline'          => false,  // Add the queries to the timeline
+            'explain' => [                 // Show EXPLAIN output on queries
+                'enabled' => false,
+                'types' => ['SELECT'],     // ['SELECT', 'INSERT', 'UPDATE', 'DELETE']; for MySQL 5.6.3+
+            ],
+            'hints'             => true,    // Show hints for common mistakes
+        ],
+        'mail' => [
+            'full_log' => false
+        ],
+        'views' => [
+            'data' => false,    //Note: Can slow down the application, because the data can be quite large..
+        ],
+        'route' => [
+            'label' => true  // show complete route on bar
+        ],
+        'logs' => [
+            'file' => null
+        ],
+        'cache' => [
+            'values' => true // collect cache values
+        ],
+    ],
+
+     // Inject Debugbar into the response
+     // Usually, the debugbar is added just before </body>, by listening to the
+     // Response after the App is done. If you disable this, you have to add them
+     // in your template yourself. See http://phpdebugbar.com/docs/rendering.html
+    'inject' => true,
+
+     // DebugBar route prefix
+     // Sometimes you want to set route prefix to be used by DebugBar to load
+     // its resources from. Usually the need comes from misconfigured web server or
+     // from trying to overcome bugs like this: http://trac.nginx.org/nginx/ticket/97
+    'route_prefix' => '_debugbar',
+
+     // DebugBar route domain
+     // By default DebugBar route served from the same domain that request served.
+     // To override default domain, specify it as a non-empty value.
+    'route_domain' => env('APP_URL', '') === 'http://bookstack.dev' ? '' : env('APP_URL', ''),
+];
similarity index 98%
rename from config/dompdf.php
rename to app/Config/dompdf.php
index 77f0cff9c73738cd40eb61c54fe56b3053a5c76a..87be53df52b373d7c701a590f3051c7066fff711 100644 (file)
@@ -69,7 +69,7 @@ return [
          * should be an absolute path.
          * This is only checked on command line call by dompdf.php, but not by
          * direct class use like:
-         * $dompdf = new DOMPDF();     $dompdf->load_html($htmldata); $dompdf->render(); $pdfdata = $dompdf->output();
+         * $dompdf = new DOMPDF();  $dompdf->load_html($htmldata); $dompdf->render(); $pdfdata = $dompdf->output();
          */
         "DOMPDF_CHROOT" => realpath(base_path()),
 
similarity index 88%
rename from config/filesystems.php
rename to app/Config/filesystems.php
index 13198a5052e83b02ad0cf3f939b89229f48a93a0..bd7d28300abae17112857ead07d3d000c4fd823b 100644 (file)
@@ -14,6 +14,12 @@ return [
     // Options: local, local_secure, s3
     'default' => env('STORAGE_TYPE', 'local'),
 
+    // Filesystem to use specifically for image uploads.
+    'images' => env('STORAGE_IMAGE_TYPE', env('STORAGE_TYPE', 'local')),
+
+    // Filesystem to use specifically for file attachments.
+    'attachments' => env('STORAGE_ATTACHMENT_TYPE', env('STORAGE_TYPE', 'local')),
+
     // Storage URL
     // This is the url to where the storage is located for when using an external
     // file storage service, such as s3, to store publicly accessible assets.
diff --git a/app/Config/hashing.php b/app/Config/hashing.php
new file mode 100644 (file)
index 0000000..756718c
--- /dev/null
@@ -0,0 +1,37 @@
+<?php
+
+/**
+ * Hashing configuration options.
+ *
+ * Changes to these config files are not supported by BookStack and may break upon updates.
+ * Configuration should be altered via the `.env` file or environment variables.
+ * Do not edit this file unless you're happy to maintain any changes yourself.
+ */
+
+return [
+
+    // Default Hash Driver
+    // This option controls the default hash driver that will be used to hash
+    // passwords for your application. By default, the bcrypt algorithm is used.
+    // Supported: "bcrypt", "argon", "argon2id"
+    'driver' => 'bcrypt',
+
+    // Bcrypt Options
+    // Here you may specify the configuration options that should be used when
+    // passwords are hashed using the Bcrypt algorithm. This will allow you
+    // to control the amount of time it takes to hash the given password.
+    'bcrypt' => [
+        'rounds' => env('BCRYPT_ROUNDS', 10),
+    ],
+
+    // Argon Options
+    // Here you may specify the configuration options that should be used when
+    // passwords are hashed using the Argon algorithm. These will allow you
+    // to control the amount of time it takes to hash the given password.
+    'argon' => [
+        'memory' => 1024,
+        'threads' => 2,
+        'time' => 2,
+    ],
+
+];
diff --git a/app/Config/logging.php b/app/Config/logging.php
new file mode 100644 (file)
index 0000000..0b55dc2
--- /dev/null
@@ -0,0 +1,82 @@
+<?php
+
+use Monolog\Handler\NullHandler;
+use Monolog\Handler\StreamHandler;
+
+/**
+ * Logging configuration options.
+ *
+ * Changes to these config files are not supported by BookStack and may break upon updates.
+ * Configuration should be altered via the `.env` file or environment variables.
+ * Do not edit this file unless you're happy to maintain any changes yourself.
+ */
+
+return [
+
+    // Default Log Channel
+    // This option defines the default log channel that gets used when writing
+    // messages to the logs. The name specified in this option should match
+    // one of the channels defined in the "channels" configuration array.
+    'default' => env('LOG_CHANNEL', 'single'),
+
+    // Log Channels
+    // Here you may configure the log channels for your application. Out of
+    // the box, Laravel uses the Monolog PHP logging library. This gives
+    // you a variety of powerful log handlers / formatters to utilize.
+    // Available Drivers: "single", "daily", "slack", "syslog",
+    //                    "errorlog", "monolog",
+    //                    "custom", "stack"
+    'channels' => [
+        'stack' => [
+            'driver' => 'stack',
+            'channels' => ['daily'],
+            'ignore_exceptions' => false,
+        ],
+
+        'single' => [
+            'driver' => 'single',
+            'path' => storage_path('logs/laravel.log'),
+            'level' => 'debug',
+            'days' => 14,
+        ],
+
+        'daily' => [
+            'driver' => 'daily',
+            'path' => storage_path('logs/laravel.log'),
+            'level' => 'debug',
+            'days' => 7,
+        ],
+
+        'slack' => [
+            'driver' => 'slack',
+            'url' => env('LOG_SLACK_WEBHOOK_URL'),
+            'username' => 'Laravel Log',
+            'emoji' => ':boom:',
+            'level' => 'critical',
+        ],
+
+        'stderr' => [
+            'driver' => 'monolog',
+            'handler' => StreamHandler::class,
+            'with' => [
+                'stream' => 'php://stderr',
+            ],
+        ],
+
+        'syslog' => [
+            'driver' => 'syslog',
+            'level' => 'debug',
+        ],
+
+        'errorlog' => [
+            'driver' => 'errorlog',
+            'level' => 'debug',
+        ],
+
+        'null' => [
+            'driver' => 'monolog',
+            'handler' => NullHandler::class,
+        ],
+    ],
+
+];
similarity index 77%
rename from config/mail.php
rename to app/Config/mail.php
index 49407bd8eae1f34d08931795f95c4d893ee63a09..a91bdf23797ef182325da8f49d621d541f1bc8d2 100644 (file)
@@ -23,7 +23,7 @@ return [
     // Global "From" address & name
     'from' => [
         'address' => env('MAIL_FROM', '[email protected]'),
-        'name' => env('MAIL_FROM_NAME','BookStack')
+        'name' => env('MAIL_FROM_NAME', 'BookStack')
     ],
 
     // Email encryption protocol
@@ -46,4 +46,10 @@ return [
         ],
     ],
 
+    // Log Channel
+    // If you are using the "log" driver, you may specify the logging channel
+    // if you prefer to keep mail messages separate from other log entries
+    // for simpler reading. Otherwise, the default channel will be used.
+    'log_channel' => env('MAIL_LOG_CHANNEL'),
+
 ];
similarity index 52%
rename from config/queue.php
rename to app/Config/queue.php
index 721eac13685cccaebc05b4e9ec417289017c4929..46f6962c5f327f4fd644f029d3159eece4f6701a 100644 (file)
@@ -12,11 +12,12 @@ return [
 
     // Default driver to use for the queue
     // Options: null, sync, redis
-    'default' => env('QUEUE_DRIVER', 'sync'),
+    'default' => env('QUEUE_CONNECTION', 'sync'),
 
     // Queue connection configuration
     'connections' => [
 
+
         'sync' => [
             'driver' => 'sync',
         ],
@@ -25,38 +26,15 @@ return [
             'driver' => 'database',
             'table' => 'jobs',
             'queue' => 'default',
-            'expire' => 60,
-        ],
-
-        'beanstalkd' => [
-            'driver' => 'beanstalkd',
-            'host'   => 'localhost',
-            'queue'  => 'default',
-            'ttr'    => 60,
-        ],
-
-        'sqs' => [
-            'driver' => 'sqs',
-            'key'    => 'your-public-key',
-            'secret' => 'your-secret-key',
-            'queue'  => 'your-queue-url',
-            'region' => 'us-east-1',
-        ],
-
-        'iron' => [
-            'driver'  => 'iron',
-            'host'    => 'mq-aws-us-east-1.iron.io',
-            'token'   => 'your-token',
-            'project' => 'your-project-id',
-            'queue'   => 'your-queue-name',
-            'encrypt' => true,
+            'retry_after' => 90,
         ],
 
         'redis' => [
             'driver' => 'redis',
             'connection' => 'default',
-            'queue'  => 'default',
-            'expire' => 60,
+            'queue' => env('REDIS_QUEUE', 'default'),
+            'retry_after' => 90,
+            'block_for' => null,
         ],
 
     ],
similarity index 89%
rename from config/services.php
rename to app/Config/services.php
index 97cb71ddc73f0cf623907936fe43b02356496269..923015f6e62815e66b388a21f6e9d4b0b2282388 100644 (file)
@@ -22,23 +22,6 @@ return [
     // Callback URL for social authentication methods
     'callback_url' => env('APP_URL', false),
 
-    'mailgun'  => [
-        'domain' => '',
-        'secret' => '',
-    ],
-
-    'ses'      => [
-        'key'    => '',
-        'secret' => '',
-        'region' => 'us-east-1',
-    ],
-
-    'stripe'   => [
-        'model'  => \BookStack\Auth\User::class,
-        'key'    => '',
-        'secret' => '',
-    ],
-
     'github'   => [
         'client_id'     => env('GITHUB_APP_ID', false),
         'client_secret' => env('GITHUB_APP_SECRET', false),
@@ -98,8 +81,8 @@ return [
     'okta' => [
         'client_id' => env('OKTA_APP_ID'),
         'client_secret' => env('OKTA_APP_SECRET'),
-        'redirect' => env('APP_URL') . '/login/service/okta/callback', 
-        'base_url' => env('OKTA_BASE_URL'), 
+        'redirect' => env('APP_URL') . '/login/service/okta/callback',
+        'base_url' => env('OKTA_BASE_URL'),
         'name'          => 'Okta',
         'auto_register' => env('OKTA_AUTO_REGISTER', false),
         'auto_confirm' => env('OKTA_AUTO_CONFIRM_EMAIL', false),
@@ -143,10 +126,10 @@ return [
         'email_attribute' => env('LDAP_EMAIL_ATTRIBUTE', 'mail'),
         'display_name_attribute' => env('LDAP_DISPLAY_NAME_ATTRIBUTE', 'cn'),
         'follow_referrals' => env('LDAP_FOLLOW_REFERRALS', false),
-               'user_to_groups' => env('LDAP_USER_TO_GROUPS',false),
-               'group_attribute' => env('LDAP_GROUP_ATTRIBUTE', 'memberOf'),
-               'remove_from_groups' => env('LDAP_REMOVE_FROM_GROUPS',false),
-               'tls_insecure' => env('LDAP_TLS_INSECURE', false),
-       ]
+        'user_to_groups' => env('LDAP_USER_TO_GROUPS', false),
+        'group_attribute' => env('LDAP_GROUP_ATTRIBUTE', 'memberOf'),
+        'remove_from_groups' => env('LDAP_REMOVE_FROM_GROUPS', false),
+        'tls_insecure' => env('LDAP_TLS_INSECURE', false),
+    ]
 
 ];
similarity index 91%
rename from config/session.php
rename to app/Config/session.php
index bdb5e554b3238f274478e835f5dd867c4b737183..37f1627bb5f5c151ae01748cdc06b8f5c7e7eeb2 100644 (file)
@@ -35,13 +35,18 @@ return [
     // Session database table, if database driver is in use
     'table' => 'sessions',
 
+    // Session Cache Store
+    // When using the "apc" or "memcached" session drivers, you may specify a
+    // cache store that should be used for these sessions. This value must
+    // correspond with one of the application's configured cache stores.
+    'store' => null,
+
     // Session Sweeping Lottery
     // Some session drivers must manually sweep their storage location to get
     // rid of old sessions from storage. Here are the chances that it will
     // happen on a given request. By default, the odds are 2 out of 100.
     'lottery' => [2, 100],
 
-
     // Session Cookie Name
     // Here you may change the name of the cookie used to identify a session
     // instance by ID. The name specified here will get used every time a
similarity index 84%
rename from config/setting-defaults.php
rename to app/Config/setting-defaults.php
index b48253eb190f1cc408c50fc944bc41b8a8d79f28..c6080df1db6df81b434a935dde272c0f038c5c0a 100644 (file)
@@ -14,9 +14,9 @@ return [
     'app-logo'             => '',
     'app-name-header'      => true,
     'app-editor'           => 'wysiwyg',
-    'app-color'            => '#0288D1',
-    'app-color-light'      => 'rgba(21, 101, 192, 0.15)',
+    'app-color'            => '#206ea7',
+    'app-color-light'      => 'rgba(32,110,167,0.15)',
     'app-custom-head'      => false,
     'registration-enabled' => false,
 
-];
\ No newline at end of file
+];
similarity index 100%
rename from config/snappy.php
rename to app/Config/snappy.php
similarity index 100%
rename from config/view.php
rename to app/Config/view.php
index 90c1ddb1c325e4e67761b5a81d9d602ad254c103..e67da871763f8b9ef379c8687961ec2612d33bc3 100644 (file)
@@ -49,7 +49,7 @@ class CreateAdmin extends Command
         if (empty($email)) {
             $email = $this->ask('Please specify an email address for the new admin user');
         }
-        if (strlen($email) < 5 || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
+        if (mb_strlen($email) < 5 || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
             return $this->error('Invalid email address provided');
         }
 
@@ -61,7 +61,7 @@ class CreateAdmin extends Command
         if (empty($name)) {
             $name = $this->ask('Please specify an name for the new admin user');
         }
-        if (strlen($name) < 2) {
+        if (mb_strlen($name) < 2) {
             return $this->error('Invalid name provided');
         }
 
@@ -69,7 +69,7 @@ class CreateAdmin extends Command
         if (empty($password)) {
             $password = $this->secret('Please specify a password for the new admin user');
         }
-        if (strlen($password) < 5) {
+        if (mb_strlen($password) < 5) {
             return $this->error('Invalid password provided, Must be at least 5 characters');
         }
 
index 77cacf632e03dad04c6b9dcec98843170e046224..4e54457b80391aa223442d92e065f376a12b86a9 100644 (file)
@@ -1,22 +1,25 @@
 <?php namespace BookStack\Entities;
 
 use BookStack\Uploads\Image;
+use Exception;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
+use Illuminate\Database\Eloquent\Relations\BelongsToMany;
+use Illuminate\Database\Eloquent\Relations\HasMany;
+use Illuminate\Support\Collection;
 
-class Book extends Entity
+/**
+ * Class Book
+ * @property string $description
+ * @property int $image_id
+ * @property Image|null $cover
+ * @package BookStack\Entities
+ */
+class Book extends Entity implements HasCoverImage
 {
     public $searchFactor = 2;
 
     protected $fillable = ['name', 'description', 'image_id'];
 
-    /**
-     * Get the morph class for this model.
-     * @return string
-     */
-    public function getMorphClass()
-    {
-        return 'BookStack\\Book';
-    }
-
     /**
      * Get the url for this book.
      * @param string|bool $path
@@ -25,9 +28,9 @@ class Book extends Entity
     public function getUrl($path = false)
     {
         if ($path !== false) {
-            return baseUrl('/books/' . urlencode($this->slug) . '/' . trim($path, '/'));
+            return url('/books/' . urlencode($this->slug) . '/' . trim($path, '/'));
         }
-        return baseUrl('/books/' . urlencode($this->slug));
+        return url('/books/' . urlencode($this->slug));
     }
 
     /**
@@ -44,8 +47,8 @@ class Book extends Entity
         }
 
         try {
-            $cover = $this->cover ? baseUrl($this->cover->getThumb($width, $height, false)) : $default;
-        } catch (\Exception $err) {
+            $cover = $this->cover ? url($this->cover->getThumb($width, $height, false)) : $default;
+        } catch (Exception $err) {
             $cover = $default;
         }
         return $cover;
@@ -53,16 +56,23 @@ class Book extends Entity
 
     /**
      * Get the cover image of the book
-     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
      */
-    public function cover()
+    public function cover(): BelongsTo
     {
         return $this->belongsTo(Image::class, 'image_id');
     }
 
+    /**
+     * Get the type of the image model that is used when storing a cover image.
+     */
+    public function coverImageTypeKey(): string
+    {
+        return 'cover_book';
+    }
+
     /**
      * Get all pages within this book.
-     * @return \Illuminate\Database\Eloquent\Relations\HasMany
+     * @return HasMany
      */
     public function pages()
     {
@@ -71,7 +81,7 @@ class Book extends Entity
 
     /**
      * Get the direct child pages of this book.
-     * @return \Illuminate\Database\Eloquent\Relations\HasMany
+     * @return HasMany
      */
     public function directPages()
     {
@@ -80,7 +90,7 @@ class Book extends Entity
 
     /**
      * Get all chapters within this book.
-     * @return \Illuminate\Database\Eloquent\Relations\HasMany
+     * @return HasMany
      */
     public function chapters()
     {
@@ -89,7 +99,7 @@ class Book extends Entity
 
     /**
      * Get the shelves this book is contained within.
-     * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
+     * @return BelongsToMany
      */
     public function shelves()
     {
@@ -97,22 +107,24 @@ class Book extends Entity
     }
 
     /**
-     * Get an excerpt of this book's description to the specified length or less.
-     * @param int $length
-     * @return string
+     * Get the direct child items within this book.
+     * @return Collection
      */
-    public function getExcerpt(int $length = 100)
+    public function getDirectChildren(): Collection
     {
-        $description = $this->description;
-        return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;
+        $pages = $this->directPages()->visible()->get();
+        $chapters = $this->chapters()->visible()->get();
+        return $pages->contact($chapters)->sortBy('priority')->sortByDesc('draft');
     }
 
     /**
-     * Return a generalised, common raw query that can be 'unioned' across entities.
+     * Get an excerpt of this book's description to the specified length or less.
+     * @param int $length
      * @return string
      */
-    public function entityRawQuery()
+    public function getExcerpt(int $length = 100)
     {
-        return "'BookStack\\\\Book' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text,'' as html, '0' as book_id, '0' as priority, '0' as chapter_id, '0' as draft, created_by, updated_by, updated_at, created_at";
+        $description = $this->description;
+        return mb_strlen($description) > $length ? mb_substr($description, 0, $length-3) . '...' : $description;
     }
 }
diff --git a/app/Entities/BookChild.php b/app/Entities/BookChild.php
new file mode 100644 (file)
index 0000000..6eac437
--- /dev/null
@@ -0,0 +1,60 @@
+<?php namespace BookStack\Entities;
+
+use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
+
+/**
+ * Class BookChild
+ * @property int $book_id
+ * @property int $priority
+ * @property Book $book
+ * @method Builder whereSlugs(string $bookSlug, string $childSlug)
+ */
+class BookChild extends Entity
+{
+
+    /**
+     * Scope a query to find items where the the child has the given childSlug
+     * where its parent has the bookSlug.
+     */
+    public function scopeWhereSlugs(Builder $query, string $bookSlug, string $childSlug)
+    {
+        return $query->with('book')
+            ->whereHas('book', function (Builder $query) use ($bookSlug) {
+                $query->where('slug', '=', $bookSlug);
+            })
+            ->where('slug', '=', $childSlug);
+    }
+
+    /**
+     * Get the book this page sits in.
+     * @return BelongsTo
+     */
+    public function book(): BelongsTo
+    {
+        return $this->belongsTo(Book::class);
+    }
+
+    /**
+     * Change the book that this entity belongs to.
+     */
+    public function changeBook(int $newBookId): Entity
+    {
+        $this->book_id = $newBookId;
+        $this->refreshSlug();
+        $this->save();
+        $this->refresh();
+
+        // Update related activity
+        $this->activity()->update(['book_id' => $newBookId]);
+
+        // Update all child pages if a chapter
+        if ($this instanceof Chapter) {
+            foreach ($this->pages as $page) {
+                $page->changeBook($newBookId);
+            }
+        }
+
+        return $this;
+    }
+}
index 1de767feca132a283803e04470b8936fa9be3ffe..62c7e2fe4f56c3e0a78f91105d6ba3c0da6dfad9 100644 (file)
@@ -1,8 +1,10 @@
 <?php namespace BookStack\Entities;
 
 use BookStack\Uploads\Image;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
+use Illuminate\Database\Eloquent\Relations\BelongsToMany;
 
-class Bookshelf extends Entity
+class Bookshelf extends Entity implements HasCoverImage
 {
     protected $table = 'bookshelves';
 
@@ -10,15 +12,6 @@ class Bookshelf extends Entity
 
     protected $fillable = ['name', 'description', 'image_id'];
 
-    /**
-     * Get the morph class for this model.
-     * @return string
-     */
-    public function getMorphClass()
-    {
-        return 'BookStack\\Bookshelf';
-    }
-
     /**
      * Get the books in this shelf.
      * Should not be used directly since does not take into account permissions.
@@ -31,6 +24,14 @@ class Bookshelf extends Entity
             ->orderBy('order', 'asc');
     }
 
+    /**
+     * Related books that are visible to the current user.
+     */
+    public function visibleBooks(): BelongsToMany
+    {
+        return $this->books()->visible();
+    }
+
     /**
      * Get the url for this bookshelf.
      * @param string|bool $path
@@ -39,9 +40,9 @@ class Bookshelf extends Entity
     public function getUrl($path = false)
     {
         if ($path !== false) {
-            return baseUrl('/shelves/' . urlencode($this->slug) . '/' . trim($path, '/'));
+            return url('/shelves/' . urlencode($this->slug) . '/' . trim($path, '/'));
         }
-        return baseUrl('/shelves/' . urlencode($this->slug));
+        return url('/shelves/' . urlencode($this->slug));
     }
 
     /**
@@ -59,7 +60,7 @@ class Bookshelf extends Entity
         }
 
         try {
-            $cover = $this->cover ? baseUrl($this->cover->getThumb($width, $height, false)) : $default;
+            $cover = $this->cover ? url($this->cover->getThumb($width, $height, false)) : $default;
         } catch (\Exception $err) {
             $cover = $default;
         }
@@ -68,13 +69,20 @@ class Bookshelf extends Entity
 
     /**
      * Get the cover image of the shelf
-     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
      */
-    public function cover()
+    public function cover(): BelongsTo
     {
         return $this->belongsTo(Image::class, 'image_id');
     }
 
+    /**
+     * Get the type of the image model that is used when storing a cover image.
+     */
+    public function coverImageTypeKey(): string
+    {
+        return 'cover_shelf';
+    }
+
     /**
      * Get an excerpt of this book's description to the specified length or less.
      * @param int $length
@@ -83,25 +91,30 @@ class Bookshelf extends Entity
     public function getExcerpt(int $length = 100)
     {
         $description = $this->description;
-        return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;
+        return mb_strlen($description) > $length ? mb_substr($description, 0, $length-3) . '...' : $description;
     }
 
     /**
-     * Return a generalised, common raw query that can be 'unioned' across entities.
-     * @return string
+     * Check if this shelf contains the given book.
+     * @param Book $book
+     * @return bool
      */
-    public function entityRawQuery()
+    public function contains(Book $book): bool
     {
-        return "'BookStack\\\\BookShelf' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text,'' as html, '0' as book_id, '0' as priority, '0' as chapter_id, '0' as draft, created_by, updated_by, updated_at, created_at";
+        return $this->books()->where('id', '=', $book->id)->count() > 0;
     }
 
     /**
-     * Check if this shelf contains the given book.
+     * Add a book to the end of this shelf.
      * @param Book $book
-     * @return bool
      */
-    public function contains(Book $book)
+    public function appendBook(Book $book)
     {
-        return $this->books()->where('id', '=', $book->id)->count() > 0;
+        if ($this->contains($book)) {
+            return;
+        }
+
+        $maxOrder = $this->books()->max('order');
+        $this->books()->attach($book->id, ['order' => $maxOrder + 1]);
     }
 }
index 97ddbc2dc86764ed930c82d3c549f47b35c83fe1..43d63d026021dd07edcb1165841018ee213b13a3 100644 (file)
@@ -1,5 +1,6 @@
 <?php namespace BookStack\Entities;
 
+use BookStack\Entities\Managers\EntityContext;
 use Illuminate\View\View;
 
 class BreadcrumbsViewComposer
@@ -9,9 +10,9 @@ class BreadcrumbsViewComposer
 
     /**
      * BreadcrumbsViewComposer constructor.
-     * @param EntityContextManager $entityContextManager
+     * @param EntityContext $entityContextManager
      */
-    public function __construct(EntityContextManager $entityContextManager)
+    public function __construct(EntityContext $entityContextManager)
     {
         $this->entityContextManager = $entityContextManager;
     }
@@ -23,8 +24,9 @@ class BreadcrumbsViewComposer
     public function compose(View $view)
     {
         $crumbs = $view->getData()['crumbs'];
-        if (array_first($crumbs) instanceof Book) {
-            $shelf = $this->entityContextManager->getContextualShelfForBook(array_first($crumbs));
+        $firstCrumb = $crumbs[0] ?? null;
+        if ($firstCrumb instanceof Book) {
+            $shelf = $this->entityContextManager->getContextualShelfForBook($firstCrumb);
             if ($shelf) {
                 array_unshift($crumbs, $shelf);
                 $view->with('crumbs', $crumbs);
index bdacb7c9d8340ee26a43c9652587594d33afadb6..848bc6448bd467b853ef25c8982c0aa8b5ad6e4c 100644 (file)
@@ -1,29 +1,18 @@
 <?php namespace BookStack\Entities;
 
-class Chapter extends Entity
+use Illuminate\Support\Collection;
+
+/**
+ * Class Chapter
+ * @property Collection<Page> $pages
+ * @package BookStack\Entities
+ */
+class Chapter extends BookChild
 {
     public $searchFactor = 1.3;
 
     protected $fillable = ['name', 'description', 'priority', 'book_id'];
 
-    /**
-     * Get the morph class for this model.
-     * @return string
-     */
-    public function getMorphClass()
-    {
-        return 'BookStack\\Chapter';
-    }
-
-    /**
-     * Get the book this chapter is within.
-     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
-     */
-    public function book()
-    {
-        return $this->belongsTo(Book::class);
-    }
-
     /**
      * Get the pages that this chapter contains.
      * @param string $dir
@@ -42,10 +31,13 @@ class Chapter extends Entity
     public function getUrl($path = false)
     {
         $bookSlug = $this->getAttribute('bookSlug') ? $this->getAttribute('bookSlug') : $this->book->slug;
+        $fullPath = '/books/' . urlencode($bookSlug) . '/chapter/' . urlencode($this->slug);
+
         if ($path !== false) {
-            return baseUrl('/books/' . urlencode($bookSlug) . '/chapter/' . urlencode($this->slug) . '/' . trim($path, '/'));
+            $fullPath .= '/' . trim($path, '/');
         }
-        return baseUrl('/books/' . urlencode($bookSlug) . '/chapter/' . urlencode($this->slug));
+
+        return url($fullPath);
     }
 
     /**
@@ -56,24 +48,26 @@ class Chapter extends Entity
     public function getExcerpt(int $length = 100)
     {
         $description = $this->text ?? $this->description;
-        return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;
+        return mb_strlen($description) > $length ? mb_substr($description, 0, $length-3) . '...' : $description;
     }
 
     /**
-     * Return a generalised, common raw query that can be 'unioned' across entities.
-     * @return string
+     * Check if this chapter has any child pages.
+     * @return bool
      */
-    public function entityRawQuery()
+    public function hasChildren()
     {
-        return "'BookStack\\\\Chapter' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text, '' as html, book_id, priority, '0' as chapter_id, '0' as draft, created_by, updated_by, updated_at, created_at";
+        return count($this->pages) > 0;
     }
 
     /**
-     * Check if this chapter has any child pages.
-     * @return bool
+     * Get the visible pages in this chapter.
      */
-    public function hasChildren()
+    public function getVisiblePages(): Collection
     {
-        return count($this->pages) > 0;
+        return $this->pages()->visible()
+        ->orderBy('draft', 'desc')
+        ->orderBy('priority', 'asc')
+        ->get();
     }
 }
index 482d217662ae20b54f62578d88367b8daacb1098..5013c39cfcf2bf309f1931594ebe702dd00dc44c 100644 (file)
@@ -6,8 +6,11 @@ use BookStack\Actions\Tag;
 use BookStack\Actions\View;
 use BookStack\Auth\Permissions\EntityPermission;
 use BookStack\Auth\Permissions\JointPermission;
+use BookStack\Facades\Permissions;
 use BookStack\Ownable;
 use Carbon\Carbon;
+use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Database\Eloquent\Collection;
 use Illuminate\Database\Eloquent\Relations\MorphMany;
 
 /**
@@ -15,7 +18,7 @@ use Illuminate\Database\Eloquent\Relations\MorphMany;
  * The base class for book-like items such as pages, chapters & books.
  * This is not a database model in itself but extended.
  *
- * @property integer $id
+ * @property int $id
  * @property string $name
  * @property string $slug
  * @property Carbon $created_at
@@ -23,6 +26,11 @@ use Illuminate\Database\Eloquent\Relations\MorphMany;
  * @property int $created_by
  * @property int $updated_by
  * @property boolean $restricted
+ * @property Collection $tags
+ * @method static Entity|Builder visible()
+ * @method static Entity|Builder hasPermission(string $permission)
+ * @method static Builder withLastView()
+ * @method static Builder withViewCount()
  *
  * @package BookStack\Entities
  */
@@ -40,14 +48,45 @@ class Entity extends Ownable
     public $searchFactor = 1.0;
 
     /**
-     * Get the morph class for this model.
-     * Set here since, due to folder changes, the namespace used
-     * in the database no longer matches the class namespace.
-     * @return string
+     * Get the entities that are visible to the current user.
+     */
+    public function scopeVisible(Builder $query)
+    {
+        return $this->scopeHasPermission($query, 'view');
+    }
+
+    /**
+     * Scope the query to those entities that the current user has the given permission for.
+     */
+    public function scopeHasPermission(Builder $query, string $permission)
+    {
+        return Permissions::restrictEntityQuery($query, $permission);
+    }
+
+    /**
+     * Query scope to get the last view from the current user.
      */
-    public function getMorphClass()
+    public function scopeWithLastView(Builder $query)
     {
-        return 'BookStack\\Entity';
+        $viewedAtQuery = View::query()->select('updated_at')
+            ->whereColumn('viewable_id', '=', $this->getTable() . '.id')
+            ->where('viewable_type', '=', $this->getMorphClass())
+            ->where('user_id', '=', user()->id)
+            ->take(1);
+
+        return $query->addSelect(['last_viewed_at' => $viewedAtQuery]);
+    }
+
+    /**
+     * Query scope to get the total view count of the entities.
+     */
+    public function scopeWithViewCount(Builder $query)
+    {
+        $viewCountQuery = View::query()->selectRaw('SUM(views) as view_count')
+            ->whereColumn('viewable_id', '=', $this->getTable() . '.id')
+            ->where('viewable_type', '=', $this->getMorphClass())->take(1);
+
+        $query->addSelect(['view_count' => $viewCountQuery]);
     }
 
     /**
@@ -87,11 +126,12 @@ class Entity extends Ownable
 
     /**
      * Gets the activity objects for this entity.
-     * @return \Illuminate\Database\Eloquent\Relations\MorphMany
+     * @return MorphMany
      */
     public function activity()
     {
-        return $this->morphMany(Activity::class, 'entity')->orderBy('created_at', 'desc');
+        return $this->morphMany(Activity::class, 'entity')
+            ->orderBy('created_at', 'desc');
     }
 
     /**
@@ -102,14 +142,9 @@ class Entity extends Ownable
         return $this->morphMany(View::class, 'viewable');
     }
 
-    public function viewCountQuery()
-    {
-        return $this->views()->selectRaw('viewable_id, sum(views) as view_count')->groupBy('viewable_id');
-    }
-
     /**
      * Get the Tag models that have been user assigned to this entity.
-     * @return \Illuminate\Database\Eloquent\Relations\MorphMany
+     * @return MorphMany
      */
     public function tags()
     {
@@ -129,7 +164,7 @@ class Entity extends Ownable
 
     /**
      * Get the related search terms.
-     * @return \Illuminate\Database\Eloquent\Relations\MorphMany
+     * @return MorphMany
      */
     public function searchTerms()
     {
@@ -158,7 +193,7 @@ class Entity extends Ownable
 
     /**
      * Get the entity jointPermissions this is connected to.
-     * @return \Illuminate\Database\Eloquent\Relations\MorphMany
+     * @return MorphMany
      */
     public function jointPermissions()
     {
@@ -238,21 +273,40 @@ class Entity extends Ownable
     }
 
     /**
-     * Return a generalised, common raw query that can be 'unioned' across entities.
+     * Get the url of this entity
+     * @param $path
      * @return string
      */
-    public function entityRawQuery()
+    public function getUrl($path = '/')
     {
-        return '';
+        return $path;
     }
 
     /**
-     * Get the url of this entity
-     * @param $path
-     * @return string
+     * Rebuild the permissions for this entity.
      */
-    public function getUrl($path = '/')
+    public function rebuildPermissions()
     {
-        return $path;
+        /** @noinspection PhpUnhandledExceptionInspection */
+        Permissions::buildJointPermissionsForEntity($this);
+    }
+
+    /**
+     * Index the current entity for search
+     */
+    public function indexForSearch()
+    {
+        $searchService = app()->make(SearchService::class);
+        $searchService->indexEntity($this);
+    }
+
+    /**
+     * Generate and set a new URL slug for this model.
+     */
+    public function refreshSlug(): string
+    {
+        $generator = new SlugGenerator($this);
+        $this->slug = $generator->generate();
+        return $this->slug;
     }
 }
index d0d4a7ad6c073a7737fdb0a03ef569fbef2487cb..6bf923b3112aa8e7387fd6eedeb601a601511dad 100644 (file)
@@ -39,11 +39,6 @@ class EntityProvider
 
     /**
      * EntityProvider constructor.
-     * @param Bookshelf $bookshelf
-     * @param Book $book
-     * @param Chapter $chapter
-     * @param Page $page
-     * @param PageRevision $pageRevision
      */
     public function __construct(
         Bookshelf $bookshelf,
@@ -62,9 +57,8 @@ class EntityProvider
     /**
      * Fetch all core entity types as an associated array
      * with their basic names as the keys.
-     * @return Entity[]
      */
-    public function all()
+    public function all(): array
     {
         return [
             'bookshelf' => $this->bookshelf,
@@ -76,10 +70,8 @@ class EntityProvider
 
     /**
      * Get an entity instance by it's basic name.
-     * @param string $type
-     * @return Entity
      */
-    public function get(string $type)
+    public function get(string $type): Entity
     {
         $type = strtolower($type);
         return $this->all()[$type];
@@ -87,15 +79,9 @@ class EntityProvider
 
     /**
      * Get the morph classes, as an array, for a single or multiple types.
-     * @param string|array $types
-     * @return array<string>
      */
-    public function getMorphClasses($types)
+    public function getMorphClasses(array $types): array
     {
-        if (is_string($types)) {
-            $types = [$types];
-        }
-
         $morphClasses = [];
         foreach ($types as $type) {
             $model = $this->get($type);
index 09635aa214f0b8e112583e2c747644030e5bd4b1..3ec867959a7d8b1046127e90534bff5050ca73f6 100644 (file)
@@ -1,35 +1,34 @@
 <?php namespace BookStack\Entities;
 
-use BookStack\Entities\Repos\EntityRepo;
+use BookStack\Entities\Managers\BookContents;
+use BookStack\Entities\Managers\PageContent;
 use BookStack\Uploads\ImageService;
+use DomPDF;
+use Exception;
+use SnappyPDF;
+use Throwable;
 
 class ExportService
 {
 
-    protected $entityRepo;
     protected $imageService;
 
     /**
      * ExportService constructor.
-     * @param EntityRepo $entityRepo
-     * @param ImageService $imageService
      */
-    public function __construct(EntityRepo $entityRepo, ImageService $imageService)
+    public function __construct(ImageService $imageService)
     {
-        $this->entityRepo = $entityRepo;
         $this->imageService = $imageService;
     }
 
     /**
      * Convert a page to a self-contained HTML file.
      * Includes required CSS & image content. Images are base64 encoded into the HTML.
-     * @param \BookStack\Entities\Page $page
-     * @return mixed|string
-     * @throws \Throwable
+     * @throws Throwable
      */
     public function pageToContainedHtml(Page $page)
     {
-        $this->entityRepo->renderPage($page);
+        $page->html = (new PageContent($page))->render();
         $pageHtml = view('pages/export', [
             'page' => $page
         ])->render();
@@ -38,15 +37,13 @@ class ExportService
 
     /**
      * Convert a chapter to a self-contained HTML file.
-     * @param \BookStack\Entities\Chapter $chapter
-     * @return mixed|string
-     * @throws \Throwable
+     * @throws Throwable
      */
     public function chapterToContainedHtml(Chapter $chapter)
     {
-        $pages = $this->entityRepo->getChapterChildren($chapter);
+        $pages = $chapter->getVisiblePages();
         $pages->each(function ($page) {
-            $page->html = $this->entityRepo->renderPage($page);
+            $page->html = (new PageContent($page))->render();
         });
         $html = view('chapters/export', [
             'chapter' => $chapter,
@@ -57,13 +54,11 @@ class ExportService
 
     /**
      * Convert a book to a self-contained HTML file.
-     * @param Book $book
-     * @return mixed|string
-     * @throws \Throwable
+     * @throws Throwable
      */
     public function bookToContainedHtml(Book $book)
     {
-        $bookTree = $this->entityRepo->getBookChildren($book, true, true);
+        $bookTree = (new BookContents($book))->getTree(false, true);
         $html = view('books/export', [
             'book' => $book,
             'bookChildren' => $bookTree
@@ -73,13 +68,11 @@ class ExportService
 
     /**
      * Convert a page to a PDF file.
-     * @param Page $page
-     * @return mixed|string
-     * @throws \Throwable
+     * @throws Throwable
      */
     public function pageToPdf(Page $page)
     {
-        $this->entityRepo->renderPage($page);
+        $page->html = (new PageContent($page))->render();
         $html = view('pages/pdf', [
             'page' => $page
         ])->render();
@@ -88,32 +81,30 @@ class ExportService
 
     /**
      * Convert a chapter to a PDF file.
-     * @param \BookStack\Entities\Chapter $chapter
-     * @return mixed|string
-     * @throws \Throwable
+     * @throws Throwable
      */
     public function chapterToPdf(Chapter $chapter)
     {
-        $pages = $this->entityRepo->getChapterChildren($chapter);
+        $pages = $chapter->getVisiblePages();
         $pages->each(function ($page) {
-            $page->html = $this->entityRepo->renderPage($page);
+            $page->html = (new PageContent($page))->render();
         });
+
         $html = view('chapters/export', [
             'chapter' => $chapter,
             'pages' => $pages
         ])->render();
+
         return $this->htmlToPdf($html);
     }
 
     /**
-     * Convert a book to a PDF file
-     * @param \BookStack\Entities\Book $book
-     * @return string
-     * @throws \Throwable
+     * Convert a book to a PDF file.
+     * @throws Throwable
      */
     public function bookToPdf(Book $book)
     {
-        $bookTree = $this->entityRepo->getBookChildren($book, true, true);
+        $bookTree = (new BookContents($book))->getTree(false, true);
         $html = view('books/export', [
             'book' => $book,
             'bookChildren' => $bookTree
@@ -122,31 +113,27 @@ class ExportService
     }
 
     /**
-     * Convert normal webpage HTML to a PDF.
-     * @param $html
-     * @return string
-     * @throws \Exception
+     * Convert normal web-page HTML to a PDF.
+     * @throws Exception
      */
-    protected function htmlToPdf($html)
+    protected function htmlToPdf(string $html): string
     {
         $containedHtml = $this->containHtml($html);
         $useWKHTML = config('snappy.pdf.binary') !== false;
         if ($useWKHTML) {
-            $pdf = \SnappyPDF::loadHTML($containedHtml);
+            $pdf = SnappyPDF::loadHTML($containedHtml);
             $pdf->setOption('print-media-type', true);
         } else {
-            $pdf = \DomPDF::loadHTML($containedHtml);
+            $pdf = DomPDF::loadHTML($containedHtml);
         }
         return $pdf->output();
     }
 
     /**
      * Bundle of the contents of a html file to be self-contained.
-     * @param $htmlContent
-     * @return mixed|string
-     * @throws \Exception
+     * @throws Exception
      */
-    protected function containHtml($htmlContent)
+    protected function containHtml(string $htmlContent): string
     {
         $imageTagsOutput = [];
         preg_match_all("/\<img.*src\=(\'|\")(.*?)(\'|\").*?\>/i", $htmlContent, $imageTagsOutput);
@@ -188,12 +175,10 @@ class ExportService
     /**
      * Converts the page contents into simple plain text.
      * This method filters any bad looking content to provide a nice final output.
-     * @param Page $page
-     * @return mixed
      */
-    public function pageToPlainText(Page $page)
+    public function pageToPlainText(Page $page): string
     {
-        $html = $this->entityRepo->renderPage($page);
+        $html = (new PageContent($page))->render();
         $text = strip_tags($html);
         // Replace multiple spaces with single spaces
         $text = preg_replace('/\ {2,}/', ' ', $text);
@@ -207,10 +192,8 @@ class ExportService
 
     /**
      * Convert a chapter into a plain text string.
-     * @param \BookStack\Entities\Chapter $chapter
-     * @return string
      */
-    public function chapterToPlainText(Chapter $chapter)
+    public function chapterToPlainText(Chapter $chapter): string
     {
         $text = $chapter->name . "\n\n";
         $text .= $chapter->description . "\n\n";
@@ -222,12 +205,10 @@ class ExportService
 
     /**
      * Convert a book into a plain text string.
-     * @param Book $book
-     * @return string
      */
-    public function bookToPlainText(Book $book)
+    public function bookToPlainText(Book $book): string
     {
-        $bookTree = $this->entityRepo->getBookChildren($book, true, true);
+        $bookTree = (new BookContents($book))->getTree(false, true);
         $text = $book->name . "\n\n";
         foreach ($bookTree as $bookChild) {
             if ($bookChild->isA('chapter')) {
diff --git a/app/Entities/HasCoverImage.php b/app/Entities/HasCoverImage.php
new file mode 100644 (file)
index 0000000..31277f4
--- /dev/null
@@ -0,0 +1,20 @@
+<?php
+
+
+namespace BookStack\Entities;
+
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
+
+interface HasCoverImage
+{
+
+    /**
+     * Get the cover image for this item.
+     */
+    public function cover(): BelongsTo;
+
+    /**
+     * Get the type of the image model that is used when storing a cover image.
+     */
+    public function coverImageTypeKey(): string;
+}
diff --git a/app/Entities/Managers/BookContents.php b/app/Entities/Managers/BookContents.php
new file mode 100644 (file)
index 0000000..8b8d02c
--- /dev/null
@@ -0,0 +1,204 @@
+<?php namespace BookStack\Entities\Managers;
+
+use BookStack\Entities\Book;
+use BookStack\Entities\BookChild;
+use BookStack\Entities\Chapter;
+use BookStack\Entities\Entity;
+use BookStack\Entities\Page;
+use BookStack\Exceptions\SortOperationException;
+use Illuminate\Support\Collection;
+
+class BookContents
+{
+
+    /**
+     * @var Book
+     */
+    protected $book;
+
+    /**
+     * BookContents constructor.
+     * @param $book
+     */
+    public function __construct(Book $book)
+    {
+        $this->book = $book;
+    }
+
+    /**
+     * Get the current priority of the last item
+     * at the top-level of the book.
+     */
+    public function getLastPriority(): int
+    {
+        $maxPage = Page::visible()->where('book_id', '=', $this->book->id)
+            ->where('draft', '=', false)
+            ->where('chapter_id', '=', 0)->max('priority');
+        $maxChapter = Chapter::visible()->where('book_id', '=', $this->book->id)
+            ->max('priority');
+        return max($maxChapter, $maxPage, 1);
+    }
+
+    /**
+     * Get the contents as a sorted collection tree.
+     * TODO - Support $renderPages option
+     */
+    public function getTree(bool $showDrafts = false, bool $renderPages = false): Collection
+    {
+        $pages = $this->getPages($showDrafts);
+        $chapters = Chapter::visible()->where('book_id', '=', $this->book->id)->get();
+        $all = collect()->concat($pages)->concat($chapters);
+        $chapterMap = $chapters->keyBy('id');
+        $lonePages = collect();
+
+        $pages->groupBy('chapter_id')->each(function ($pages, $chapter_id) use ($chapterMap, &$lonePages) {
+            $chapter = $chapterMap->get($chapter_id);
+            if ($chapter) {
+                $chapter->setAttribute('pages', collect($pages)->sortBy($this->bookChildSortFunc()));
+            } else {
+                $lonePages = $lonePages->concat($pages);
+            }
+        });
+
+        $all->each(function (Entity $entity) {
+            $entity->setRelation('book', $this->book);
+        });
+
+        return collect($chapters)->concat($lonePages)->sortBy($this->bookChildSortFunc());
+    }
+
+    /**
+     * Function for providing a sorting score for an entity in relation to the
+     * other items within the book.
+     */
+    protected function bookChildSortFunc(): callable
+    {
+        return function (Entity $entity) {
+            if (isset($entity['draft']) && $entity['draft']) {
+                return -100;
+            }
+            return $entity['priority'] ?? 0;
+        };
+    }
+
+    /**
+     * Get the visible pages within this book.
+     */
+    protected function getPages(bool $showDrafts = false): Collection
+    {
+        $query = Page::visible()->where('book_id', '=', $this->book->id);
+
+        if (!$showDrafts) {
+            $query->where('draft', '=', false);
+        }
+
+        return $query->get();
+    }
+
+    /**
+     * Sort the books content using the given map.
+     * The map is a single-dimension collection of objects in the following format:
+     *   {
+     *     +"id": "294" (ID of item)
+     *     +"sort": 1 (Sort order index)
+     *     +"parentChapter": false (ID of parent chapter, as string, or false)
+     *     +"type": "page" (Entity type of item)
+     *     +"book": "1" (Id of book to place item in)
+     *   }
+     *
+     * Returns a list of books that were involved in the operation.
+     * @throws SortOperationException
+     */
+    public function sortUsingMap(Collection $sortMap): Collection
+    {
+        // Load models into map
+        $this->loadModelsIntoSortMap($sortMap);
+        $booksInvolved = $this->getBooksInvolvedInSort($sortMap);
+
+        // Perform the sort
+        $sortMap->each(function ($mapItem) {
+            $this->applySortUpdates($mapItem);
+        });
+
+        // Update permissions and activity.
+        $booksInvolved->each(function (Book $book) {
+            $book->rebuildPermissions();
+        });
+
+        return $booksInvolved;
+    }
+
+    /**
+     * Using the given sort map item, detect changes for the related model
+     * and update it if required.
+     */
+    protected function applySortUpdates(\stdClass $sortMapItem)
+    {
+        /** @var BookChild $model */
+        $model = $sortMapItem->model;
+
+        $priorityChanged = intval($model->priority) !== intval($sortMapItem->sort);
+        $bookChanged = intval($model->book_id) !== intval($sortMapItem->book);
+        $chapterChanged = ($sortMapItem->type === 'page') && intval($model->chapter_id) !== $sortMapItem->parentChapter;
+
+        if ($bookChanged) {
+            $model->changeBook($sortMapItem->book);
+        }
+
+        if ($chapterChanged) {
+            $model->chapter_id = intval($sortMapItem->parentChapter);
+            $model->save();
+        }
+
+        if ($priorityChanged) {
+            $model->priority = intval($sortMapItem->sort);
+            $model->save();
+        }
+    }
+
+    /**
+     * Load models from the database into the given sort map.
+     */
+    protected function loadModelsIntoSortMap(Collection $sortMap): void
+    {
+        $keyMap = $sortMap->keyBy(function (\stdClass $sortMapItem) {
+            return  $sortMapItem->type . ':' . $sortMapItem->id;
+        });
+        $pageIds = $sortMap->where('type', '=', 'page')->pluck('id');
+        $chapterIds = $sortMap->where('type', '=', 'chapter')->pluck('id');
+
+        $pages = Page::visible()->whereIn('id', $pageIds)->get();
+        $chapters = Chapter::visible()->whereIn('id', $chapterIds)->get();
+
+        foreach ($pages as $page) {
+            $sortItem = $keyMap->get('page:' . $page->id);
+            $sortItem->model = $page;
+        }
+
+        foreach ($chapters as $chapter) {
+            $sortItem = $keyMap->get('chapter:' . $chapter->id);
+            $sortItem->model = $chapter;
+        }
+    }
+
+    /**
+     * Get the books involved in a sort.
+     * The given sort map should have its models loaded first.
+     * @throws SortOperationException
+     */
+    protected function getBooksInvolvedInSort(Collection $sortMap): Collection
+    {
+        $bookIdsInvolved = collect([$this->book->id]);
+        $bookIdsInvolved = $bookIdsInvolved->concat($sortMap->pluck('book'));
+        $bookIdsInvolved = $bookIdsInvolved->concat($sortMap->pluck('model.book_id'));
+        $bookIdsInvolved = $bookIdsInvolved->unique()->toArray();
+
+        $books = Book::hasPermission('update')->whereIn('id', $bookIdsInvolved)->get();
+
+        if (count($books) !== count($bookIdsInvolved)) {
+            throw new SortOperationException("Could not find all books requested in sort operation");
+        }
+
+        return $books;
+    }
+}
similarity index 53%
rename from app/Entities/EntityContextManager.php
rename to app/Entities/Managers/EntityContext.php
index 20be0de2b01a5e34fc61a6d0b2752b6d8c2ae903..551cd1a100c142f26cea88d8e86aaa282c768fa0 100644 (file)
@@ -1,44 +1,38 @@
-<?php namespace BookStack\Entities;
+<?php namespace BookStack\Entities\Managers;
 
-use BookStack\Entities\Repos\EntityRepo;
+use BookStack\Entities\Book;
+use BookStack\Entities\Bookshelf;
 use Illuminate\Session\Store;
 
-class EntityContextManager
+class EntityContext
 {
     protected $session;
-    protected $entityRepo;
 
     protected $KEY_SHELF_CONTEXT_ID = 'context_bookshelf_id';
 
     /**
      * EntityContextManager constructor.
-     * @param Store $session
-     * @param EntityRepo $entityRepo
      */
-    public function __construct(Store $session, EntityRepo $entityRepo)
+    public function __construct(Store $session)
     {
         $this->session = $session;
-        $this->entityRepo = $entityRepo;
     }
 
     /**
      * Get the current bookshelf context for the given book.
-     * @param Book $book
-     * @return Bookshelf|null
      */
-    public function getContextualShelfForBook(Book $book)
+    public function getContextualShelfForBook(Book $book): ?Bookshelf
     {
         $contextBookshelfId = $this->session->get($this->KEY_SHELF_CONTEXT_ID, null);
-        if (is_int($contextBookshelfId)) {
 
-            /** @var Bookshelf $shelf */
-            $shelf = $this->entityRepo->getById('bookshelf', $contextBookshelfId);
-
-            if ($shelf && $shelf->contains($book)) {
-                return $shelf;
-            }
+        if (!is_int($contextBookshelfId)) {
+            return null;
         }
-        return null;
+
+        $shelf = Bookshelf::visible()->find($contextBookshelfId);
+        $shelfContainsBook = $shelf && $shelf->contains($book);
+
+        return $shelfContainsBook ? $shelf : null;
     }
 
     /**
diff --git a/app/Entities/Managers/PageContent.php b/app/Entities/Managers/PageContent.php
new file mode 100644 (file)
index 0000000..36bc244
--- /dev/null
@@ -0,0 +1,304 @@
+<?php namespace BookStack\Entities\Managers;
+
+use BookStack\Entities\Page;
+use DOMDocument;
+use DOMElement;
+use DOMNodeList;
+use DOMXPath;
+
+class PageContent
+{
+
+    protected $page;
+
+    /**
+     * PageContent constructor.
+     */
+    public function __construct(Page $page)
+    {
+        $this->page = $page;
+    }
+
+    /**
+     * Update the content of the page with new provided HTML.
+     */
+    public function setNewHTML(string $html)
+    {
+        $this->page->html = $this->formatHtml($html);
+        $this->page->text = $this->toPlainText();
+    }
+
+    /**
+     * Formats a page's html to be tagged correctly within the system.
+     */
+    protected function formatHtml(string $htmlText): string
+    {
+        if ($htmlText == '') {
+            return $htmlText;
+        }
+
+        libxml_use_internal_errors(true);
+        $doc = new DOMDocument();
+        $doc->loadHTML(mb_convert_encoding($htmlText, 'HTML-ENTITIES', 'UTF-8'));
+
+        $container = $doc->documentElement;
+        $body = $container->childNodes->item(0);
+        $childNodes = $body->childNodes;
+
+        // Set ids on top-level nodes
+        $idMap = [];
+        foreach ($childNodes as $index => $childNode) {
+            $this->setUniqueId($childNode, $idMap);
+        }
+
+        // Ensure no duplicate ids within child items
+        $xPath = new DOMXPath($doc);
+        $idElems = $xPath->query('//body//*//*[@id]');
+        foreach ($idElems as $domElem) {
+            $this->setUniqueId($domElem, $idMap);
+        }
+
+        // Generate inner html as a string
+        $html = '';
+        foreach ($childNodes as $childNode) {
+            $html .= $doc->saveHTML($childNode);
+        }
+
+        return $html;
+    }
+
+    /**
+     * Set a unique id on the given DOMElement.
+     * A map for existing ID's should be passed in to check for current existence.
+     * @param DOMElement $element
+     * @param array $idMap
+     */
+    protected function setUniqueId($element, array &$idMap)
+    {
+        if (get_class($element) !== 'DOMElement') {
+            return;
+        }
+
+        // Overwrite id if not a BookStack custom id
+        $existingId = $element->getAttribute('id');
+        if (strpos($existingId, 'bkmrk') === 0 && !isset($idMap[$existingId])) {
+            $idMap[$existingId] = true;
+            return;
+        }
+
+        // Create an unique id for the element
+        // Uses the content as a basis to ensure output is the same every time
+        // the same content is passed through.
+        $contentId = 'bkmrk-' . mb_substr(strtolower(preg_replace('/\s+/', '-', trim($element->nodeValue))), 0, 20);
+        $newId = urlencode($contentId);
+        $loopIndex = 0;
+
+        while (isset($idMap[$newId])) {
+            $newId = urlencode($contentId . '-' . $loopIndex);
+            $loopIndex++;
+        }
+
+        $element->setAttribute('id', $newId);
+        $idMap[$newId] = true;
+    }
+
+    /**
+     * Get a plain-text visualisation of this page.
+     */
+    protected function toPlainText(): string
+    {
+        $html = $this->render(true);
+        return strip_tags($html);
+    }
+
+    /**
+     * Render the page for viewing
+     */
+    public function render(bool $blankIncludes = false) : string
+    {
+        $content = $this->page->html;
+
+        if (!config('app.allow_content_scripts')) {
+            $content = $this->escapeScripts($content);
+        }
+
+        if ($blankIncludes) {
+            $content = $this->blankPageIncludes($content);
+        } else {
+            $content = $this->parsePageIncludes($content);
+        }
+
+        return $content;
+    }
+
+    /**
+     * Parse the headers on the page to get a navigation menu
+     */
+    public function getNavigation(string $htmlContent): array
+    {
+        if (empty($htmlContent)) {
+            return [];
+        }
+
+        libxml_use_internal_errors(true);
+        $doc = new DOMDocument();
+        $doc->loadHTML(mb_convert_encoding($htmlContent, 'HTML-ENTITIES', 'UTF-8'));
+        $xPath = new DOMXPath($doc);
+        $headers = $xPath->query("//h1|//h2|//h3|//h4|//h5|//h6");
+
+        return $headers ? $this->headerNodesToLevelList($headers) : [];
+    }
+
+    /**
+     * Convert a DOMNodeList into an array of readable header attributes
+     * with levels normalised to the lower header level.
+     */
+    protected function headerNodesToLevelList(DOMNodeList $nodeList): array
+    {
+        $tree = collect($nodeList)->map(function ($header) {
+            $text = trim(str_replace("\xc2\xa0", '', $header->nodeValue));
+            $text = mb_substr($text, 0, 100);
+
+            return [
+                'nodeName' => strtolower($header->nodeName),
+                'level' => intval(str_replace('h', '', $header->nodeName)),
+                'link' => '#' . $header->getAttribute('id'),
+                'text' => $text,
+            ];
+        })->filter(function ($header) {
+            return mb_strlen($header['text']) > 0;
+        });
+
+        // Shift headers if only smaller headers have been used
+        $levelChange = ($tree->pluck('level')->min() - 1);
+        $tree = $tree->map(function ($header) use ($levelChange) {
+            $header['level'] -= ($levelChange);
+            return $header;
+        });
+
+        return $tree->toArray();
+    }
+
+    /**
+     * Remove any page include tags within the given HTML.
+     */
+    protected function blankPageIncludes(string $html) : string
+    {
+        return preg_replace("/{{@\s?([0-9].*?)}}/", '', $html);
+    }
+
+    /**
+     * Parse any include tags "{{@<page_id>#section}}" to be part of the page.
+     */
+    protected function parsePageIncludes(string $html) : string
+    {
+        $matches = [];
+        preg_match_all("/{{@\s?([0-9].*?)}}/", $html, $matches);
+
+        foreach ($matches[1] as $index => $includeId) {
+            $fullMatch = $matches[0][$index];
+            $splitInclude = explode('#', $includeId, 2);
+
+            // Get page id from reference
+            $pageId = intval($splitInclude[0]);
+            if (is_nan($pageId)) {
+                continue;
+            }
+
+            // Find page and skip this if page not found
+            $matchedPage = Page::visible()->find($pageId);
+            if ($matchedPage === null) {
+                $html = str_replace($fullMatch, '', $html);
+                continue;
+            }
+
+            // If we only have page id, just insert all page html and continue.
+            if (count($splitInclude) === 1) {
+                $html = str_replace($fullMatch, $matchedPage->html, $html);
+                continue;
+            }
+
+            // Create and load HTML into a document
+            $innerContent = $this->fetchSectionOfPage($matchedPage, $splitInclude[1]);
+            $html = str_replace($fullMatch, trim($innerContent), $html);
+        }
+
+        return $html;
+    }
+
+
+    /**
+     * Fetch the content from a specific section of the given page.
+     */
+    protected function fetchSectionOfPage(Page $page, string $sectionId): string
+    {
+        $topLevelTags = ['table', 'ul', 'ol'];
+        $doc = new DOMDocument();
+        libxml_use_internal_errors(true);
+        $doc->loadHTML(mb_convert_encoding('<body>'.$page->html.'</body>', 'HTML-ENTITIES', 'UTF-8'));
+
+        // Search included content for the id given and blank out if not exists.
+        $matchingElem = $doc->getElementById($sectionId);
+        if ($matchingElem === null) {
+            return '';
+        }
+
+        // Otherwise replace the content with the found content
+        // Checks if the top-level wrapper should be included by matching on tag types
+        $innerContent = '';
+        $isTopLevel = in_array(strtolower($matchingElem->nodeName), $topLevelTags);
+        if ($isTopLevel) {
+            $innerContent .= $doc->saveHTML($matchingElem);
+        } else {
+            foreach ($matchingElem->childNodes as $childNode) {
+                $innerContent .= $doc->saveHTML($childNode);
+            }
+        }
+        libxml_clear_errors();
+
+        return $innerContent;
+    }
+
+    /**
+     * Escape script tags within HTML content.
+     */
+    protected function escapeScripts(string $html) : string
+    {
+        if (empty($html)) {
+            return $html;
+        }
+
+        libxml_use_internal_errors(true);
+        $doc = new DOMDocument();
+        $doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
+        $xPath = new DOMXPath($doc);
+
+        // Remove standard script tags
+        $scriptElems = $xPath->query('//script');
+        foreach ($scriptElems as $scriptElem) {
+            $scriptElem->parentNode->removeChild($scriptElem);
+        }
+
+        // Remove data or JavaScript iFrames
+        $badIframes = $xPath->query('//*[contains(@src, \'data:\')] | //*[contains(@src, \'javascript:\')] | //*[@srcdoc]');
+        foreach ($badIframes as $badIframe) {
+            $badIframe->parentNode->removeChild($badIframe);
+        }
+
+        // Remove 'on*' attributes
+        $onAttributes = $xPath->query('//@*[starts-with(name(), \'on\')]');
+        foreach ($onAttributes as $attr) {
+            /** @var \DOMAttr $attr*/
+            $attrName = $attr->nodeName;
+            $attr->parentNode->removeAttribute($attrName);
+        }
+
+        $html = '';
+        $topElems = $doc->documentElement->childNodes->item(0)->childNodes;
+        foreach ($topElems as $child) {
+            $html .= $doc->saveHTML($child);
+        }
+
+        return $html;
+    }
+}
diff --git a/app/Entities/Managers/PageEditActivity.php b/app/Entities/Managers/PageEditActivity.php
new file mode 100644 (file)
index 0000000..cebbf87
--- /dev/null
@@ -0,0 +1,74 @@
+<?php namespace BookStack\Entities\Managers;
+
+use BookStack\Entities\Page;
+use BookStack\Entities\PageRevision;
+use Carbon\Carbon;
+use Illuminate\Database\Eloquent\Builder;
+
+class PageEditActivity
+{
+
+    protected $page;
+
+    /**
+     * PageEditActivity constructor.
+     */
+    public function __construct(Page $page)
+    {
+        $this->page = $page;
+    }
+
+    /**
+     * Check if there's active editing being performed on this page.
+     * @return bool
+     */
+    public function hasActiveEditing(): bool
+    {
+        return $this->activePageEditingQuery(60)->count() > 0;
+    }
+
+    /**
+     * Get a notification message concerning the editing activity on the page.
+     */
+    public function activeEditingMessage(): string
+    {
+        $pageDraftEdits = $this->activePageEditingQuery(60)->get();
+        $count = $pageDraftEdits->count();
+
+        $userMessage = $count > 1 ? trans('entities.pages_draft_edit_active.start_a', ['count' => $count]): trans('entities.pages_draft_edit_active.start_b', ['userName' => $pageDraftEdits->first()->createdBy->name]);
+        $timeMessage = trans('entities.pages_draft_edit_active.time_b', ['minCount'=> 60]);
+        return trans('entities.pages_draft_edit_active.message', ['start' => $userMessage, 'time' => $timeMessage]);
+    }
+
+    /**
+     * Get the message to show when the user will be editing one of their drafts.
+     * @param PageRevision $draft
+     * @return string
+     */
+    public function getEditingActiveDraftMessage(PageRevision $draft): string
+    {
+        $message = trans('entities.pages_editing_draft_notification', ['timeDiff' => $draft->updated_at->diffForHumans()]);
+        if ($draft->page->updated_at->timestamp <= $draft->updated_at->timestamp) {
+            return $message;
+        }
+        return $message . "\n" . trans('entities.pages_draft_edited_notification');
+    }
+
+    /**
+     * A query to check for active update drafts on a particular page
+     * within the last given many minutes.
+     */
+    protected function activePageEditingQuery(int $withinMinutes): Builder
+    {
+        $checkTime = Carbon::now()->subMinutes($withinMinutes);
+        $query = PageRevision::query()
+            ->where('type', '=', 'update_draft')
+            ->where('page_id', '=', $this->page->id)
+            ->where('updated_at', '>', $this->page->updated_at)
+            ->where('created_by', '!=', user()->id)
+            ->where('updated_at', '>=', $checkTime)
+            ->with('createdBy');
+
+        return $query;
+    }
+}
diff --git a/app/Entities/Managers/TrashCan.php b/app/Entities/Managers/TrashCan.php
new file mode 100644 (file)
index 0000000..1a32294
--- /dev/null
@@ -0,0 +1,109 @@
+<?php namespace BookStack\Entities\Managers;
+
+use BookStack\Entities\Book;
+use BookStack\Entities\Bookshelf;
+use BookStack\Entities\Chapter;
+use BookStack\Entities\Entity;
+use BookStack\Entities\HasCoverImage;
+use BookStack\Entities\Page;
+use BookStack\Exceptions\NotifyException;
+use BookStack\Facades\Activity;
+use BookStack\Uploads\AttachmentService;
+use BookStack\Uploads\ImageService;
+use Exception;
+use Illuminate\Contracts\Container\BindingResolutionException;
+
+class TrashCan
+{
+
+    /**
+     * Remove a bookshelf from the system.
+     * @throws Exception
+     */
+    public function destroyShelf(Bookshelf $shelf)
+    {
+        $this->destroyCommonRelations($shelf);
+        $shelf->delete();
+    }
+
+    /**
+     * Remove a book from the system.
+     * @throws NotifyException
+     * @throws BindingResolutionException
+     */
+    public function destroyBook(Book $book)
+    {
+        foreach ($book->pages as $page) {
+            $this->destroyPage($page);
+        }
+
+        foreach ($book->chapters as $chapter) {
+            $this->destroyChapter($chapter);
+        }
+
+        $this->destroyCommonRelations($book);
+        $book->delete();
+    }
+
+    /**
+     * Remove a page from the system.
+     * @throws NotifyException
+     */
+    public function destroyPage(Page $page)
+    {
+        // Check if set as custom homepage & remove setting if not used or throw error if active
+        $customHome = setting('app-homepage', '0:');
+        if (intval($page->id) === intval(explode(':', $customHome)[0])) {
+            if (setting('app-homepage-type') === 'page') {
+                throw new NotifyException(trans('errors.page_custom_home_deletion'), $page->getUrl());
+            }
+            setting()->remove('app-homepage');
+        }
+
+        $this->destroyCommonRelations($page);
+
+        // Delete Attached Files
+        $attachmentService = app(AttachmentService::class);
+        foreach ($page->attachments as $attachment) {
+            $attachmentService->deleteFile($attachment);
+        }
+
+        $page->delete();
+    }
+
+    /**
+     * Remove a chapter from the system.
+     * @throws Exception
+     */
+    public function destroyChapter(Chapter $chapter)
+    {
+        if (count($chapter->pages) > 0) {
+            foreach ($chapter->pages as $page) {
+                $page->chapter_id = 0;
+                $page->save();
+            }
+        }
+
+        $this->destroyCommonRelations($chapter);
+        $chapter->delete();
+    }
+
+    /**
+     * Update entity relations to remove or update outstanding connections.
+     */
+    protected function destroyCommonRelations(Entity $entity)
+    {
+        Activity::removeEntity($entity);
+        $entity->views()->delete();
+        $entity->permissions()->delete();
+        $entity->tags()->delete();
+        $entity->comments()->delete();
+        $entity->jointPermissions()->delete();
+        $entity->searchTerms()->delete();
+
+        if ($entity instanceof HasCoverImage && $entity->cover) {
+            $imageService = app()->make(ImageService::class);
+            $imageService->destroy($entity->cover);
+        }
+    }
+}
index 1c2cc5cff69c29daa5385d963bc89ab2ad4612ff..76dc628fbf3f59e0bbbcf98897e715b743638213 100644 (file)
@@ -1,8 +1,25 @@
 <?php namespace BookStack\Entities;
 
 use BookStack\Uploads\Attachment;
-
-class Page extends Entity
+use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Database\Eloquent\Collection;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
+use Illuminate\Database\Eloquent\Relations\HasMany;
+use Permissions;
+
+/**
+ * Class Page
+ * @property int $chapter_id
+ * @property string $html
+ * @property string $markdown
+ * @property string $text
+ * @property bool $template
+ * @property bool $draft
+ * @property int $revision_count
+ * @property Chapter $chapter
+ * @property Collection $attachments
+ */
+class Page extends BookChild
 {
     protected $fillable = ['name', 'html', 'priority', 'markdown'];
 
@@ -11,12 +28,12 @@ class Page extends Entity
     public $textField = 'text';
 
     /**
-     * Get the morph class for this model.
-     * @return string
+     * Get the entities that are visible to the current user.
      */
-    public function getMorphClass()
+    public function scopeVisible(Builder $query)
     {
-        return 'BookStack\\Page';
+        $query = Permissions::enforceDraftVisiblityOnQuery($query);
+        return parent::scopeVisible($query);
     }
 
     /**
@@ -30,27 +47,17 @@ class Page extends Entity
         return $array;
     }
 
-    /**
-     * Get the book this page sits in.
-     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
-     */
-    public function book()
-    {
-        return $this->belongsTo(Book::class);
-    }
-
     /**
      * Get the parent item
-     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
      */
-    public function parent()
+    public function parent(): Entity
     {
-        return $this->chapter_id ? $this->chapter() : $this->book();
+        return $this->chapter_id ? $this->chapter : $this->book;
     }
 
     /**
      * Get the chapter that this page is in, If applicable.
-     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
+     * @return BelongsTo
      */
     public function chapter()
     {
@@ -72,12 +79,12 @@ class Page extends Entity
      */
     public function revisions()
     {
-        return $this->hasMany(PageRevision::class)->where('type', '=', 'version')->orderBy('created_at', 'desc');
+        return $this->hasMany(PageRevision::class)->where('type', '=', 'version')->orderBy('created_at', 'desc')->orderBy('id', 'desc');
     }
 
     /**
      * Get the attachments assigned to this page.
-     * @return \Illuminate\Database\Eloquent\Relations\HasMany
+     * @return HasMany
      */
     public function attachments()
     {
@@ -95,27 +102,17 @@ class Page extends Entity
         $midText = $this->draft ? '/draft/' : '/page/';
         $idComponent = $this->draft ? $this->id : urlencode($this->slug);
 
+        $url = '/books/' . urlencode($bookSlug) . $midText . $idComponent;
         if ($path !== false) {
-            return baseUrl('/books/' . urlencode($bookSlug) . $midText . $idComponent . '/' . trim($path, '/'));
+            $url .= '/' . trim($path, '/');
         }
 
-        return baseUrl('/books/' . urlencode($bookSlug) . $midText . $idComponent);
-    }
-
-    /**
-     * Return a generalised, common raw query that can be 'unioned' across entities.
-     * @param bool $withContent
-     * @return string
-     */
-    public function entityRawQuery($withContent = false)
-    {
-        $htmlQuery = $withContent ? 'html' : "'' as html";
-        return "'BookStack\\\\Page' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text, {$htmlQuery}, book_id, priority, chapter_id, draft, created_by, updated_by, updated_at, created_at";
+        return url($url);
     }
 
     /**
      * Get the current revision for the page if existing
-     * @return \BookStack\Entities\PageRevision|null
+     * @return PageRevision|null
      */
     public function getCurrentRevision()
     {
index d30147bfc52b1789f2faf4ecf89d8e3b50345c09..13dc713ba43be37453ca525f06e91f143793476e 100644 (file)
@@ -2,7 +2,21 @@
 
 use BookStack\Auth\User;
 use BookStack\Model;
+use Carbon\Carbon;
 
+/**
+ * Class PageRevision
+ * @property int $page_id
+ * @property string $slug
+ * @property string $book_slug
+ * @property int $created_by
+ * @property Carbon $created_at
+ * @property string $type
+ * @property string $summary
+ * @property string $markdown
+ * @property string $html
+ * @property int $revision_number
+ */
 class PageRevision extends Model
 {
     protected $fillable = ['name', 'html', 'text', 'markdown', 'summary'];
@@ -41,13 +55,18 @@ class PageRevision extends Model
 
     /**
      * Get the previous revision for the same page if existing
-     * @return \BookStack\PageRevision|null
+     * @return \BookStack\Entities\PageRevision|null
      */
     public function getPrevious()
     {
-        if ($id = static::where('page_id', '=', $this->page_id)->where('id', '<', $this->id)->max('id')) {
-            return static::find($id);
+        $id = static::newQuery()->where('page_id', '=', $this->page_id)
+            ->where('id', '<', $this->id)
+            ->max('id');
+
+        if ($id) {
+            return static::query()->find($id);
         }
+
         return null;
     }
 
diff --git a/app/Entities/Repos/BaseRepo.php b/app/Entities/Repos/BaseRepo.php
new file mode 100644 (file)
index 0000000..78ce505
--- /dev/null
@@ -0,0 +1,118 @@
+<?php
+
+namespace BookStack\Entities\Repos;
+
+use BookStack\Actions\TagRepo;
+use BookStack\Entities\Book;
+use BookStack\Entities\Entity;
+use BookStack\Entities\HasCoverImage;
+use BookStack\Exceptions\ImageUploadException;
+use BookStack\Uploads\ImageRepo;
+use Illuminate\Http\UploadedFile;
+use Illuminate\Support\Collection;
+
+class BaseRepo
+{
+
+    protected $tagRepo;
+    protected $imageRepo;
+
+
+    /**
+     * BaseRepo constructor.
+     * @param $tagRepo
+     */
+    public function __construct(TagRepo $tagRepo, ImageRepo $imageRepo)
+    {
+        $this->tagRepo = $tagRepo;
+        $this->imageRepo = $imageRepo;
+    }
+
+    /**
+     * Create a new entity in the system
+     */
+    public function create(Entity $entity, array $input)
+    {
+        $entity->fill($input);
+        $entity->forceFill([
+            'created_by' => user()->id,
+            'updated_by' => user()->id,
+        ]);
+        $entity->refreshSlug();
+        $entity->save();
+
+        if (isset($input['tags'])) {
+            $this->tagRepo->saveTagsToEntity($entity, $input['tags']);
+        }
+
+        $entity->rebuildPermissions();
+        $entity->indexForSearch();
+    }
+
+    /**
+     * Update the given entity.
+     */
+    public function update(Entity $entity, array $input)
+    {
+        $entity->fill($input);
+        $entity->updated_by = user()->id;
+
+        if ($entity->isDirty('name')) {
+            $entity->refreshSlug();
+        }
+
+        $entity->save();
+
+        if (isset($input['tags'])) {
+            $this->tagRepo->saveTagsToEntity($entity, $input['tags']);
+        }
+
+        $entity->rebuildPermissions();
+        $entity->indexForSearch();
+    }
+
+    /**
+     * Update the given items' cover image, or clear it.
+     * @throws ImageUploadException
+     * @throws \Exception
+     */
+    public function updateCoverImage(HasCoverImage $entity, UploadedFile $coverImage = null, bool $removeImage = false)
+    {
+        if ($coverImage) {
+            $this->imageRepo->destroyImage($entity->cover);
+            $image = $this->imageRepo->saveNew($coverImage, 'cover_book', $entity->id, 512, 512, true);
+            $entity->cover()->associate($image);
+        }
+
+        if ($removeImage) {
+            $this->imageRepo->destroyImage($entity->cover);
+            $entity->image_id = 0;
+            $entity->save();
+        }
+    }
+
+    /**
+     * Update the permissions of an entity.
+     */
+    public function updatePermissions(Entity $entity, bool $restricted, Collection $permissions = null)
+    {
+        $entity->restricted = $restricted;
+        $entity->permissions()->delete();
+
+        if (!is_null($permissions)) {
+            $entityPermissionData = $permissions->flatMap(function ($restrictions, $roleId) {
+                return collect($restrictions)->keys()->map(function ($action) use ($roleId) {
+                    return [
+                        'role_id' => $roleId,
+                        'action' => strtolower($action),
+                    ] ;
+                });
+            });
+
+            $entity->permissions()->createMany($entityPermissionData);
+        }
+
+        $entity->save();
+        $entity->rebuildPermissions();
+    }
+}
diff --git a/app/Entities/Repos/BookRepo.php b/app/Entities/Repos/BookRepo.php
new file mode 100644 (file)
index 0000000..7fcc80f
--- /dev/null
@@ -0,0 +1,134 @@
+<?php namespace BookStack\Entities\Repos;
+
+use BookStack\Actions\TagRepo;
+use BookStack\Entities\Book;
+use BookStack\Entities\Managers\TrashCan;
+use BookStack\Exceptions\ImageUploadException;
+use BookStack\Exceptions\NotFoundException;
+use BookStack\Exceptions\NotifyException;
+use BookStack\Uploads\ImageRepo;
+use Exception;
+use Illuminate\Contracts\Container\BindingResolutionException;
+use Illuminate\Contracts\Pagination\LengthAwarePaginator;
+use Illuminate\Http\UploadedFile;
+use Illuminate\Support\Collection;
+
+class BookRepo
+{
+
+    protected $baseRepo;
+    protected $tagRepo;
+    protected $imageRepo;
+
+    /**
+     * BookRepo constructor.
+     * @param $tagRepo
+     */
+    public function __construct(BaseRepo $baseRepo, TagRepo $tagRepo, ImageRepo $imageRepo)
+    {
+        $this->baseRepo = $baseRepo;
+        $this->tagRepo = $tagRepo;
+        $this->imageRepo = $imageRepo;
+    }
+
+    /**
+     * Get all books in a paginated format.
+     */
+    public function getAllPaginated(int $count = 20, string $sort = 'name', string $order = 'asc'): LengthAwarePaginator
+    {
+        return Book::visible()->orderBy($sort, $order)->paginate($count);
+    }
+
+    /**
+     * Get the books that were most recently viewed by this user.
+     */
+    public function getRecentlyViewed(int $count = 20): Collection
+    {
+        return Book::visible()->withLastView()
+            ->having('last_viewed_at', '>', 0)
+            ->orderBy('last_viewed_at', 'desc')
+            ->take($count)->get();
+    }
+
+    /**
+     * Get the most popular books in the system.
+     */
+    public function getPopular(int $count = 20): Collection
+    {
+        return Book::visible()->withViewCount()
+            ->having('view_count', '>', 0)
+            ->orderBy('view_count', 'desc')
+            ->take($count)->get();
+    }
+
+    /**
+     * Get the most recently created books from the system.
+     */
+    public function getRecentlyCreated(int $count = 20): Collection
+    {
+        return Book::visible()->orderBy('created_at', 'desc')
+            ->take($count)->get();
+    }
+
+    /**
+     * Get a book by its slug.
+     */
+    public function getBySlug(string $slug): Book
+    {
+        $book = Book::visible()->where('slug', '=', $slug)->first();
+
+        if ($book === null) {
+            throw new NotFoundException(trans('errors.book_not_found'));
+        }
+
+        return $book;
+    }
+
+    /**
+     * Create a new book in the system
+     */
+    public function create(array $input): Book
+    {
+        $book = new Book();
+        $this->baseRepo->create($book, $input);
+        return $book;
+    }
+
+    /**
+     * Update the given book.
+     */
+    public function update(Book $book, array $input): Book
+    {
+        $this->baseRepo->update($book, $input);
+        return $book;
+    }
+
+    /**
+     * Update the given book's cover image, or clear it.
+     * @throws ImageUploadException
+     * @throws Exception
+     */
+    public function updateCoverImage(Book $book, UploadedFile $coverImage = null, bool $removeImage = false)
+    {
+        $this->baseRepo->updateCoverImage($book, $coverImage, $removeImage);
+    }
+
+    /**
+     * Update the permissions of a book.
+     */
+    public function updatePermissions(Book $book, bool $restricted, Collection $permissions = null)
+    {
+        $this->baseRepo->updatePermissions($book, $restricted, $permissions);
+    }
+
+    /**
+     * Remove a book from the system.
+     * @throws NotifyException
+     * @throws BindingResolutionException
+     */
+    public function destroy(Book $book)
+    {
+        $trashCan = new TrashCan();
+        $trashCan->destroyBook($book);
+    }
+}
diff --git a/app/Entities/Repos/BookshelfRepo.php b/app/Entities/Repos/BookshelfRepo.php
new file mode 100644 (file)
index 0000000..ab4a518
--- /dev/null
@@ -0,0 +1,173 @@
+<?php namespace BookStack\Entities\Repos;
+
+use BookStack\Entities\Book;
+use BookStack\Entities\Bookshelf;
+use BookStack\Entities\Managers\TrashCan;
+use BookStack\Exceptions\ImageUploadException;
+use BookStack\Exceptions\NotFoundException;
+use Exception;
+use Illuminate\Contracts\Pagination\LengthAwarePaginator;
+use Illuminate\Http\UploadedFile;
+use Illuminate\Support\Collection;
+
+class BookshelfRepo
+{
+    protected $baseRepo;
+
+    /**
+     * BookshelfRepo constructor.
+     * @param $baseRepo
+     */
+    public function __construct(BaseRepo $baseRepo)
+    {
+        $this->baseRepo = $baseRepo;
+    }
+
+    /**
+     * Get all bookshelves in a paginated format.
+     */
+    public function getAllPaginated(int $count = 20, string $sort = 'name', string $order = 'asc'): LengthAwarePaginator
+    {
+        return Bookshelf::visible()->with('visibleBooks')
+            ->orderBy($sort, $order)->paginate($count);
+    }
+
+    /**
+     * Get the bookshelves that were most recently viewed by this user.
+     */
+    public function getRecentlyViewed(int $count = 20): Collection
+    {
+        return Bookshelf::visible()->withLastView()
+            ->having('last_viewed_at', '>', 0)
+            ->orderBy('last_viewed_at', 'desc')
+            ->take($count)->get();
+    }
+
+    /**
+     * Get the most popular bookshelves in the system.
+     */
+    public function getPopular(int $count = 20): Collection
+    {
+        return Bookshelf::visible()->withViewCount()
+            ->having('view_count', '>', 0)
+            ->orderBy('view_count', 'desc')
+            ->take($count)->get();
+    }
+
+    /**
+     * Get the most recently created bookshelves from the system.
+     */
+    public function getRecentlyCreated(int $count = 20): Collection
+    {
+        return Bookshelf::visible()->orderBy('created_at', 'desc')
+            ->take($count)->get();
+    }
+
+    /**
+     * Get a shelf by its slug.
+     */
+    public function getBySlug(string $slug): Bookshelf
+    {
+        $shelf = Bookshelf::visible()->where('slug', '=', $slug)->first();
+
+        if ($shelf === null) {
+            throw new NotFoundException(trans('errors.bookshelf_not_found'));
+        }
+
+        return $shelf;
+    }
+
+    /**
+     * Create a new shelf in the system.
+     */
+    public function create(array $input, array $bookIds): Bookshelf
+    {
+        $shelf = new Bookshelf();
+        $this->baseRepo->create($shelf, $input);
+        $this->updateBooks($shelf, $bookIds);
+        return $shelf;
+    }
+
+    /**
+     * Create a new shelf in the system.
+     */
+    public function update(Bookshelf $shelf, array $input, array $bookIds): Bookshelf
+    {
+        $this->baseRepo->update($shelf, $input);
+        $this->updateBooks($shelf, $bookIds);
+        return $shelf;
+    }
+
+    /**
+     * Update which books are assigned to this shelf by
+     * syncing the given book ids.
+     * Function ensures the books are visible to the current user and existing.
+     */
+    protected function updateBooks(Bookshelf $shelf, array $bookIds)
+    {
+        $numericIDs = collect($bookIds)->map(function ($id) {
+            return intval($id);
+        });
+
+        $syncData = Book::visible()
+            ->whereIn('id', $bookIds)
+            ->get(['id'])->pluck('id')->mapWithKeys(function ($bookId) use ($numericIDs) {
+                return [$bookId => ['order' => $numericIDs->search($bookId)]];
+            });
+
+        $shelf->books()->sync($syncData);
+    }
+
+    /**
+     * Update the given shelf cover image, or clear it.
+     * @throws ImageUploadException
+     * @throws Exception
+     */
+    public function updateCoverImage(Bookshelf $shelf, UploadedFile $coverImage = null, bool $removeImage = false)
+    {
+        $this->baseRepo->updateCoverImage($shelf, $coverImage, $removeImage);
+    }
+
+    /**
+     * Update the permissions of a bookshelf.
+     */
+    public function updatePermissions(Bookshelf $shelf, bool $restricted, Collection $permissions = null)
+    {
+        $this->baseRepo->updatePermissions($shelf, $restricted, $permissions);
+    }
+
+    /**
+     * Copy down the permissions of the given shelf to all child books.
+     */
+    public function copyDownPermissions(Bookshelf $shelf): int
+    {
+        $shelfPermissions = $shelf->permissions()->get(['role_id', 'action'])->toArray();
+        $shelfBooks = $shelf->books()->get();
+        $updatedBookCount = 0;
+
+        /** @var Book $book */
+        foreach ($shelfBooks as $book) {
+            if (!userCan('restrictions-manage', $book)) {
+                continue;
+            }
+            $book->permissions()->delete();
+            $book->restricted = $shelf->restricted;
+            $book->permissions()->createMany($shelfPermissions);
+            $book->save();
+            $book->rebuildPermissions();
+            $updatedBookCount++;
+        }
+
+        return $updatedBookCount;
+    }
+
+    /**
+     * Remove a bookshelf from the system.
+     * @throws Exception
+     */
+    public function destroy(Bookshelf $shelf)
+    {
+        $trashCan = new TrashCan();
+        $trashCan->destroyShelf($shelf);
+    }
+}
diff --git a/app/Entities/Repos/ChapterRepo.php b/app/Entities/Repos/ChapterRepo.php
new file mode 100644 (file)
index 0000000..c6f3a2d
--- /dev/null
@@ -0,0 +1,108 @@
+<?php namespace BookStack\Entities\Repos;
+
+use BookStack\Entities\Book;
+use BookStack\Entities\Chapter;
+use BookStack\Entities\Managers\BookContents;
+use BookStack\Entities\Managers\TrashCan;
+use BookStack\Exceptions\MoveOperationException;
+use BookStack\Exceptions\NotFoundException;
+use BookStack\Exceptions\NotifyException;
+use Exception;
+use Illuminate\Contracts\Container\BindingResolutionException;
+use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Support\Collection;
+
+class ChapterRepo
+{
+
+    protected $baseRepo;
+
+    /**
+     * ChapterRepo constructor.
+     * @param $baseRepo
+     */
+    public function __construct(BaseRepo $baseRepo)
+    {
+        $this->baseRepo = $baseRepo;
+    }
+
+    /**
+     * Get a chapter via the slug.
+     * @throws NotFoundException
+     */
+    public function getBySlug(string $bookSlug, string $chapterSlug): Chapter
+    {
+        $chapter = Chapter::visible()->whereSlugs($bookSlug, $chapterSlug)->first();
+
+        if ($chapter === null) {
+            throw new NotFoundException(trans('errors.chapter_not_found'));
+        }
+
+        return $chapter;
+    }
+
+    /**
+     * Create a new chapter in the system.
+     */
+    public function create(array $input, Book $parentBook): Chapter
+    {
+        $chapter = new Chapter();
+        $chapter->book_id = $parentBook->id;
+        $chapter->priority = (new BookContents($parentBook))->getLastPriority() + 1;
+        $this->baseRepo->create($chapter, $input);
+        return $chapter;
+    }
+
+    /**
+     * Update the given chapter.
+     */
+    public function update(Chapter $chapter, array $input): Chapter
+    {
+        $this->baseRepo->update($chapter, $input);
+        return $chapter;
+    }
+
+    /**
+     * Update the permissions of a chapter.
+     */
+    public function updatePermissions(Chapter $chapter, bool $restricted, Collection $permissions = null)
+    {
+        $this->baseRepo->updatePermissions($chapter, $restricted, $permissions);
+    }
+
+    /**
+     * Remove a chapter from the system.
+     * @throws Exception
+     */
+    public function destroy(Chapter $chapter)
+    {
+        $trashCan = new TrashCan();
+        $trashCan->destroyChapter($chapter);
+    }
+
+    /**
+     * Move the given chapter into a new parent book.
+     * The $parentIdentifier must be a string of the following format:
+     * 'book:<id>' (book:5)
+     * @throws MoveOperationException
+     */
+    public function move(Chapter $chapter, string $parentIdentifier): Book
+    {
+        $stringExploded = explode(':', $parentIdentifier);
+        $entityType = $stringExploded[0];
+        $entityId = intval($stringExploded[1]);
+
+        if ($entityType !== 'book') {
+            throw new MoveOperationException('Chapters can only be moved into books');
+        }
+
+        $parent = Book::visible()->where('id', '=', $entityId)->first();
+        if ($parent === null) {
+            throw new MoveOperationException('Book to move chapter into not found');
+        }
+
+        $chapter->changeBook($parent->id);
+        $chapter->rebuildPermissions();
+        return $parent;
+    }
+}
diff --git a/app/Entities/Repos/EntityRepo.php b/app/Entities/Repos/EntityRepo.php
deleted file mode 100644 (file)
index a093453..0000000
+++ /dev/null
@@ -1,915 +0,0 @@
-<?php namespace BookStack\Entities\Repos;
-
-use Activity;
-use BookStack\Actions\TagRepo;
-use BookStack\Actions\ViewService;
-use BookStack\Auth\Permissions\PermissionService;
-use BookStack\Auth\User;
-use BookStack\Entities\Book;
-use BookStack\Entities\Bookshelf;
-use BookStack\Entities\Chapter;
-use BookStack\Entities\Entity;
-use BookStack\Entities\EntityProvider;
-use BookStack\Entities\Page;
-use BookStack\Entities\SearchService;
-use BookStack\Exceptions\NotFoundException;
-use BookStack\Exceptions\NotifyException;
-use BookStack\Uploads\AttachmentService;
-use DOMDocument;
-use DOMNode;
-use DOMXPath;
-use Illuminate\Contracts\Pagination\LengthAwarePaginator;
-use Illuminate\Database\Eloquent\Builder;
-use Illuminate\Http\Request;
-use Illuminate\Support\Collection;
-use Throwable;
-
-class EntityRepo
-{
-
-    /**
-     * @var EntityProvider
-     */
-    protected $entityProvider;
-
-    /**
-     * @var PermissionService
-     */
-    protected $permissionService;
-
-    /**
-     * @var ViewService
-     */
-    protected $viewService;
-
-    /**
-     * @var TagRepo
-     */
-    protected $tagRepo;
-
-    /**
-     * @var SearchService
-     */
-    protected $searchService;
-
-    /**
-     * EntityRepo constructor.
-     * @param EntityProvider $entityProvider
-     * @param ViewService $viewService
-     * @param PermissionService $permissionService
-     * @param TagRepo $tagRepo
-     * @param SearchService $searchService
-     */
-    public function __construct(
-        EntityProvider $entityProvider,
-        ViewService $viewService,
-        PermissionService $permissionService,
-        TagRepo $tagRepo,
-        SearchService $searchService
-    ) {
-        $this->entityProvider = $entityProvider;
-        $this->viewService = $viewService;
-        $this->permissionService = $permissionService;
-        $this->tagRepo = $tagRepo;
-        $this->searchService = $searchService;
-    }
-
-    /**
-     * Base query for searching entities via permission system
-     * @param string $type
-     * @param bool $allowDrafts
-     * @param string $permission
-     * @return \Illuminate\Database\Query\Builder
-     */
-    protected function entityQuery($type, $allowDrafts = false, $permission = 'view')
-    {
-        $q = $this->permissionService->enforceEntityRestrictions($type, $this->entityProvider->get($type), $permission);
-        if (strtolower($type) === 'page' && !$allowDrafts) {
-            $q = $q->where('draft', '=', false);
-        }
-        return $q;
-    }
-
-    /**
-     * Check if an entity with the given id exists.
-     * @param $type
-     * @param $id
-     * @return bool
-     */
-    public function exists($type, $id)
-    {
-        return $this->entityQuery($type)->where('id', '=', $id)->exists();
-    }
-
-    /**
-     * Get an entity by ID
-     * @param string $type
-     * @param integer $id
-     * @param bool $allowDrafts
-     * @param bool $ignorePermissions
-     * @return Entity
-     */
-    public function getById($type, $id, $allowDrafts = false, $ignorePermissions = false)
-    {
-        $query = $this->entityQuery($type, $allowDrafts);
-
-        if ($ignorePermissions) {
-            $query = $this->entityProvider->get($type)->newQuery();
-        }
-
-        return $query->find($id);
-    }
-
-    /**
-     * @param string $type
-     * @param []int $ids
-     * @param bool $allowDrafts
-     * @param bool $ignorePermissions
-     * @return Builder[]|\Illuminate\Database\Eloquent\Collection|Collection
-     */
-    public function getManyById($type, $ids, $allowDrafts = false, $ignorePermissions = false)
-    {
-        $query = $this->entityQuery($type, $allowDrafts);
-
-        if ($ignorePermissions) {
-            $query = $this->entityProvider->get($type)->newQuery();
-        }
-
-        return $query->whereIn('id', $ids)->get();
-    }
-
-    /**
-     * Get an entity by its url slug.
-     * @param string $type
-     * @param string $slug
-     * @param string|bool $bookSlug
-     * @return Entity
-     * @throws NotFoundException
-     */
-    public function getBySlug($type, $slug, $bookSlug = false)
-    {
-        $q = $this->entityQuery($type)->where('slug', '=', $slug);
-
-        if (strtolower($type) === 'chapter' || strtolower($type) === 'page') {
-            $q = $q->where('book_id', '=', function ($query) use ($bookSlug) {
-                $query->select('id')
-                    ->from($this->entityProvider->book->getTable())
-                    ->where('slug', '=', $bookSlug)->limit(1);
-            });
-        }
-        $entity = $q->first();
-        if ($entity === null) {
-            throw new NotFoundException(trans('errors.' . strtolower($type) . '_not_found'));
-        }
-        return $entity;
-    }
-
-
-    /**
-     * Get all entities of a type with the given permission, limited by count unless count is false.
-     * @param string $type
-     * @param integer|bool $count
-     * @param string $permission
-     * @return Collection
-     */
-    public function getAll($type, $count = 20, $permission = 'view')
-    {
-        $q = $this->entityQuery($type, false, $permission)->orderBy('name', 'asc');
-        if ($count !== false) {
-            $q = $q->take($count);
-        }
-        return $q->get();
-    }
-
-    /**
-     * Get all entities in a paginated format
-     * @param $type
-     * @param int $count
-     * @param string $sort
-     * @param string $order
-     * @param null|callable $queryAddition
-     * @return LengthAwarePaginator
-     */
-    public function getAllPaginated($type, int $count = 10, string $sort = 'name', string $order = 'asc', $queryAddition = null)
-    {
-        $query = $this->entityQuery($type);
-        $query = $this->addSortToQuery($query, $sort, $order);
-        if ($queryAddition) {
-            $queryAddition($query);
-        }
-        return $query->paginate($count);
-    }
-
-    /**
-     * Add sorting operations to an entity query.
-     * @param Builder $query
-     * @param string $sort
-     * @param string $order
-     * @return Builder
-     */
-    protected function addSortToQuery(Builder $query, string $sort = 'name', string $order = 'asc')
-    {
-        $order = ($order === 'asc') ? 'asc' : 'desc';
-        $propertySorts = ['name', 'created_at', 'updated_at'];
-
-        if (in_array($sort, $propertySorts)) {
-            return $query->orderBy($sort, $order);
-        }
-
-        return $query;
-    }
-
-    /**
-     * Get the most recently created entities of the given type.
-     * @param string $type
-     * @param int $count
-     * @param int $page
-     * @param bool|callable $additionalQuery
-     * @return Collection
-     */
-    public function getRecentlyCreated($type, $count = 20, $page = 0, $additionalQuery = false)
-    {
-        $query = $this->permissionService->enforceEntityRestrictions($type, $this->entityProvider->get($type))
-            ->orderBy('created_at', 'desc');
-        if (strtolower($type) === 'page') {
-            $query = $query->where('draft', '=', false);
-        }
-        if ($additionalQuery !== false && is_callable($additionalQuery)) {
-            $additionalQuery($query);
-        }
-        return $query->skip($page * $count)->take($count)->get();
-    }
-
-    /**
-     * Get the most recently updated entities of the given type.
-     * @param string $type
-     * @param int $count
-     * @param int $page
-     * @param bool|callable $additionalQuery
-     * @return Collection
-     */
-    public function getRecentlyUpdated($type, $count = 20, $page = 0, $additionalQuery = false)
-    {
-        $query = $this->permissionService->enforceEntityRestrictions($type, $this->entityProvider->get($type))
-            ->orderBy('updated_at', 'desc');
-        if (strtolower($type) === 'page') {
-            $query = $query->where('draft', '=', false);
-        }
-        if ($additionalQuery !== false && is_callable($additionalQuery)) {
-            $additionalQuery($query);
-        }
-        return $query->skip($page * $count)->take($count)->get();
-    }
-
-    /**
-     * Get the most recently viewed entities.
-     * @param string|bool $type
-     * @param int $count
-     * @param int $page
-     * @return mixed
-     */
-    public function getRecentlyViewed($type, $count = 10, $page = 0)
-    {
-        $filter = is_bool($type) ? false : $this->entityProvider->get($type);
-        return $this->viewService->getUserRecentlyViewed($count, $page, $filter);
-    }
-
-    /**
-     * Get the latest pages added to the system with pagination.
-     * @param string $type
-     * @param int $count
-     * @return mixed
-     */
-    public function getRecentlyCreatedPaginated($type, $count = 20)
-    {
-        return $this->entityQuery($type)->orderBy('created_at', 'desc')->paginate($count);
-    }
-
-    /**
-     * Get the latest pages added to the system with pagination.
-     * @param string $type
-     * @param int $count
-     * @return mixed
-     */
-    public function getRecentlyUpdatedPaginated($type, $count = 20)
-    {
-        return $this->entityQuery($type)->orderBy('updated_at', 'desc')->paginate($count);
-    }
-
-    /**
-     * Get the most popular entities base on all views.
-     * @param string $type
-     * @param int $count
-     * @param int $page
-     * @return mixed
-     */
-    public function getPopular(string $type, int $count = 10, int $page = 0)
-    {
-        return $this->viewService->getPopular($count, $page, $type);
-    }
-
-    /**
-     * Get draft pages owned by the current user.
-     * @param int $count
-     * @param int $page
-     * @return Collection
-     */
-    public function getUserDraftPages($count = 20, $page = 0)
-    {
-        return $this->entityProvider->page->where('draft', '=', true)
-            ->where('created_by', '=', user()->id)
-            ->orderBy('updated_at', 'desc')
-            ->skip($count * $page)->take($count)->get();
-    }
-
-    /**
-     * Get the number of entities the given user has created.
-     * @param string $type
-     * @param User $user
-     * @return int
-     */
-    public function getUserTotalCreated(string $type, User $user)
-    {
-        return $this->entityProvider->get($type)
-            ->where('created_by', '=', $user->id)->count();
-    }
-
-    /**
-     * Get the child items for a chapter sorted by priority but
-     * with draft items floated to the top.
-     * @param Bookshelf $bookshelf
-     * @return \Illuminate\Database\Eloquent\Collection|static[]
-     */
-    public function getBookshelfChildren(Bookshelf $bookshelf)
-    {
-        return $this->permissionService->enforceEntityRestrictions('book', $bookshelf->books())->get();
-    }
-
-    /**
-     * Get the direct children of a book.
-     * @param Book $book
-     * @return \Illuminate\Database\Eloquent\Collection
-     */
-    public function getBookDirectChildren(Book $book)
-    {
-        $pages = $this->permissionService->enforceEntityRestrictions('page', $book->directPages())->get();
-        $chapters = $this->permissionService->enforceEntityRestrictions('chapters', $book->chapters())->get();
-        return collect()->concat($pages)->concat($chapters)->sortBy('priority')->sortByDesc('draft');
-    }
-
-    /**
-     * Get all child objects of a book.
-     * Returns a sorted collection of Pages and Chapters.
-     * Loads the book slug onto child elements to prevent access database access for getting the slug.
-     * @param Book $book
-     * @param bool $filterDrafts
-     * @param bool $renderPages
-     * @return mixed
-     */
-    public function getBookChildren(Book $book, $filterDrafts = false, $renderPages = false)
-    {
-        $q = $this->permissionService->bookChildrenQuery($book->id, $filterDrafts, $renderPages)->get();
-        $entities = [];
-        $parents = [];
-        $tree = [];
-
-        foreach ($q as $index => $rawEntity) {
-            if ($rawEntity->entity_type ===  $this->entityProvider->page->getMorphClass()) {
-                $entities[$index] = $this->entityProvider->page->newFromBuilder($rawEntity);
-                if ($renderPages) {
-                    $entities[$index]->html = $rawEntity->html;
-                    $entities[$index]->html = $this->renderPage($entities[$index]);
-                };
-            } else if ($rawEntity->entity_type === $this->entityProvider->chapter->getMorphClass()) {
-                $entities[$index] = $this->entityProvider->chapter->newFromBuilder($rawEntity);
-                $key = $entities[$index]->entity_type . ':' . $entities[$index]->id;
-                $parents[$key] = $entities[$index];
-                $parents[$key]->setAttribute('pages', collect());
-            }
-            if ($entities[$index]->chapter_id === 0 || $entities[$index]->chapter_id === '0') {
-                $tree[] = $entities[$index];
-            }
-            $entities[$index]->book = $book;
-        }
-
-        foreach ($entities as $entity) {
-            if ($entity->chapter_id === 0 || $entity->chapter_id === '0') {
-                continue;
-            }
-            $parentKey = $this->entityProvider->chapter->getMorphClass() . ':' . $entity->chapter_id;
-            if (!isset($parents[$parentKey])) {
-                $tree[] = $entity;
-                continue;
-            }
-            $chapter = $parents[$parentKey];
-            $chapter->pages->push($entity);
-        }
-
-        return collect($tree);
-    }
-
-    /**
-     * Get the child items for a chapter sorted by priority but
-     * with draft items floated to the top.
-     * @param Chapter $chapter
-     * @return \Illuminate\Database\Eloquent\Collection|static[]
-     */
-    public function getChapterChildren(Chapter $chapter)
-    {
-        return $this->permissionService->enforceEntityRestrictions('page', $chapter->pages())
-            ->orderBy('draft', 'DESC')->orderBy('priority', 'ASC')->get();
-    }
-
-
-    /**
-     * Get the next sequential priority for a new child element in the given book.
-     * @param Book $book
-     * @return int
-     */
-    public function getNewBookPriority(Book $book)
-    {
-        $lastElem = $this->getBookChildren($book)->pop();
-        return $lastElem ? $lastElem->priority + 1 : 0;
-    }
-
-    /**
-     * Get a new priority for a new page to be added to the given chapter.
-     * @param Chapter $chapter
-     * @return int
-     */
-    public function getNewChapterPriority(Chapter $chapter)
-    {
-        $lastPage = $chapter->pages('DESC')->first();
-        return $lastPage !== null ? $lastPage->priority + 1 : 0;
-    }
-
-    /**
-     * Find a suitable slug for an entity.
-     * @param string $type
-     * @param string $name
-     * @param bool|integer $currentId
-     * @param bool|integer $bookId Only pass if type is not a book
-     * @return string
-     */
-    public function findSuitableSlug($type, $name, $currentId = false, $bookId = false)
-    {
-        $slug = $this->nameToSlug($name);
-        while ($this->slugExists($type, $slug, $currentId, $bookId)) {
-            $slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
-        }
-        return $slug;
-    }
-
-    /**
-     * Check if a slug already exists in the database.
-     * @param string $type
-     * @param string $slug
-     * @param bool|integer $currentId
-     * @param bool|integer $bookId
-     * @return bool
-     */
-    protected function slugExists($type, $slug, $currentId = false, $bookId = false)
-    {
-        $query = $this->entityProvider->get($type)->where('slug', '=', $slug);
-        if (strtolower($type) === 'page' || strtolower($type) === 'chapter') {
-            $query = $query->where('book_id', '=', $bookId);
-        }
-        if ($currentId) {
-            $query = $query->where('id', '!=', $currentId);
-        }
-        return $query->count() > 0;
-    }
-
-    /**
-     * Updates entity restrictions from a request
-     * @param Request $request
-     * @param Entity $entity
-     * @throws Throwable
-     */
-    public function updateEntityPermissionsFromRequest(Request $request, Entity $entity)
-    {
-        $entity->restricted = $request->get('restricted', '') === 'true';
-        $entity->permissions()->delete();
-
-        if ($request->filled('restrictions')) {
-            foreach ($request->get('restrictions') as $roleId => $restrictions) {
-                foreach ($restrictions as $action => $value) {
-                    $entity->permissions()->create([
-                        'role_id' => $roleId,
-                        'action'  => strtolower($action)
-                    ]);
-                }
-            }
-        }
-
-        $entity->save();
-        $this->permissionService->buildJointPermissionsForEntity($entity);
-    }
-
-
-
-    /**
-     * Create a new entity from request input.
-     * Used for books and chapters.
-     * @param string $type
-     * @param array $input
-     * @param bool|Book $book
-     * @return Entity
-     */
-    public function createFromInput($type, $input = [], $book = false)
-    {
-        $isChapter = strtolower($type) === 'chapter';
-        $entityModel = $this->entityProvider->get($type)->newInstance($input);
-        $entityModel->slug = $this->findSuitableSlug($type, $entityModel->name, false, $isChapter ? $book->id : false);
-        $entityModel->created_by = user()->id;
-        $entityModel->updated_by = user()->id;
-        $isChapter ? $book->chapters()->save($entityModel) : $entityModel->save();
-
-        if (isset($input['tags'])) {
-            $this->tagRepo->saveTagsToEntity($entityModel, $input['tags']);
-        }
-
-        $this->permissionService->buildJointPermissionsForEntity($entityModel);
-        $this->searchService->indexEntity($entityModel);
-        return $entityModel;
-    }
-
-    /**
-     * Update entity details from request input.
-     * Used for books and chapters
-     * @param string $type
-     * @param Entity $entityModel
-     * @param array $input
-     * @return Entity
-     */
-    public function updateFromInput($type, Entity $entityModel, $input = [])
-    {
-        if ($entityModel->name !== $input['name']) {
-            $entityModel->slug = $this->findSuitableSlug($type, $input['name'], $entityModel->id);
-        }
-        $entityModel->fill($input);
-        $entityModel->updated_by = user()->id;
-        $entityModel->save();
-
-        if (isset($input['tags'])) {
-            $this->tagRepo->saveTagsToEntity($entityModel, $input['tags']);
-        }
-
-        $this->permissionService->buildJointPermissionsForEntity($entityModel);
-        $this->searchService->indexEntity($entityModel);
-        return $entityModel;
-    }
-
-    /**
-     * Sync the books assigned to a shelf from a comma-separated list
-     * of book IDs.
-     * @param Bookshelf $shelf
-     * @param string $books
-     */
-    public function updateShelfBooks(Bookshelf $shelf, string $books)
-    {
-        $ids = explode(',', $books);
-
-        // Check books exist and match ordering
-        $bookIds = $this->entityQuery('book')->whereIn('id', $ids)->get(['id'])->pluck('id');
-        $syncData = [];
-        foreach ($ids as $index => $id) {
-            if ($bookIds->contains($id)) {
-                $syncData[$id] = ['order' => $index];
-            }
-        }
-
-        $shelf->books()->sync($syncData);
-    }
-
-    /**
-     * Append a Book to a BookShelf.
-     * @param Bookshelf $shelf
-     * @param Book $book
-     */
-    public function appendBookToShelf(Bookshelf $shelf, Book $book)
-    {
-        if ($shelf->contains($book)) {
-            return;
-        }
-
-        $maxOrder = $shelf->books()->max('order');
-        $shelf->books()->attach($book->id, ['order' => $maxOrder + 1]);
-    }
-
-    /**
-     * Change the book that an entity belongs to.
-     * @param string $type
-     * @param integer $newBookId
-     * @param Entity $entity
-     * @param bool $rebuildPermissions
-     * @return Entity
-     */
-    public function changeBook($type, $newBookId, Entity $entity, $rebuildPermissions = false)
-    {
-        $entity->book_id = $newBookId;
-        // Update related activity
-        foreach ($entity->activity as $activity) {
-            $activity->book_id = $newBookId;
-            $activity->save();
-        }
-        $entity->slug = $this->findSuitableSlug($type, $entity->name, $entity->id, $newBookId);
-        $entity->save();
-
-        // Update all child pages if a chapter
-        if (strtolower($type) === 'chapter') {
-            foreach ($entity->pages as $page) {
-                $this->changeBook('page', $newBookId, $page, false);
-            }
-        }
-
-        // Update permissions if applicable
-        if ($rebuildPermissions) {
-            $entity->load('book');
-            $this->permissionService->buildJointPermissionsForEntity($entity->book);
-        }
-
-        return $entity;
-    }
-
-    /**
-     * Alias method to update the book jointPermissions in the PermissionService.
-     * @param Book $book
-     */
-    public function buildJointPermissionsForBook(Book $book)
-    {
-        $this->permissionService->buildJointPermissionsForEntity($book);
-    }
-
-    /**
-     * Format a name as a url slug.
-     * @param $name
-     * @return string
-     */
-    protected function nameToSlug($name)
-    {
-        $slug = preg_replace('/[\+\/\\\?\@\}\{\.\,\=\[\]\#\&\!\*\'\;\:\$\%]/', '', mb_strtolower($name));
-        $slug = preg_replace('/\s{2,}/', ' ', $slug);
-        $slug = str_replace(' ', '-', $slug);
-        if ($slug === "") {
-            $slug = substr(md5(rand(1, 500)), 0, 5);
-        }
-        return $slug;
-    }
-
-    /**
-     * Render the page for viewing
-     * @param Page $page
-     * @param bool $blankIncludes
-     * @return string
-     */
-    public function renderPage(Page $page, bool $blankIncludes = false) : string
-    {
-        $content = $page->html;
-
-        if (!config('app.allow_content_scripts')) {
-            $content = $this->escapeScripts($content);
-        }
-
-        if ($blankIncludes) {
-            $content = $this->blankPageIncludes($content);
-        } else {
-            $content = $this->parsePageIncludes($content);
-        }
-
-        return $content;
-    }
-
-    /**
-     * Remove any page include tags within the given HTML.
-     * @param string $html
-     * @return string
-     */
-    protected function blankPageIncludes(string $html) : string
-    {
-        return preg_replace("/{{@\s?([0-9].*?)}}/", '', $html);
-    }
-
-    /**
-     * Parse any include tags "{{@<page_id>#section}}" to be part of the page.
-     * @param string $html
-     * @return mixed|string
-     */
-    protected function parsePageIncludes(string $html) : string
-    {
-        $matches = [];
-        preg_match_all("/{{@\s?([0-9].*?)}}/", $html, $matches);
-
-        $topLevelTags = ['table', 'ul', 'ol'];
-        foreach ($matches[1] as $index => $includeId) {
-            $splitInclude = explode('#', $includeId, 2);
-            $pageId = intval($splitInclude[0]);
-            if (is_nan($pageId)) {
-                continue;
-            }
-
-            $matchedPage = $this->getById('page', $pageId);
-            if ($matchedPage === null) {
-                $html = str_replace($matches[0][$index], '', $html);
-                continue;
-            }
-
-            if (count($splitInclude) === 1) {
-                $html = str_replace($matches[0][$index], $matchedPage->html, $html);
-                continue;
-            }
-
-            $doc = new DOMDocument();
-            libxml_use_internal_errors(true);
-            $doc->loadHTML(mb_convert_encoding('<body>'.$matchedPage->html.'</body>', 'HTML-ENTITIES', 'UTF-8'));
-            $matchingElem = $doc->getElementById($splitInclude[1]);
-            if ($matchingElem === null) {
-                $html = str_replace($matches[0][$index], '', $html);
-                continue;
-            }
-            $innerContent = '';
-            $isTopLevel = in_array(strtolower($matchingElem->nodeName), $topLevelTags);
-            if ($isTopLevel) {
-                $innerContent .= $doc->saveHTML($matchingElem);
-            } else {
-                foreach ($matchingElem->childNodes as $childNode) {
-                    $innerContent .= $doc->saveHTML($childNode);
-                }
-            }
-            libxml_clear_errors();
-            $html = str_replace($matches[0][$index], trim($innerContent), $html);
-        }
-
-        return $html;
-    }
-
-    /**
-     * Escape script tags within HTML content.
-     * @param string $html
-     * @return string
-     */
-    protected function escapeScripts(string $html) : string
-    {
-        if ($html == '') {
-            return $html;
-        }
-
-        libxml_use_internal_errors(true);
-        $doc = new DOMDocument();
-        $doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
-        $xPath = new DOMXPath($doc);
-
-        // Remove standard script tags
-        $scriptElems = $xPath->query('//body//*//script');
-        foreach ($scriptElems as $scriptElem) {
-            $scriptElem->parentNode->removeChild($scriptElem);
-        }
-
-        // Remove 'on*' attributes
-        $onAttributes = $xPath->query('//body//*/@*[starts-with(name(), \'on\')]');
-        foreach ($onAttributes as $attr) {
-            /** @var \DOMAttr $attr*/
-            $attrName = $attr->nodeName;
-            $attr->parentNode->removeAttribute($attrName);
-        }
-
-        $html = '';
-        $topElems = $doc->documentElement->childNodes->item(0)->childNodes;
-        foreach ($topElems as $child) {
-            $html .= $doc->saveHTML($child);
-        }
-
-        return $html;
-    }
-
-    /**
-     * Search for image usage within page content.
-     * @param $imageString
-     * @return mixed
-     */
-    public function searchForImage($imageString)
-    {
-        $pages = $this->entityQuery('page')->where('html', 'like', '%' . $imageString . '%')->get(['id', 'name', 'slug', 'book_id']);
-        foreach ($pages as $page) {
-            $page->url = $page->getUrl();
-            $page->html = '';
-            $page->text = '';
-        }
-        return count($pages) > 0 ? $pages : false;
-    }
-
-    /**
-     * Destroy a bookshelf instance
-     * @param Bookshelf $shelf
-     * @throws Throwable
-     */
-    public function destroyBookshelf(Bookshelf $shelf)
-    {
-        $this->destroyEntityCommonRelations($shelf);
-        $shelf->delete();
-    }
-
-    /**
-     * Destroy the provided book and all its child entities.
-     * @param Book $book
-     * @throws NotifyException
-     * @throws Throwable
-     */
-    public function destroyBook(Book $book)
-    {
-        foreach ($book->pages as $page) {
-            $this->destroyPage($page);
-        }
-        foreach ($book->chapters as $chapter) {
-            $this->destroyChapter($chapter);
-        }
-        $this->destroyEntityCommonRelations($book);
-        $book->delete();
-    }
-
-    /**
-     * Destroy a chapter and its relations.
-     * @param Chapter $chapter
-     * @throws Throwable
-     */
-    public function destroyChapter(Chapter $chapter)
-    {
-        if (count($chapter->pages) > 0) {
-            foreach ($chapter->pages as $page) {
-                $page->chapter_id = 0;
-                $page->save();
-            }
-        }
-        $this->destroyEntityCommonRelations($chapter);
-        $chapter->delete();
-    }
-
-    /**
-     * Destroy a given page along with its dependencies.
-     * @param Page $page
-     * @throws NotifyException
-     * @throws Throwable
-     */
-    public function destroyPage(Page $page)
-    {
-        // Check if set as custom homepage
-        $customHome = setting('app-homepage', '0:');
-        if (intval($page->id) === intval(explode(':', $customHome)[0])) {
-            throw new NotifyException(trans('errors.page_custom_home_deletion'), $page->getUrl());
-        }
-
-        $this->destroyEntityCommonRelations($page);
-
-        // Delete Attached Files
-        $attachmentService = app(AttachmentService::class);
-        foreach ($page->attachments as $attachment) {
-            $attachmentService->deleteFile($attachment);
-        }
-
-        $page->delete();
-    }
-
-    /**
-     * Destroy or handle the common relations connected to an entity.
-     * @param Entity $entity
-     * @throws Throwable
-     */
-    protected function destroyEntityCommonRelations(Entity $entity)
-    {
-        Activity::removeEntity($entity);
-        $entity->views()->delete();
-        $entity->permissions()->delete();
-        $entity->tags()->delete();
-        $entity->comments()->delete();
-        $this->permissionService->deleteJointPermissionsForEntity($entity);
-        $this->searchService->deleteEntityTerms($entity);
-    }
-
-    /**
-     * Copy the permissions of a bookshelf to all child books.
-     * Returns the number of books that had permissions updated.
-     * @param Bookshelf $bookshelf
-     * @return int
-     * @throws Throwable
-     */
-    public function copyBookshelfPermissions(Bookshelf $bookshelf)
-    {
-        $shelfPermissions = $bookshelf->permissions()->get(['role_id', 'action'])->toArray();
-        $shelfBooks = $bookshelf->books()->get();
-        $updatedBookCount = 0;
-
-        foreach ($shelfBooks as $book) {
-            if (!userCan('restrictions-manage', $book)) {
-                continue;
-            }
-            $book->permissions()->delete();
-            $book->restricted = $bookshelf->restricted;
-            $book->permissions()->createMany($shelfPermissions);
-            $book->save();
-            $this->permissionService->buildJointPermissionsForEntity($book);
-            $updatedBookCount++;
-        }
-
-        return $updatedBookCount;
-    }
-}
index 208aa5fa309444dabd61839a3fdc43b2d0a536b3..0fc68f95345189d6767087f04db9ec5ace2bb673 100644 (file)
 use BookStack\Entities\Book;
 use BookStack\Entities\Chapter;
 use BookStack\Entities\Entity;
+use BookStack\Entities\Managers\BookContents;
+use BookStack\Entities\Managers\PageContent;
+use BookStack\Entities\Managers\TrashCan;
 use BookStack\Entities\Page;
 use BookStack\Entities\PageRevision;
-use Carbon\Carbon;
-use DOMDocument;
-use DOMElement;
-use DOMXPath;
-
-class PageRepo extends EntityRepo
+use BookStack\Exceptions\MoveOperationException;
+use BookStack\Exceptions\NotFoundException;
+use BookStack\Exceptions\NotifyException;
+use BookStack\Exceptions\PermissionsException;
+use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Pagination\LengthAwarePaginator;
+use Illuminate\Support\Collection;
+
+class PageRepo
 {
 
+    protected $baseRepo;
+
     /**
-     * Get page by slug.
-     * @param string $pageSlug
-     * @param string $bookSlug
-     * @return Page
-     * @throws \BookStack\Exceptions\NotFoundException
+     * PageRepo constructor.
      */
-    public function getPageBySlug(string $pageSlug, string $bookSlug)
+    public function __construct(BaseRepo $baseRepo)
     {
-        return $this->getBySlug('page', $pageSlug, $bookSlug);
+        $this->baseRepo = $baseRepo;
     }
 
     /**
-     * Search through page revisions and retrieve the last page in the
-     * current book that has a slug equal to the one given.
-     * @param string $pageSlug
-     * @param string $bookSlug
-     * @return null|Page
+     * Get a page by ID.
+     * @throws NotFoundException
      */
-    public function getPageByOldSlug(string $pageSlug, string $bookSlug)
+    public function getById(int $id): Page
     {
-        $revision = $this->entityProvider->pageRevision->where('slug', '=', $pageSlug)
-            ->whereHas('page', function ($query) {
-                $this->permissionService->enforceEntityRestrictions('page', $query);
-            })
-            ->where('type', '=', 'version')
-            ->where('book_slug', '=', $bookSlug)
-            ->orderBy('created_at', 'desc')
-            ->with('page')->first();
-        return $revision !== null ? $revision->page : null;
+        $page = Page::visible()->with(['book'])->find($id);
+
+        if (!$page) {
+            throw new NotFoundException(trans('errors.page_not_found'));
+        }
+
+        return $page;
     }
 
     /**
-     * Updates a page with any fillable data and saves it into the database.
-     * @param Page $page
-     * @param int $book_id
-     * @param array $input
-     * @return Page
-     * @throws \Exception
+     * Get a page its book and own slug.
+     * @throws NotFoundException
      */
-    public function updatePage(Page $page, int $book_id, array $input)
+    public function getBySlug(string $bookSlug, string $pageSlug): Page
     {
-        // Hold the old details to compare later
-        $oldHtml = $page->html;
-        $oldName = $page->name;
+        $page = Page::visible()->whereSlugs($bookSlug, $pageSlug)->first();
 
-        // Prevent slug being updated if no name change
-        if ($page->name !== $input['name']) {
-            $page->slug = $this->findSuitableSlug('page', $input['name'], $page->id, $book_id);
+        if (!$page) {
+            throw new NotFoundException(trans('errors.page_not_found'));
         }
 
-        // Save page tags if present
-        if (isset($input['tags'])) {
-            $this->tagRepo->saveTagsToEntity($page, $input['tags']);
-        }
-
-        // Update with new details
-        $userId = user()->id;
-        $page->fill($input);
-        $page->html = $this->formatHtml($input['html']);
-        $page->text = $this->pageToPlainText($page);
-        if (setting('app-editor') !== 'markdown') {
-            $page->markdown = '';
-        }
-        $page->updated_by = $userId;
-        $page->revision_count++;
-        $page->save();
+        return $page;
+    }
 
-        // Remove all update drafts for this user & page.
-        $this->userUpdatePageDraftsQuery($page, $userId)->delete();
+    /**
+     * Get a page by its old slug but checking the revisions table
+     * for the last revision that matched the given page and book slug.
+     */
+    public function getByOldSlug(string $bookSlug, string $pageSlug): ?Page
+    {
+        $revision = PageRevision::query()
+            ->whereHas('page', function (Builder $query) {
+                $query->visible();
+            })
+            ->where('slug', '=', $pageSlug)
+            ->where('type', '=', 'version')
+            ->where('book_slug', '=', $bookSlug)
+            ->orderBy('created_at', 'desc')
+            ->with('page')
+            ->first();
+        return $revision ? $revision->page : null;
+    }
 
-        // Save a revision after updating
-        if ($oldHtml !== $input['html'] || $oldName !== $input['name'] || $input['summary'] !== null) {
-            $this->savePageRevision($page, $input['summary']);
+    /**
+     * Get pages that have been marked as a template.
+     */
+    public function getTemplates(int $count = 10, int $page = 1, string $search = ''): LengthAwarePaginator
+    {
+        $query = Page::visible()
+            ->where('template', '=', true)
+            ->orderBy('name', 'asc')
+            ->skip(($page - 1) * $count)
+            ->take($count);
+
+        if ($search) {
+            $query->where('name', 'like', '%' . $search . '%');
         }
 
-        $this->searchService->indexEntity($page);
+        $paginator = $query->paginate($count, ['*'], 'page', $page);
+        $paginator->withPath('/templates');
 
-        return $page;
+        return $paginator;
     }
 
     /**
-     * Saves a page revision into the system.
-     * @param Page $page
-     * @param null|string $summary
-     * @return PageRevision
-     * @throws \Exception
+     * Get a parent item via slugs.
      */
-    public function savePageRevision(Page $page, string $summary = null)
+    public function getParentFromSlugs(string $bookSlug, string $chapterSlug = null): Entity
     {
-        $revision = $this->entityProvider->pageRevision->newInstance($page->toArray());
-        if (setting('app-editor') !== 'markdown') {
-            $revision->markdown = '';
+        if ($chapterSlug !== null) {
+            return $chapter = Chapter::visible()->whereSlugs($bookSlug, $chapterSlug)->firstOrFail();
         }
-        $revision->page_id = $page->id;
-        $revision->slug = $page->slug;
-        $revision->book_slug = $page->book->slug;
-        $revision->created_by = user()->id;
-        $revision->created_at = $page->updated_at;
-        $revision->type = 'version';
-        $revision->summary = $summary;
-        $revision->revision_number = $page->revision_count;
-        $revision->save();
 
-        $revisionLimit = config('app.revision_limit');
-        if ($revisionLimit !== false) {
-            $revisionsToDelete = $this->entityProvider->pageRevision->where('page_id', '=', $page->id)
-                ->orderBy('created_at', 'desc')->skip(intval($revisionLimit))->take(10)->get(['id']);
-            if ($revisionsToDelete->count() > 0) {
-                $this->entityProvider->pageRevision->whereIn('id', $revisionsToDelete->pluck('id'))->delete();
-            }
-        }
+        return Book::visible()->where('slug', '=', $bookSlug)->firstOrFail();
+    }
 
+    /**
+     * Get the draft copy of the given page for the current user.
+     */
+    public function getUserDraft(Page $page): ?PageRevision
+    {
+        $revision = $this->getUserDraftQuery($page)->first();
         return $revision;
     }
 
     /**
-     * Formats a page's html to be tagged correctly within the system.
-     * @param string $htmlText
-     * @return string
+     * Get a new draft page belonging to the given parent entity.
      */
-    protected function formatHtml(string $htmlText)
+    public function getNewDraftPage(Entity $parent)
     {
-        if ($htmlText == '') {
-            return $htmlText;
+        $page = (new Page())->forceFill([
+            'name' => trans('entities.pages_initial_name'),
+            'created_by' => user()->id,
+            'updated_by' => user()->id,
+            'draft' => true,
+        ]);
+
+        if ($parent instanceof Chapter) {
+            $page->chapter_id = $parent->id;
+            $page->book_id = $parent->book_id;
+        } else {
+            $page->book_id = $parent->id;
         }
 
-        libxml_use_internal_errors(true);
-        $doc = new DOMDocument();
-        $doc->loadHTML(mb_convert_encoding($htmlText, 'HTML-ENTITIES', 'UTF-8'));
-
-        $container = $doc->documentElement;
-        $body = $container->childNodes->item(0);
-        $childNodes = $body->childNodes;
-
-        // Set ids on top-level nodes
-        $idMap = [];
-        foreach ($childNodes as $index => $childNode) {
-            $this->setUniqueId($childNode, $idMap);
-        }
+        $page->save();
+        $page->refresh()->rebuildPermissions();
+        return $page;
+    }
 
-        // Ensure no duplicate ids within child items
-        $xPath = new DOMXPath($doc);
-        $idElems = $xPath->query('//body//*//*[@id]');
-        foreach ($idElems as $domElem) {
-            $this->setUniqueId($domElem, $idMap);
+    /**
+     * Publish a draft page to make it a live, non-draft page.
+     */
+    public function publishDraft(Page $draft, array $input): Page
+    {
+        $this->baseRepo->update($draft, $input);
+        if (isset($input['template']) && userCan('templates-manage')) {
+            $draft->template = ($input['template'] === 'true');
         }
 
-        // Generate inner html as a string
-        $html = '';
-        foreach ($childNodes as $childNode) {
-            $html .= $doc->saveHTML($childNode);
-        }
+        $pageContent = new PageContent($draft);
+        $pageContent->setNewHTML($input['html']);
+        $draft->draft = false;
+        $draft->revision_count = 1;
+        $draft->priority = $this->getNewPriority($draft);
+        $draft->refreshSlug();
+        $draft->save();
 
-        return $html;
+        $this->savePageRevision($draft, trans('entities.pages_initial_revision'));
+        $draft->indexForSearch();
+        return $draft->refresh();
     }
 
     /**
-     * Set a unique id on the given DOMElement.
-     * A map for existing ID's should be passed in to check for current existence.
-     * @param DOMElement $element
-     * @param array $idMap
+     * Update a page in the system.
      */
-    protected function setUniqueId($element, array &$idMap)
+    public function update(Page $page, array $input): Page
     {
-        if (get_class($element) !== 'DOMElement') {
-            return;
-        }
+        // Hold the old details to compare later
+        $oldHtml = $page->html;
+        $oldName = $page->name;
 
-        // Overwrite id if not a BookStack custom id
-        $existingId = $element->getAttribute('id');
-        if (strpos($existingId, 'bkmrk') === 0 && !isset($idMap[$existingId])) {
-            $idMap[$existingId] = true;
-            return;
+        if (isset($input['template']) && userCan('templates-manage')) {
+            $page->template = ($input['template'] === 'true');
         }
 
-        // Create an unique id for the element
-        // Uses the content as a basis to ensure output is the same every time
-        // the same content is passed through.
-        $contentId = 'bkmrk-' . substr(strtolower(preg_replace('/\s+/', '-', trim($element->nodeValue))), 0, 20);
-        $newId = urlencode($contentId);
-        $loopIndex = 0;
+        $this->baseRepo->update($page, $input);
 
-        while (isset($idMap[$newId])) {
-            $newId = urlencode($contentId . '-' . $loopIndex);
-            $loopIndex++;
+        // Update with new details
+        $page->fill($input);
+        $pageContent = new PageContent($page);
+        $pageContent->setNewHTML($input['html']);
+        $page->revision_count++;
+
+        if (setting('app-editor') !== 'markdown') {
+            $page->markdown = '';
         }
 
-        $element->setAttribute('id', $newId);
-        $idMap[$newId] = true;
-    }
+        $page->save();
 
-    /**
-     * Get the plain text version of a page's content.
-     * @param \BookStack\Entities\Page $page
-     * @return string
-     */
-    protected function pageToPlainText(Page $page) : string
-    {
-        $html = $this->renderPage($page, true);
-        return strip_tags($html);
+        // Remove all update drafts for this user & page.
+        $this->getUserDraftQuery($page)->delete();
+
+        // Save a revision after updating
+        $summary = $input['summary'] ?? null;
+        if ($oldHtml !== $input['html'] || $oldName !== $input['name'] || $summary !== null) {
+            $this->savePageRevision($page, $summary);
+        }
+
+        return $page;
     }
 
     /**
-     * Get a new draft page instance.
-     * @param Book $book
-     * @param Chapter|null $chapter
-     * @return \BookStack\Entities\Page
-     * @throws \Throwable
+     * Saves a page revision into the system.
      */
-    public function getDraftPage(Book $book, Chapter $chapter = null)
+    protected function savePageRevision(Page $page, string $summary = null)
     {
-        $page = $this->entityProvider->page->newInstance();
-        $page->name = trans('entities.pages_initial_name');
-        $page->created_by = user()->id;
-        $page->updated_by = user()->id;
-        $page->draft = true;
+        $revision = new PageRevision($page->toArray());
 
-        if ($chapter) {
-            $page->chapter_id = $chapter->id;
+        if (setting('app-editor') !== 'markdown') {
+            $revision->markdown = '';
         }
 
-        $book->pages()->save($page);
-        $page = $this->entityProvider->page->find($page->id);
-        $this->permissionService->buildJointPermissionsForEntity($page);
-        return $page;
+        $revision->page_id = $page->id;
+        $revision->slug = $page->slug;
+        $revision->book_slug = $page->book->slug;
+        $revision->created_by = user()->id;
+        $revision->created_at = $page->updated_at;
+        $revision->type = 'version';
+        $revision->summary = $summary;
+        $revision->revision_number = $page->revision_count;
+        $revision->save();
+
+        $this->deleteOldRevisions($page);
+        return $revision;
     }
 
     /**
      * Save a page update draft.
-     * @param Page $page
-     * @param array $data
-     * @return PageRevision|Page
      */
-    public function updatePageDraft(Page $page, array $data = [])
+    public function updatePageDraft(Page $page, array $input)
     {
         // If the page itself is a draft simply update that
         if ($page->draft) {
-            $page->fill($data);
-            if (isset($data['html'])) {
-                $page->text = $this->pageToPlainText($page);
+            $page->fill($input);
+            if (isset($input['html'])) {
+                $content = new PageContent($page);
+                $content->setNewHTML($input['html']);
             }
             $page->save();
             return $page;
         }
 
         // Otherwise save the data to a revision
-        $userId = user()->id;
-        $drafts = $this->userUpdatePageDraftsQuery($page, $userId)->get();
-
-        if ($drafts->count() > 0) {
-            $draft = $drafts->first();
-        } else {
-            $draft = $this->entityProvider->pageRevision->newInstance();
-            $draft->page_id = $page->id;
-            $draft->slug = $page->slug;
-            $draft->book_slug = $page->book->slug;
-            $draft->created_by = $userId;
-            $draft->type = 'update_draft';
-        }
-
-        $draft->fill($data);
+        $draft = $this->getPageRevisionToUpdate($page);
+        $draft->fill($input);
         if (setting('app-editor') !== 'markdown') {
             $draft->markdown = '';
         }
@@ -284,245 +259,201 @@ class PageRepo extends EntityRepo
     }
 
     /**
-     * Publish a draft page to make it a normal page.
-     * Sets the slug and updates the content.
-     * @param Page $draftPage
-     * @param array $input
-     * @return Page
-     * @throws \Exception
+     * Destroy a page from the system.
+     * @throws NotifyException
      */
-    public function publishPageDraft(Page $draftPage, array $input)
+    public function destroy(Page $page)
     {
-        $draftPage->fill($input);
-
-        // Save page tags if present
-        if (isset($input['tags'])) {
-            $this->tagRepo->saveTagsToEntity($draftPage, $input['tags']);
-        }
-
-        $draftPage->slug = $this->findSuitableSlug('page', $draftPage->name, false, $draftPage->book->id);
-        $draftPage->html = $this->formatHtml($input['html']);
-        $draftPage->text = $this->pageToPlainText($draftPage);
-        $draftPage->draft = false;
-        $draftPage->revision_count = 1;
-
-        $draftPage->save();
-        $this->savePageRevision($draftPage, trans('entities.pages_initial_revision'));
-        $this->searchService->indexEntity($draftPage);
-        return $draftPage;
+        $trashCan = new TrashCan();
+        $trashCan->destroyPage($page);
     }
 
     /**
-     * The base query for getting user update drafts.
-     * @param Page $page
-     * @param $userId
-     * @return mixed
+     * Restores a revision's content back into a page.
      */
-    protected function userUpdatePageDraftsQuery(Page $page, int $userId)
+    public function restoreRevision(Page $page, int $revisionId): Page
     {
-        return $this->entityProvider->pageRevision->where('created_by', '=', $userId)
-            ->where('type', 'update_draft')
-            ->where('page_id', '=', $page->id)
-            ->orderBy('created_at', 'desc');
+        $page->revision_count++;
+        $this->savePageRevision($page);
+
+        $revision = $page->revisions()->where('id', '=', $revisionId)->first();
+        $page->fill($revision->toArray());
+        $content = new PageContent($page);
+        $content->setNewHTML($page->html);
+        $page->updated_by = user()->id;
+        $page->refreshSlug();
+        $page->save();
+
+        $page->indexForSearch();
+        return $page;
     }
 
     /**
-     * Get the latest updated draft revision for a particular page and user.
-     * @param Page $page
-     * @param $userId
-     * @return PageRevision|null
+     * Move the given page into a new parent book or chapter.
+     * The $parentIdentifier must be a string of the following format:
+     * 'book:<id>' (book:5)
+     * @throws MoveOperationException
+     * @throws PermissionsException
      */
-    public function getUserPageDraft(Page $page, int $userId)
+    public function move(Page $page, string $parentIdentifier): Book
     {
-        return $this->userUpdatePageDraftsQuery($page, $userId)->first();
+        $parent = $this->findParentByIdentifier($parentIdentifier);
+        if ($parent === null) {
+            throw new MoveOperationException('Book or chapter to move page into not found');
+        }
+
+        if (!userCan('page-create', $parent)) {
+            throw new PermissionsException('User does not have permission to create a page within the new parent');
+        }
+
+        $page->changeBook($parent instanceof Book ? $parent->id : $parent->book->id);
+        $page->rebuildPermissions();
+        return $parent;
     }
 
     /**
-     * Get the notification message that informs the user that they are editing a draft page.
-     * @param PageRevision $draft
-     * @return string
+     * Copy an existing page in the system.
+     * Optionally providing a new parent via string identifier and a new name.
+     * @throws MoveOperationException
+     * @throws PermissionsException
      */
-    public function getUserPageDraftMessage(PageRevision $draft)
+    public function copy(Page $page, string $parentIdentifier = null, string $newName = null): Page
     {
-        $message = trans('entities.pages_editing_draft_notification', ['timeDiff' => $draft->updated_at->diffForHumans()]);
-        if ($draft->page->updated_at->timestamp <= $draft->updated_at->timestamp) {
-            return $message;
+        $parent = $parentIdentifier ? $this->findParentByIdentifier($parentIdentifier) : $page->parent();
+        if ($parent === null) {
+            throw new MoveOperationException('Book or chapter to move page into not found');
+        }
+
+        if (!userCan('page-create', $parent)) {
+            throw new PermissionsException('User does not have permission to create a page within the new parent');
         }
-        return $message . "\n" . trans('entities.pages_draft_edited_notification');
+
+        $copyPage = $this->getNewDraftPage($parent);
+        $pageData = $page->getAttributes();
+
+        // Update name
+        if (!empty($newName)) {
+            $pageData['name'] = $newName;
+        }
+
+        // Copy tags from previous page if set
+        if ($page->tags) {
+            $pageData['tags'] = [];
+            foreach ($page->tags as $tag) {
+                $pageData['tags'][] = ['name' => $tag->name, 'value' => $tag->value];
+            }
+        }
+
+        return $this->publishDraft($copyPage, $pageData);
     }
 
     /**
-     * A query to check for active update drafts on a particular page.
-     * @param Page $page
-     * @param int $minRange
-     * @return mixed
+     * Find a page parent entity via a identifier string in the format:
+     * {type}:{id}
+     * Example: (book:5)
+     * @throws MoveOperationException
      */
-    protected function activePageEditingQuery(Page $page, int $minRange = null)
+    protected function findParentByIdentifier(string $identifier): ?Entity
     {
-        $query = $this->entityProvider->pageRevision->where('type', '=', 'update_draft')
-            ->where('page_id', '=', $page->id)
-            ->where('updated_at', '>', $page->updated_at)
-            ->where('created_by', '!=', user()->id)
-            ->with('createdBy');
+        $stringExploded = explode(':', $identifier);
+        $entityType = $stringExploded[0];
+        $entityId = intval($stringExploded[1]);
 
-        if ($minRange !== null) {
-            $query = $query->where('updated_at', '>=', Carbon::now()->subMinutes($minRange));
+        if ($entityType !== 'book' && $entityType !== 'chapter') {
+            throw new MoveOperationException('Pages can only be in books or chapters');
         }
 
-        return $query;
+        $parentClass = $entityType === 'book' ? Book::class : Chapter::class;
+        return $parentClass::visible()->where('id', '=', $entityId)->first();
     }
 
     /**
-     * Check if a page is being actively editing.
-     * Checks for edits since last page updated.
-     * Passing in a minuted range will check for edits
-     * within the last x minutes.
-     * @param Page $page
-     * @param int $minRange
-     * @return bool
+     * Update the permissions of a page.
      */
-    public function isPageEditingActive(Page $page, int $minRange = null)
+    public function updatePermissions(Page $page, bool $restricted, Collection $permissions = null)
     {
-        $draftSearch = $this->activePageEditingQuery($page, $minRange);
-        return $draftSearch->count() > 0;
+        $this->baseRepo->updatePermissions($page, $restricted, $permissions);
     }
 
     /**
-     * Get a notification message concerning the editing activity on a particular page.
-     * @param Page $page
-     * @param int $minRange
-     * @return string
+     * Change the page's parent to the given entity.
      */
-    public function getPageEditingActiveMessage(Page $page, int $minRange = null)
+    protected function changeParent(Page $page, Entity $parent)
     {
-        $pageDraftEdits = $this->activePageEditingQuery($page, $minRange)->get();
+        $book = ($parent instanceof Book) ? $parent : $parent->book;
+        $page->chapter_id = ($parent instanceof Chapter) ? $parent->id : 0;
+        $page->save();
+
+        if ($page->book->id !== $book->id) {
+            $page->changeBook($book->id);
+        }
 
-        $userMessage = $pageDraftEdits->count() > 1 ? trans('entities.pages_draft_edit_active.start_a', ['count' => $pageDraftEdits->count()]): trans('entities.pages_draft_edit_active.start_b', ['userName' => $pageDraftEdits->first()->createdBy->name]);
-        $timeMessage = $minRange === null ? trans('entities.pages_draft_edit_active.time_a') : trans('entities.pages_draft_edit_active.time_b', ['minCount'=>$minRange]);
-        return trans('entities.pages_draft_edit_active.message', ['start' => $userMessage, 'time' => $timeMessage]);
+        $page->load('book');
+        $book->rebuildPermissions();
     }
 
     /**
-     * Parse the headers on the page to get a navigation menu
-     * @param string $pageContent
-     * @return array
+     * Get a page revision to update for the given page.
+     * Checks for an existing revisions before providing a fresh one.
      */
-    public function getPageNav(string $pageContent)
+    protected function getPageRevisionToUpdate(Page $page): PageRevision
     {
-        if ($pageContent == '') {
-            return [];
-        }
-        libxml_use_internal_errors(true);
-        $doc = new DOMDocument();
-        $doc->loadHTML(mb_convert_encoding($pageContent, 'HTML-ENTITIES', 'UTF-8'));
-        $xPath = new DOMXPath($doc);
-        $headers = $xPath->query("//h1|//h2|//h3|//h4|//h5|//h6");
-
-        if (is_null($headers)) {
-            return [];
+        $drafts = $this->getUserDraftQuery($page)->get();
+        if ($drafts->count() > 0) {
+            return $drafts->first();
         }
 
-        $tree = collect($headers)->map(function($header) {
-            $text = trim(str_replace("\xc2\xa0", '', $header->nodeValue));
-            if (strlen($text) > 30) {
-                $text = substr($text, 0, 27) . '...';
-            }
-
-            return [
-                'nodeName' => strtolower($header->nodeName),
-                'level' => intval(str_replace('h', '', $header->nodeName)),
-                'link' => '#' . $header->getAttribute('id'),
-                'text' => $text,
-            ];
-        })->filter(function($header) {
-            return strlen($header['text']) > 0;
-        });
-
-        // Normalise headers if only smaller headers have been used
-        $minLevel = $tree->pluck('level')->min();
-        $tree = $tree->map(function ($header) use ($minLevel) {
-            $header['level'] -= ($minLevel - 2);
-            return $header;
-        });
-
-        return $tree->toArray();
+        $draft = new PageRevision();
+        $draft->page_id = $page->id;
+        $draft->slug = $page->slug;
+        $draft->book_slug = $page->book->slug;
+        $draft->created_by = user()->id;
+        $draft->type = 'update_draft';
+        return $draft;
     }
 
     /**
-     * Restores a revision's content back into a page.
-     * @param Page $page
-     * @param Book $book
-     * @param  int $revisionId
-     * @return Page
-     * @throws \Exception
+     * Delete old revisions, for the given page, from the system.
      */
-    public function restorePageRevision(Page $page, Book $book, int $revisionId)
+    protected function deleteOldRevisions(Page $page)
     {
-        $page->revision_count++;
-        $this->savePageRevision($page);
-        $revision = $page->revisions()->where('id', '=', $revisionId)->first();
-        $page->fill($revision->toArray());
-        $page->slug = $this->findSuitableSlug('page', $page->name, $page->id, $book->id);
-        $page->text = $this->pageToPlainText($page);
-        $page->updated_by = user()->id;
-        $page->save();
-        $this->searchService->indexEntity($page);
-        return $page;
+        $revisionLimit = config('app.revision_limit');
+        if ($revisionLimit === false) {
+            return;
+        }
+
+        $revisionsToDelete = PageRevision::query()
+            ->where('page_id', '=', $page->id)
+            ->orderBy('created_at', 'desc')
+            ->skip(intval($revisionLimit))
+            ->take(10)
+            ->get(['id']);
+        if ($revisionsToDelete->count() > 0) {
+            PageRevision::query()->whereIn('id', $revisionsToDelete->pluck('id'))->delete();
+        }
     }
 
     /**
-     * Change the page's parent to the given entity.
-     * @param Page $page
-     * @param Entity $parent
-     * @throws \Throwable
+     * Get a new priority for a page
      */
-    public function changePageParent(Page $page, Entity $parent)
+    protected function getNewPriority(Page $page): int
     {
-        $book = $parent->isA('book') ? $parent : $parent->book;
-        $page->chapter_id = $parent->isA('chapter') ? $parent->id : 0;
-        $page->save();
-        if ($page->book->id !== $book->id) {
-            $page = $this->changeBook('page', $book->id, $page);
+        if ($page->parent() instanceof Chapter) {
+            $lastPage = $page->parent()->pages('desc')->first();
+            return $lastPage ? $lastPage->priority + 1 : 0;
         }
-        $page->load('book');
-        $this->permissionService->buildJointPermissionsForEntity($book);
+
+        return (new BookContents($page->book))->getLastPriority() + 1;
     }
 
     /**
-     * Create a copy of a page in a new location with a new name.
-     * @param \BookStack\Entities\Page $page
-     * @param \BookStack\Entities\Entity $newParent
-     * @param string $newName
-     * @return \BookStack\Entities\Page
-     * @throws \Throwable
+     * Get the query to find the user's draft copies of the given page.
      */
-    public function copyPage(Page $page, Entity $newParent, string $newName = '')
+    protected function getUserDraftQuery(Page $page)
     {
-        $newBook = $newParent->isA('book') ? $newParent : $newParent->book;
-        $newChapter = $newParent->isA('chapter') ? $newParent : null;
-        $copyPage = $this->getDraftPage($newBook, $newChapter);
-        $pageData = $page->getAttributes();
-
-        // Update name
-        if (!empty($newName)) {
-            $pageData['name'] = $newName;
-        }
-
-        // Copy tags from previous page if set
-        if ($page->tags) {
-            $pageData['tags'] = [];
-            foreach ($page->tags as $tag) {
-                $pageData['tags'][] = ['name' => $tag->name, 'value' => $tag->value];
-            }
-        }
-
-        // Set priority
-        if ($newParent->isA('chapter')) {
-            $pageData['priority'] = $this->getNewChapterPriority($newParent);
-        } else {
-            $pageData['priority'] = $this->getNewBookPriority($newParent);
-        }
-
-        return $this->publishPageDraft($copyPage, $pageData);
+        return PageRevision::query()->where('created_by', '=', user()->id)
+            ->where('type', 'update_draft')
+            ->where('page_id', '=', $page->id)
+            ->orderBy('created_at', 'desc');
     }
 }
index 9e7cfdd0cdf1c8de852ee88a5464b662580e6f5b..ee9b87786a57a1e059ed050621f0c694427b17cb 100644 (file)
@@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
 use Illuminate\Database\Query\Builder;
 use Illuminate\Database\Query\JoinClause;
 use Illuminate\Support\Collection;
+use Illuminate\Support\Str;
 
 class SearchService
 {
@@ -210,7 +211,7 @@ class SearchService
 
         // Handle filters
         foreach ($terms['filters'] as $filterTerm => $filterValue) {
-            $functionName = camel_case('filter_' . $filterTerm);
+            $functionName = Str::camel('filter_' . $filterTerm);
             if (method_exists($this, $functionName)) {
                 $this->$functionName($entitySelect, $entity, $filterValue);
             }
@@ -514,7 +515,7 @@ class SearchService
 
     protected function filterSortBy(EloquentBuilder $query, Entity $model, $input)
     {
-        $functionName = camel_case('sort_by_' . $input);
+        $functionName = Str::camel('sort_by_' . $input);
         if (method_exists($this, $functionName)) {
             $this->$functionName($query, $model);
         }
diff --git a/app/Entities/SlugGenerator.php b/app/Entities/SlugGenerator.php
new file mode 100644 (file)
index 0000000..459a526
--- /dev/null
@@ -0,0 +1,62 @@
+<?php namespace BookStack\Entities;
+
+class SlugGenerator
+{
+
+    protected $entity;
+
+    /**
+     * SlugGenerator constructor.
+     * @param $entity
+     */
+    public function __construct(Entity $entity)
+    {
+        $this->entity = $entity;
+    }
+
+    /**
+     * Generate a fresh slug for the given entity.
+     * The slug will generated so it does not conflict within the same parent item.
+     */
+    public function generate(): string
+    {
+        $slug = $this->formatNameAsSlug($this->entity->name);
+        while ($this->slugInUse($slug)) {
+            $slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
+        }
+        return $slug;
+    }
+
+    /**
+     * Format a name as a url slug.
+     */
+    protected function formatNameAsSlug(string $name): string
+    {
+        $slug = preg_replace('/[\+\/\\\?\@\}\{\.\,\=\[\]\#\&\!\*\'\;\:\$\%]/', '', mb_strtolower($name));
+        $slug = preg_replace('/\s{2,}/', ' ', $slug);
+        $slug = str_replace(' ', '-', $slug);
+        if ($slug === "") {
+            $slug = substr(md5(rand(1, 500)), 0, 5);
+        }
+        return $slug;
+    }
+
+    /**
+     * Check if a slug is already in-use for this
+     * type of model within the same parent.
+     */
+    protected function slugInUse(string $slug): bool
+    {
+        $query = $this->entity->newQuery()->where('slug', '=', $slug);
+
+        if ($this->entity instanceof BookChild) {
+            $query->where('book_id', '=', $this->entity->book_id);
+        }
+
+        if ($this->entity->id) {
+            $query->where('id', '!=', $this->entity->id);
+        }
+
+        return $query->count() > 0;
+    }
+}
diff --git a/app/Exceptions/MoveOperationException.php b/app/Exceptions/MoveOperationException.php
new file mode 100644 (file)
index 0000000..c237dfa
--- /dev/null
@@ -0,0 +1,8 @@
+<?php namespace BookStack\Exceptions;
+
+use Exception;
+
+class MoveOperationException extends Exception
+{
+
+}
diff --git a/app/Exceptions/SortOperationException.php b/app/Exceptions/SortOperationException.php
new file mode 100644 (file)
index 0000000..8f91217
--- /dev/null
@@ -0,0 +1,8 @@
+<?php namespace BookStack\Exceptions;
+
+use Exception;
+
+class SortOperationException extends Exception
+{
+
+}
diff --git a/app/Exceptions/UserTokenExpiredException.php b/app/Exceptions/UserTokenExpiredException.php
new file mode 100644 (file)
index 0000000..e197074
--- /dev/null
@@ -0,0 +1,18 @@
+<?php namespace BookStack\Exceptions;
+
+class UserTokenExpiredException extends \Exception
+{
+
+    public $userId;
+
+    /**
+     * UserTokenExpiredException constructor.
+     * @param string $message
+     * @param int $userId
+     */
+    public function __construct(string $message, int $userId)
+    {
+        $this->userId = $userId;
+        parent::__construct($message);
+    }
+}
diff --git a/app/Exceptions/UserTokenNotFoundException.php b/app/Exceptions/UserTokenNotFoundException.php
new file mode 100644 (file)
index 0000000..3ed53f7
--- /dev/null
@@ -0,0 +1,6 @@
+<?php namespace BookStack\Exceptions;
+
+class UserTokenNotFoundException extends \Exception
+{
+
+}
diff --git a/app/Facades/Permissions.php b/app/Facades/Permissions.php
new file mode 100644 (file)
index 0000000..c552d7c
--- /dev/null
@@ -0,0 +1,16 @@
+<?php namespace BookStack\Facades;
+
+use Illuminate\Support\Facades\Facade;
+
+class Permissions extends Facade
+{
+    /**
+     * Get the registered name of the component.
+     *
+     * @return string
+     */
+    protected static function getFacadeAccessor()
+    {
+        return 'permissions';
+    }
+}
index 0289f8e1d8f09e5bdf1bde68c7437c2e5a4746be..8f5da49ed83c10c5979b9e4b8eaa74ac324e14b9 100644 (file)
@@ -1,37 +1,37 @@
 <?php namespace BookStack\Http\Controllers;
 
-use BookStack\Entities\Repos\EntityRepo;
+use BookStack\Entities\Repos\PageRepo;
 use BookStack\Exceptions\FileUploadException;
 use BookStack\Exceptions\NotFoundException;
 use BookStack\Uploads\Attachment;
 use BookStack\Uploads\AttachmentService;
+use Exception;
+use Illuminate\Contracts\Filesystem\FileNotFoundException;
 use Illuminate\Http\Request;
+use Illuminate\Validation\ValidationException;
 
 class AttachmentController extends Controller
 {
     protected $attachmentService;
     protected $attachment;
-    protected $entityRepo;
+    protected $pageRepo;
 
     /**
      * AttachmentController constructor.
-     * @param \BookStack\Uploads\AttachmentService $attachmentService
-     * @param Attachment $attachment
-     * @param EntityRepo $entityRepo
      */
-    public function __construct(AttachmentService $attachmentService, Attachment $attachment, EntityRepo $entityRepo)
+    public function __construct(AttachmentService $attachmentService, Attachment $attachment, PageRepo $pageRepo)
     {
         $this->attachmentService = $attachmentService;
         $this->attachment = $attachment;
-        $this->entityRepo = $entityRepo;
+        $this->pageRepo = $pageRepo;
         parent::__construct();
     }
 
 
     /**
      * Endpoint at which attachments are uploaded to.
-     * @param Request $request
-     * @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response
+     * @throws ValidationException
+     * @throws NotFoundException
      */
     public function upload(Request $request)
     {
@@ -41,7 +41,7 @@ class AttachmentController extends Controller
         ]);
 
         $pageId = $request->get('uploaded_to');
-        $page = $this->entityRepo->getById('page', $pageId, true);
+        $page = $this->pageRepo->getById($pageId);
 
         $this->checkPermission('attachment-create-all');
         $this->checkOwnablePermission('page-update', $page);
@@ -59,11 +59,10 @@ class AttachmentController extends Controller
 
     /**
      * Update an uploaded attachment.
-     * @param int $attachmentId
-     * @param Request $request
-     * @return mixed
+     * @throws ValidationException
+     * @throws NotFoundException
      */
-    public function uploadUpdate($attachmentId, Request $request)
+    public function uploadUpdate(Request $request, $attachmentId)
     {
         $this->validate($request, [
             'uploaded_to' => 'required|integer|exists:pages,id',
@@ -71,7 +70,7 @@ class AttachmentController extends Controller
         ]);
 
         $pageId = $request->get('uploaded_to');
-        $page = $this->entityRepo->getById('page', $pageId, true);
+        $page = $this->pageRepo->getById($pageId);
         $attachment = $this->attachment->findOrFail($attachmentId);
 
         $this->checkOwnablePermission('page-update', $page);
@@ -94,11 +93,10 @@ class AttachmentController extends Controller
 
     /**
      * Update the details of an existing file.
-     * @param $attachmentId
-     * @param Request $request
-     * @return Attachment|mixed
+     * @throws ValidationException
+     * @throws NotFoundException
      */
-    public function update($attachmentId, Request $request)
+    public function update(Request $request, $attachmentId)
     {
         $this->validate($request, [
             'uploaded_to' => 'required|integer|exists:pages,id',
@@ -107,7 +105,7 @@ class AttachmentController extends Controller
         ]);
 
         $pageId = $request->get('uploaded_to');
-        $page = $this->entityRepo->getById('page', $pageId, true);
+        $page = $this->pageRepo->getById($pageId);
         $attachment = $this->attachment->findOrFail($attachmentId);
 
         $this->checkOwnablePermission('page-update', $page);
@@ -123,8 +121,8 @@ class AttachmentController extends Controller
 
     /**
      * Attach a link to a page.
-     * @param Request $request
-     * @return mixed
+     * @throws ValidationException
+     * @throws NotFoundException
      */
     public function attachLink(Request $request)
     {
@@ -135,7 +133,7 @@ class AttachmentController extends Controller
         ]);
 
         $pageId = $request->get('uploaded_to');
-        $page = $this->entityRepo->getById('page', $pageId, true);
+        $page = $this->pageRepo->getById($pageId);
 
         $this->checkPermission('attachment-create-all');
         $this->checkOwnablePermission('page-update', $page);
@@ -149,29 +147,26 @@ class AttachmentController extends Controller
 
     /**
      * Get the attachments for a specific page.
-     * @param $pageId
-     * @return mixed
      */
-    public function listForPage($pageId)
+    public function listForPage(int $pageId)
     {
-        $page = $this->entityRepo->getById('page', $pageId, true);
+        $page = $this->pageRepo->getById($pageId);
         $this->checkOwnablePermission('page-view', $page);
         return response()->json($page->attachments);
     }
 
     /**
      * Update the attachment sorting.
-     * @param $pageId
-     * @param Request $request
-     * @return mixed
+     * @throws ValidationException
+     * @throws NotFoundException
      */
-    public function sortForPage($pageId, Request $request)
+    public function sortForPage(Request $request, int $pageId)
     {
         $this->validate($request, [
             'files' => 'required|array',
             'files.*.id' => 'required|integer',
         ]);
-        $page = $this->entityRepo->getById('page', $pageId);
+        $page = $this->pageRepo->getById($pageId);
         $this->checkOwnablePermission('page-update', $page);
 
         $attachments = $request->get('files');
@@ -181,16 +176,15 @@ class AttachmentController extends Controller
 
     /**
      * Get an attachment from storage.
-     * @param $attachmentId
-     * @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Symfony\Component\HttpFoundation\Response
-     * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
+     * @throws FileNotFoundException
      * @throws NotFoundException
      */
-    public function get($attachmentId)
+    public function get(int $attachmentId)
     {
         $attachment = $this->attachment->findOrFail($attachmentId);
-        $page = $this->entityRepo->getById('page', $attachment->uploaded_to);
-        if ($page === null) {
+        try {
+            $page = $this->pageRepo->getById($attachment->uploaded_to);
+        } catch (NotFoundException $exception) {
             throw new NotFoundException(trans('errors.attachment_not_found'));
         }
 
@@ -208,9 +202,9 @@ class AttachmentController extends Controller
      * Delete a specific attachment in the system.
      * @param $attachmentId
      * @return mixed
-     * @throws \Exception
+     * @throws Exception
      */
-    public function delete($attachmentId)
+    public function delete(int $attachmentId)
     {
         $attachment = $this->attachment->findOrFail($attachmentId);
         $this->checkOwnablePermission('attachment-delete', $attachment);
diff --git a/app/Http/Controllers/Auth/ConfirmEmailController.php b/app/Http/Controllers/Auth/ConfirmEmailController.php
new file mode 100644 (file)
index 0000000..099558e
--- /dev/null
@@ -0,0 +1,116 @@
+<?php
+
+namespace BookStack\Http\Controllers\Auth;
+
+use BookStack\Auth\Access\EmailConfirmationService;
+use BookStack\Auth\UserRepo;
+use BookStack\Exceptions\ConfirmationEmailException;
+use BookStack\Exceptions\UserTokenExpiredException;
+use BookStack\Exceptions\UserTokenNotFoundException;
+use BookStack\Http\Controllers\Controller;
+use Exception;
+use Illuminate\Http\RedirectResponse;
+use Illuminate\Http\Request;
+use Illuminate\Routing\Redirector;
+use Illuminate\View\View;
+
+class ConfirmEmailController extends Controller
+{
+    protected $emailConfirmationService;
+    protected $userRepo;
+
+    /**
+     * Create a new controller instance.
+     *
+     * @param EmailConfirmationService $emailConfirmationService
+     * @param UserRepo $userRepo
+     */
+    public function __construct(EmailConfirmationService $emailConfirmationService, UserRepo $userRepo)
+    {
+        $this->emailConfirmationService = $emailConfirmationService;
+        $this->userRepo = $userRepo;
+        parent::__construct();
+    }
+
+
+    /**
+     * Show the page to tell the user to check their email
+     * and confirm their address.
+     */
+    public function show()
+    {
+        return view('auth.register-confirm');
+    }
+
+    /**
+     * 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');
+    }
+
+    /**
+     * Confirms an email via a token and logs the user into the system.
+     * @param $token
+     * @return RedirectResponse|Redirector
+     * @throws ConfirmationEmailException
+     * @throws Exception
+     */
+    public function confirm($token)
+    {
+        try {
+            $userId = $this->emailConfirmationService->checkTokenAndGetUserId($token);
+        } catch (Exception $exception) {
+            if ($exception instanceof UserTokenNotFoundException) {
+                $this->showErrorNotification(trans('errors.email_confirmation_invalid'));
+                return redirect('/register');
+            }
+
+            if ($exception instanceof UserTokenExpiredException) {
+                $user = $this->userRepo->getById($exception->userId);
+                $this->emailConfirmationService->sendConfirmation($user);
+                $this->showErrorNotification(trans('errors.email_confirmation_expired'));
+                return redirect('/register/confirm');
+            }
+
+            throw $exception;
+        }
+
+        $user = $this->userRepo->getById($userId);
+        $user->email_confirmed = true;
+        $user->save();
+
+        auth()->login($user);
+        $this->showSuccessNotification(trans('auth.email_confirm_success'));
+        $this->emailConfirmationService->deleteByUser($user);
+
+        return redirect('/');
+    }
+
+
+    /**
+     * Resend the confirmation email
+     * @param Request $request
+     * @return View
+     */
+    public function resend(Request $request)
+    {
+        $this->validate($request, [
+            'email' => 'required|email|exists:users,email'
+        ]);
+        $user = $this->userRepo->getByEmail($request->get('email'));
+
+        try {
+            $this->emailConfirmationService->sendConfirmation($user);
+        } catch (Exception $e) {
+            $this->showErrorNotification(trans('auth.email_confirm_send_error'));
+            return redirect('/register/confirm');
+        }
+
+        $this->showSuccessNotification(trans('auth.email_confirm_resent'));
+        return redirect('/register/confirm');
+    }
+}
index a0cbae9c611bffeb421e64788ebca7651ef2f65e..a3c0433a56f9e4af9d574ae0446edac02dbd5c1f 100644 (file)
@@ -53,7 +53,7 @@ class ForgotPasswordController extends Controller
 
         if ($response === Password::RESET_LINK_SENT) {
             $message = trans('auth.reset_password_sent_success', ['email' => $request->get('email')]);
-            session()->flash('success', $message);
+            $this->showSuccessNotification($message);
             return back()->with('status', trans($response));
         }
 
index 78a8d33c0aed8ae342a5a80d86b4c2015ef02158..c739fd9a337387a973ba12d119059f84cf27cd07 100644 (file)
@@ -53,8 +53,8 @@ class LoginController extends Controller
         $this->socialAuthService = $socialAuthService;
         $this->ldapService = $ldapService;
         $this->userRepo = $userRepo;
-        $this->redirectPath = baseUrl('/');
-        $this->redirectAfterLogout = baseUrl('/login');
+        $this->redirectPath = url('/');
+        $this->redirectAfterLogout = url('/login');
         parent::__construct();
     }
 
@@ -106,9 +106,7 @@ class LoginController extends Controller
             $this->ldapService->syncGroups($user, $request->get($this->username()));
         }
 
-        $path = session()->pull('url.intended', '/');
-        $path = baseUrl($path, true);
-        return redirect($path);
+        return redirect()->intended('/');
     }
 
     /**
index 79d69665298130643405f24a8bfff18d30d20c57..304d3bed2e69999b381e0a8e1b4cb6a22790f492 100644 (file)
@@ -2,17 +2,24 @@
 
 namespace BookStack\Http\Controllers\Auth;
 
+use BookStack\Auth\Access\EmailConfirmationService;
+use BookStack\Auth\Access\SocialAuthService;
 use BookStack\Auth\SocialAccount;
 use BookStack\Auth\User;
 use BookStack\Auth\UserRepo;
+use BookStack\Exceptions\SocialDriverNotConfigured;
 use BookStack\Exceptions\SocialSignInAccountNotUsed;
 use BookStack\Exceptions\SocialSignInException;
 use BookStack\Exceptions\UserRegistrationException;
 use BookStack\Http\Controllers\Controller;
 use Exception;
 use Illuminate\Foundation\Auth\RegistersUsers;
+use Illuminate\Http\RedirectResponse;
 use Illuminate\Http\Request;
 use Illuminate\Http\Response;
+use Illuminate\Routing\Redirector;
+use Illuminate\Support\Facades\Hash;
+use Illuminate\Support\Str;
 use Laravel\Socialite\Contracts\User as SocialUser;
 use Validator;
 
@@ -46,18 +53,18 @@ class RegisterController extends Controller
     /**
      * Create a new controller instance.
      *
-     * @param \BookStack\Auth\Access\SocialAuthService $socialAuthService
-     * @param \BookStack\Auth\EmailConfirmationService $emailConfirmationService
-     * @param \BookStack\Auth\UserRepo $userRepo
+     * @param SocialAuthService $socialAuthService
+     * @param EmailConfirmationService $emailConfirmationService
+     * @param UserRepo $userRepo
      */
-    public function __construct(\BookStack\Auth\Access\SocialAuthService $socialAuthService, \BookStack\Auth\Access\EmailConfirmationService $emailConfirmationService, UserRepo $userRepo)
+    public function __construct(SocialAuthService $socialAuthService, EmailConfirmationService $emailConfirmationService, UserRepo $userRepo)
     {
         $this->middleware('guest')->only(['getRegister', 'postRegister', 'socialRegister']);
         $this->socialAuthService = $socialAuthService;
         $this->emailConfirmationService = $emailConfirmationService;
         $this->userRepo = $userRepo;
-        $this->redirectTo = baseUrl('/');
-        $this->redirectPath = baseUrl('/');
+        $this->redirectTo = url('/');
+        $this->redirectPath = url('/');
         parent::__construct();
     }
 
@@ -72,7 +79,7 @@ class RegisterController extends Controller
         return Validator::make($data, [
             'name' => 'required|min:2|max:255',
             'email' => 'required|email|max:255|unique:users',
-            'password' => 'required|min:6',
+            'password' => 'required|min:8',
         ]);
     }
 
@@ -101,8 +108,8 @@ class RegisterController extends Controller
 
     /**
      * Handle a registration request for the application.
-     * @param Request|\Illuminate\Http\Request $request
-     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
+     * @param Request|Request $request
+     * @return RedirectResponse|Redirector
      * @throws UserRegistrationException
      */
     public function postRegister(Request $request)
@@ -117,14 +124,14 @@ class RegisterController extends Controller
     /**
      * Create a new user instance after a valid registration.
      * @param  array  $data
-     * @return \BookStack\Auth\User
+     * @return User
      */
     protected function create(array $data)
     {
         return User::create([
             'name' => $data['name'],
             'email' => $data['email'],
-            'password' => bcrypt($data['password']),
+            'password' => Hash::make($data['password']),
         ]);
     }
 
@@ -133,7 +140,7 @@ class RegisterController extends Controller
      * @param array $userData
      * @param bool|false|SocialAccount $socialAccount
      * @param bool $emailVerified
-     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
+     * @return RedirectResponse|Redirector
      * @throws UserRegistrationException
      */
     protected function registerUser(array $userData, $socialAccount = false, $emailVerified = false)
@@ -142,7 +149,7 @@ class RegisterController extends Controller
 
         if ($registrationRestrict) {
             $restrictedEmailDomains = explode(',', str_replace(' ', '', $registrationRestrict));
-            $userEmailDomain = $domain = substr(strrchr($userData['email'], "@"), 1);
+            $userEmailDomain = $domain = mb_substr(mb_strrchr($userData['email'], "@"), 1);
             if (!in_array($userEmailDomain, $restrictedEmailDomains)) {
                 throw new UserRegistrationException(trans('auth.registration_email_domain_invalid'), '/register');
             }
@@ -153,89 +160,29 @@ class RegisterController extends Controller
             $newUser->socialAccounts()->save($socialAccount);
         }
 
-        if ((setting('registration-confirmation') || $registrationRestrict) && !$emailVerified) {
+        if ($this->emailConfirmationService->confirmationRequired() && !$emailVerified) {
             $newUser->save();
 
             try {
                 $this->emailConfirmationService->sendConfirmation($newUser);
             } catch (Exception $e) {
-                session()->flash('error', trans('auth.email_confirm_send_error'));
+                $this->showErrorNotification(trans('auth.email_confirm_send_error'));
             }
 
             return redirect('/register/confirm');
         }
 
         auth()->login($newUser);
-        session()->flash('success', trans('auth.register_success'));
+        $this->showSuccessNotification(trans('auth.register_success'));
         return redirect($this->redirectPath());
     }
 
-    /**
-     * Show the page to tell the user to check their email
-     * and confirm their address.
-     */
-    public function getRegisterConfirmation()
-    {
-        return view('auth.register-confirm');
-    }
-
-    /**
-     * Confirms an email via a token and logs the user into the system.
-     * @param $token
-     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
-     * @throws UserRegistrationException
-     */
-    public function confirmEmail($token)
-    {
-        $confirmation = $this->emailConfirmationService->getEmailConfirmationFromToken($token);
-        $user = $confirmation->user;
-        $user->email_confirmed = true;
-        $user->save();
-        auth()->login($user);
-        session()->flash('success', trans('auth.email_confirm_success'));
-        $this->emailConfirmationService->deleteConfirmationsByUser($user);
-        return redirect($this->redirectPath);
-    }
-
-    /**
-     * Shows a notice that a user's email address has not been confirmed,
-     * Also has the option to re-send the confirmation email.
-     * @return \Illuminate\View\View
-     */
-    public function showAwaitingConfirmation()
-    {
-        return view('auth.user-unconfirmed');
-    }
-
-    /**
-     * Resend the confirmation email
-     * @param Request $request
-     * @return \Illuminate\View\View
-     */
-    public function resendConfirmation(Request $request)
-    {
-        $this->validate($request, [
-            'email' => 'required|email|exists:users,email'
-        ]);
-        $user = $this->userRepo->getByEmail($request->get('email'));
-
-        try {
-            $this->emailConfirmationService->sendConfirmation($user);
-        } catch (Exception $e) {
-            session()->flash('error', trans('auth.email_confirm_send_error'));
-            return redirect('/register/confirm');
-        }
-
-        session()->flash('success', trans('auth.email_confirm_resent'));
-        return redirect('/register/confirm');
-    }
-
     /**
      * Redirect to the social site for authentication intended to register.
      * @param $socialDriver
      * @return mixed
      * @throws UserRegistrationException
-     * @throws \BookStack\Exceptions\SocialDriverNotConfigured
+     * @throws SocialDriverNotConfigured
      */
     public function socialRegister($socialDriver)
     {
@@ -246,14 +193,14 @@ class RegisterController extends Controller
 
     /**
      * The callback for social login services.
-     * @param $socialDriver
      * @param Request $request
-     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
+     * @param string $socialDriver
+     * @return RedirectResponse|Redirector
      * @throws SocialSignInException
      * @throws UserRegistrationException
-     * @throws \BookStack\Exceptions\SocialDriverNotConfigured
+     * @throws SocialDriverNotConfigured
      */
-    public function socialCallback($socialDriver, Request $request)
+    public function socialCallback(Request $request, string $socialDriver)
     {
         if (!session()->has('social-callback')) {
             throw new SocialSignInException(trans('errors.social_no_action_defined'), '/login');
@@ -292,7 +239,7 @@ class RegisterController extends Controller
     /**
      * Detach a social account from a user.
      * @param $socialDriver
-     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
+     * @return RedirectResponse|Redirector
      */
     public function detachSocialAccount($socialDriver)
     {
@@ -303,7 +250,7 @@ class RegisterController extends Controller
      * Register a new user after a registration callback.
      * @param string $socialDriver
      * @param SocialUser $socialUser
-     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
+     * @return RedirectResponse|Redirector
      * @throws UserRegistrationException
      */
     protected function socialRegisterCallback(string $socialDriver, SocialUser $socialUser)
@@ -316,7 +263,7 @@ class RegisterController extends Controller
         $userData = [
             'name' => $socialUser->getName(),
             'email' => $socialUser->getEmail(),
-            'password' => str_random(30)
+            'password' => Str::random(30)
         ];
         return $this->registerUser($userData, $socialAccount, $emailVerified);
     }
index 56f1cf026f0727af67bc51d785c012bd6b74daa7..4d98eca597c3fffcece65fc79209bae646a49ab9 100644 (file)
@@ -4,6 +4,7 @@ namespace BookStack\Http\Controllers\Auth;
 
 use BookStack\Http\Controllers\Controller;
 use Illuminate\Foundation\Auth\ResetsPasswords;
+use Illuminate\Http\Request;
 
 class ResetPasswordController extends Controller
 {
@@ -36,13 +37,14 @@ class ResetPasswordController extends Controller
     /**
      * Get the response for a successful password reset.
      *
-     * @param  string  $response
+     * @param Request $request
+     * @param string $response
      * @return \Illuminate\Http\Response
      */
-    protected function sendResetResponse($response)
+    protected function sendResetResponse(Request $request, $response)
     {
         $message = trans('auth.reset_password_success');
-        session()->flash('success', $message);
+        $this->showSuccessNotification($message);
         return redirect($this->redirectPath())
             ->with('status', trans($response));
     }
diff --git a/app/Http/Controllers/Auth/UserInviteController.php b/app/Http/Controllers/Auth/UserInviteController.php
new file mode 100644 (file)
index 0000000..c361b0a
--- /dev/null
@@ -0,0 +1,105 @@
+<?php
+
+namespace BookStack\Http\Controllers\Auth;
+
+use BookStack\Auth\Access\UserInviteService;
+use BookStack\Auth\UserRepo;
+use BookStack\Exceptions\UserTokenExpiredException;
+use BookStack\Exceptions\UserTokenNotFoundException;
+use BookStack\Http\Controllers\Controller;
+use Exception;
+use Illuminate\Contracts\View\Factory;
+use Illuminate\Http\RedirectResponse;
+use Illuminate\Http\Request;
+use Illuminate\Routing\Redirector;
+use Illuminate\View\View;
+
+class UserInviteController extends Controller
+{
+    protected $inviteService;
+    protected $userRepo;
+
+    /**
+     * Create a new controller instance.
+     *
+     * @param UserInviteService $inviteService
+     * @param UserRepo $userRepo
+     */
+    public function __construct(UserInviteService $inviteService, UserRepo $userRepo)
+    {
+        $this->inviteService = $inviteService;
+        $this->userRepo = $userRepo;
+        $this->middleware('guest');
+        parent::__construct();
+    }
+
+    /**
+     * Show the page for the user to set the password for their account.
+     * @param string $token
+     * @return Factory|View|RedirectResponse
+     * @throws Exception
+     */
+    public function showSetPassword(string $token)
+    {
+        try {
+            $this->inviteService->checkTokenAndGetUserId($token);
+        } catch (Exception $exception) {
+            return $this->handleTokenException($exception);
+        }
+
+        return view('auth.invite-set-password', [
+            'token' => $token,
+        ]);
+    }
+
+    /**
+     * Sets the password for an invited user and then grants them access.
+     * @param Request $request
+     * @param string $token
+     * @return RedirectResponse|Redirector
+     * @throws Exception
+     */
+    public function setPassword(Request $request, string $token)
+    {
+        $this->validate($request, [
+            'password' => 'required|min:8'
+        ]);
+
+        try {
+            $userId = $this->inviteService->checkTokenAndGetUserId($token);
+        } catch (Exception $exception) {
+            return $this->handleTokenException($exception);
+        }
+
+        $user = $this->userRepo->getById($userId);
+        $user->password = bcrypt($request->get('password'));
+        $user->email_confirmed = true;
+        $user->save();
+
+        auth()->login($user);
+        $this->showSuccessNotification(trans('auth.user_invite_success', ['appName' => setting('app-name')]));
+        $this->inviteService->deleteByUser($user);
+
+        return redirect('/');
+    }
+
+    /**
+     * Check and validate the exception thrown when checking an invite token.
+     * @param Exception $exception
+     * @return RedirectResponse|Redirector
+     * @throws Exception
+     */
+    protected function handleTokenException(Exception $exception)
+    {
+        if ($exception instanceof UserTokenNotFoundException) {
+            return redirect('/');
+        }
+
+        if ($exception instanceof UserTokenExpiredException) {
+            $this->showErrorNotification(trans('errors.invite_token_expired'));
+            return redirect('/password/email');
+        }
+
+        throw $exception;
+    }
+}
index a990eed9a950e4ffb45365bae858a35b279e1d43..e7d788d91dde01b78fdd158035fe07fd6cd5f54e 100644 (file)
@@ -1,67 +1,46 @@
 <?php namespace BookStack\Http\Controllers;
 
 use Activity;
-use BookStack\Auth\UserRepo;
-use BookStack\Entities\Book;
-use BookStack\Entities\EntityContextManager;
-use BookStack\Entities\Repos\EntityRepo;
-use BookStack\Entities\ExportService;
-use BookStack\Uploads\ImageRepo;
+use BookStack\Entities\Managers\BookContents;
+use BookStack\Entities\Bookshelf;
+use BookStack\Entities\Managers\EntityContext;
+use BookStack\Entities\Repos\BookRepo;
+use BookStack\Exceptions\ImageUploadException;
+use BookStack\Exceptions\NotifyException;
 use Illuminate\Http\Request;
-use Illuminate\Http\Response;
+use Illuminate\Validation\ValidationException;
+use Throwable;
 use Views;
 
 class BookController extends Controller
 {
 
-    protected $entityRepo;
-    protected $userRepo;
-    protected $exportService;
+    protected $bookRepo;
     protected $entityContextManager;
-    protected $imageRepo;
 
     /**
      * BookController constructor.
-     * @param EntityRepo $entityRepo
-     * @param UserRepo $userRepo
-     * @param ExportService $exportService
-     * @param EntityContextManager $entityContextManager
-     * @param ImageRepo $imageRepo
      */
-    public function __construct(
-        EntityRepo $entityRepo,
-        UserRepo $userRepo,
-        ExportService $exportService,
-        EntityContextManager $entityContextManager,
-        ImageRepo $imageRepo
-    ) {
-        $this->entityRepo = $entityRepo;
-        $this->userRepo = $userRepo;
-        $this->exportService = $exportService;
+    public function __construct(EntityContext $entityContextManager, BookRepo $bookRepo)
+    {
+        $this->bookRepo = $bookRepo;
         $this->entityContextManager = $entityContextManager;
-        $this->imageRepo = $imageRepo;
         parent::__construct();
     }
 
     /**
      * Display a listing of the book.
-     * @return Response
      */
     public function index()
     {
-        $view = setting()->getUser($this->currentUser, 'books_view_type', config('app.views.books'));
-        $sort = setting()->getUser($this->currentUser, 'books_sort', 'name');
-        $order = setting()->getUser($this->currentUser, 'books_sort_order', 'asc');
-        $sortOptions = [
-            'name' => trans('common.sort_name'),
-            'created_at' => trans('common.sort_created_at'),
-            'updated_at' => trans('common.sort_updated_at'),
-        ];
-
-        $books = $this->entityRepo->getAllPaginated('book', 18, $sort, $order);
-        $recents = $this->signedIn ? $this->entityRepo->getRecentlyViewed('book', 4, 0) : false;
-        $popular = $this->entityRepo->getPopular('book', 4, 0);
-        $new = $this->entityRepo->getRecentlyCreated('book', 4, 0);
+        $view = setting()->getForCurrentUser('books_view_type', config('app.views.books'));
+        $sort = setting()->getForCurrentUser('books_sort', 'name');
+        $order = setting()->getForCurrentUser('books_sort_order', 'asc');
+
+        $books = $this->bookRepo->getAllPaginated(18, $sort, $order);
+        $recents = $this->isSignedIn() ? $this->bookRepo->getRecentlyViewed(4) : false;
+        $popular = $this->bookRepo->getPopular(4);
+        $new = $this->bookRepo->getRecentlyCreated(4);
 
         $this->entityContextManager->clearShelfContext();
 
@@ -74,25 +53,22 @@ class BookController extends Controller
             'view' => $view,
             'sort' => $sort,
             'order' => $order,
-            'sortOptions' => $sortOptions,
         ]);
     }
 
     /**
      * Show the form for creating a new book.
-     * @param string $shelfSlug
-     * @return Response
-     * @throws \BookStack\Exceptions\NotFoundException
      */
     public function create(string $shelfSlug = null)
     {
+        $this->checkPermission('book-create-all');
+
         $bookshelf = null;
         if ($shelfSlug !== null) {
-            $bookshelf = $this->entityRepo->getBySlug('bookshelf', $shelfSlug);
+            $bookshelf = Bookshelf::visible()->where('slug', '=', $shelfSlug)->firstOrFail();
             $this->checkOwnablePermission('bookshelf-update', $bookshelf);
         }
 
-        $this->checkPermission('book-create-all');
         $this->setPageTitle(trans('entities.books_create'));
         return view('books.create', [
             'bookshelf' => $bookshelf
@@ -101,12 +77,8 @@ class BookController extends Controller
 
     /**
      * Store a newly created book in storage.
-     *
-     * @param Request $request
-     * @param string $shelfSlug
-     * @return Response
-     * @throws \BookStack\Exceptions\NotFoundException
-     * @throws \BookStack\Exceptions\ImageUploadException
+     * @throws ImageUploadException
+     * @throws ValidationException
      */
     public function store(Request $request, string $shelfSlug = null)
     {
@@ -114,21 +86,21 @@ class BookController extends Controller
         $this->validate($request, [
             'name' => 'required|string|max:255',
             'description' => 'string|max:1000',
-            'image' => $this->imageRepo->getImageValidationRules(),
+            'image' => $this->getImageValidationRules(),
         ]);
 
         $bookshelf = null;
         if ($shelfSlug !== null) {
-            $bookshelf = $this->entityRepo->getBySlug('bookshelf', $shelfSlug);
+            $bookshelf = Bookshelf::visible()->where('slug', '=', $shelfSlug)->firstOrFail();
             $this->checkOwnablePermission('bookshelf-update', $bookshelf);
         }
 
-        $book = $this->entityRepo->createFromInput('book', $request->all());
-        $this->bookUpdateActions($book, $request);
+        $book = $this->bookRepo->create($request->all());
+        $this->bookRepo->updateCoverImage($book, $request->file('image', null));
         Activity::add($book, 'book_create', $book->id);
 
         if ($bookshelf) {
-            $this->entityRepo->appendBookToShelf($bookshelf, $book);
+            $bookshelf->appendBook($book);
             Activity::add($bookshelf, 'bookshelf_update');
         }
 
@@ -137,17 +109,11 @@ class BookController extends Controller
 
     /**
      * Display the specified book.
-     * @param $slug
-     * @param Request $request
-     * @return Response
-     * @throws \BookStack\Exceptions\NotFoundException
      */
-    public function show($slug, Request $request)
+    public function show(Request $request, string $slug)
     {
-        $book = $this->entityRepo->getBySlug('book', $slug);
-        $this->checkOwnablePermission('book-view', $book);
-
-        $bookChildren = $this->entityRepo->getBookChildren($book);
+        $book = $this->bookRepo->getBySlug($slug);
+        $bookChildren = (new BookContents($book))->getTree(true);
 
         Views::add($book);
         if ($request->has('shelf')) {
@@ -165,12 +131,10 @@ class BookController extends Controller
 
     /**
      * Show the form for editing the specified book.
-     * @param $slug
-     * @return Response
      */
-    public function edit($slug)
+    public function edit(string $slug)
     {
-        $book = $this->entityRepo->getBySlug('book', $slug);
+        $book = $this->bookRepo->getBySlug($slug);
         $this->checkOwnablePermission('book-update', $book);
         $this->setPageTitle(trans('entities.books_edit_named', ['bookName'=>$book->getShortName()]));
         return view('books.edit', ['book' => $book, 'current' => $book]);
@@ -178,254 +142,83 @@ class BookController extends Controller
 
     /**
      * Update the specified book in storage.
-     * @param Request $request
-     * @param          $slug
-     * @return Response
-     * @throws \BookStack\Exceptions\ImageUploadException
-     * @throws \BookStack\Exceptions\NotFoundException
+     * @throws ImageUploadException
+     * @throws ValidationException
+     * @throws Throwable
      */
     public function update(Request $request, string $slug)
     {
-        $book = $this->entityRepo->getBySlug('book', $slug);
+        $book = $this->bookRepo->getBySlug($slug);
         $this->checkOwnablePermission('book-update', $book);
         $this->validate($request, [
             'name' => 'required|string|max:255',
             'description' => 'string|max:1000',
-            'image' => $this->imageRepo->getImageValidationRules(),
+            'image' => $this->getImageValidationRules(),
         ]);
 
-         $book = $this->entityRepo->updateFromInput('book', $book, $request->all());
-         $this->bookUpdateActions($book, $request);
+        $book = $this->bookRepo->update($book, $request->all());
+        $resetCover = $request->has('image_reset');
+        $this->bookRepo->updateCoverImage($book, $request->file('image', null), $resetCover);
 
-         Activity::add($book, 'book_update', $book->id);
+        Activity::add($book, 'book_update', $book->id);
 
-         return redirect($book->getUrl());
+        return redirect($book->getUrl());
     }
 
     /**
-     * Shows the page to confirm deletion
-     * @param $bookSlug
-     * @return \Illuminate\View\View
+     * Shows the page to confirm deletion.
      */
-    public function showDelete($bookSlug)
+    public function showDelete(string $bookSlug)
     {
-        $book = $this->entityRepo->getBySlug('book', $bookSlug);
+        $book = $this->bookRepo->getBySlug($bookSlug);
         $this->checkOwnablePermission('book-delete', $book);
-        $this->setPageTitle(trans('entities.books_delete_named', ['bookName'=>$book->getShortName()]));
+        $this->setPageTitle(trans('entities.books_delete_named', ['bookName' => $book->getShortName()]));
         return view('books.delete', ['book' => $book, 'current' => $book]);
     }
 
     /**
-     * Shows the view which allows pages to be re-ordered and sorted.
-     * @param string $bookSlug
-     * @return \Illuminate\View\View
-     * @throws \BookStack\Exceptions\NotFoundException
-     */
-    public function sort($bookSlug)
-    {
-        $book = $this->entityRepo->getBySlug('book', $bookSlug);
-        $this->checkOwnablePermission('book-update', $book);
-
-        $bookChildren = $this->entityRepo->getBookChildren($book, true);
-
-        $this->setPageTitle(trans('entities.books_sort_named', ['bookName'=>$book->getShortName()]));
-        return view('books.sort', ['book' => $book, 'current' => $book, 'bookChildren' => $bookChildren]);
-    }
-
-    /**
-     * Shows the sort box for a single book.
-     * Used via AJAX when loading in extra books to a sort.
-     * @param $bookSlug
-     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
+     * Remove the specified book from the system.
+     * @throws Throwable
+     * @throws NotifyException
      */
-    public function getSortItem($bookSlug)
+    public function destroy(string $bookSlug)
     {
-        $book = $this->entityRepo->getBySlug('book', $bookSlug);
-        $bookChildren = $this->entityRepo->getBookChildren($book);
-        return view('books.sort-box', ['book' => $book, 'bookChildren' => $bookChildren]);
-    }
-
-    /**
-     * Saves an array of sort mapping to pages and chapters.
-     * @param  string $bookSlug
-     * @param Request $request
-     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
-     */
-    public function saveSort($bookSlug, Request $request)
-    {
-        $book = $this->entityRepo->getBySlug('book', $bookSlug);
-        $this->checkOwnablePermission('book-update', $book);
-
-        // Return if no map sent
-        if (!$request->filled('sort-tree')) {
-            return redirect($book->getUrl());
-        }
-
-        // Sort pages and chapters
-        $sortMap = collect(json_decode($request->get('sort-tree')));
-        $bookIdsInvolved = collect([$book->id]);
-
-        // Load models into map
-        $sortMap->each(function ($mapItem) use ($bookIdsInvolved) {
-            $mapItem->type = ($mapItem->type === 'page' ? 'page' : 'chapter');
-            $mapItem->model = $this->entityRepo->getById($mapItem->type, $mapItem->id);
-            // Store source and target books
-            $bookIdsInvolved->push(intval($mapItem->model->book_id));
-            $bookIdsInvolved->push(intval($mapItem->book));
-        });
-
-        // Get the books involved in the sort
-        $bookIdsInvolved = $bookIdsInvolved->unique()->toArray();
-        $booksInvolved = $this->entityRepo->getManyById('book', $bookIdsInvolved, false, true);
-        // Throw permission error if invalid ids or inaccessible books given.
-        if (count($bookIdsInvolved) !== count($booksInvolved)) {
-            $this->showPermissionError();
-        }
-        // Check permissions of involved books
-        $booksInvolved->each(function (Book $book) {
-             $this->checkOwnablePermission('book-update', $book);
-        });
-
-        // Perform the sort
-        $sortMap->each(function ($mapItem) {
-            $model = $mapItem->model;
-
-            $priorityChanged = intval($model->priority) !== intval($mapItem->sort);
-            $bookChanged = intval($model->book_id) !== intval($mapItem->book);
-            $chapterChanged = ($mapItem->type === 'page') && intval($model->chapter_id) !== $mapItem->parentChapter;
-
-            if ($bookChanged) {
-                $this->entityRepo->changeBook($mapItem->type, $mapItem->book, $model);
-            }
-            if ($chapterChanged) {
-                $model->chapter_id = intval($mapItem->parentChapter);
-                $model->save();
-            }
-            if ($priorityChanged) {
-                $model->priority = intval($mapItem->sort);
-                $model->save();
-            }
-        });
-
-        // Rebuild permissions and add activity for involved books.
-        $booksInvolved->each(function (Book $book) {
-            $this->entityRepo->buildJointPermissionsForBook($book);
-            Activity::add($book, 'book_sort', $book->id);
-        });
-
-        return redirect($book->getUrl());
-    }
-
-    /**
-     * Remove the specified book from storage.
-     * @param $bookSlug
-     * @return Response
-     */
-    public function destroy($bookSlug)
-    {
-        $book = $this->entityRepo->getBySlug('book', $bookSlug);
+        $book = $this->bookRepo->getBySlug($bookSlug);
         $this->checkOwnablePermission('book-delete', $book);
-        Activity::addMessage('book_delete', 0, $book->name);
 
-        if ($book->cover) {
-            $this->imageRepo->destroyImage($book->cover);
-        }
-        $this->entityRepo->destroyBook($book);
+        Activity::addMessage('book_delete', $book->name);
+        $this->bookRepo->destroy($book);
 
         return redirect('/books');
     }
 
     /**
-     * Show the Restrictions view.
-     * @param $bookSlug
-     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
+     * Show the permissions view.
      */
-    public function showPermissions($bookSlug)
+    public function showPermissions(string $bookSlug)
     {
-        $book = $this->entityRepo->getBySlug('book', $bookSlug);
+        $book = $this->bookRepo->getBySlug($bookSlug);
         $this->checkOwnablePermission('restrictions-manage', $book);
-        $roles = $this->userRepo->getRestrictableRoles();
+
         return view('books.permissions', [
             'book' => $book,
-            'roles' => $roles
         ]);
     }
 
     /**
      * Set the restrictions for this book.
-     * @param $bookSlug
-     * @param Request $request
-     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
-     * @throws \BookStack\Exceptions\NotFoundException
-     * @throws \Throwable
+     * @throws Throwable
      */
-    public function permissions($bookSlug, Request $request)
+    public function permissions(Request $request, string $bookSlug)
     {
-        $book = $this->entityRepo->getBySlug('book', $bookSlug);
+        $book = $this->bookRepo->getBySlug($bookSlug);
         $this->checkOwnablePermission('restrictions-manage', $book);
-        $this->entityRepo->updateEntityPermissionsFromRequest($request, $book);
-        session()->flash('success', trans('entities.books_permissions_updated'));
-        return redirect($book->getUrl());
-    }
-
-    /**
-     * Export a book as a PDF file.
-     * @param string $bookSlug
-     * @return mixed
-     */
-    public function exportPdf($bookSlug)
-    {
-        $book = $this->entityRepo->getBySlug('book', $bookSlug);
-        $pdfContent = $this->exportService->bookToPdf($book);
-        return $this->downloadResponse($pdfContent, $bookSlug . '.pdf');
-    }
-
-    /**
-     * Export a book as a contained HTML file.
-     * @param string $bookSlug
-     * @return mixed
-     */
-    public function exportHtml($bookSlug)
-    {
-        $book = $this->entityRepo->getBySlug('book', $bookSlug);
-        $htmlContent = $this->exportService->bookToContainedHtml($book);
-        return $this->downloadResponse($htmlContent, $bookSlug . '.html');
-    }
-
-    /**
-     * Export a book as a plain text file.
-     * @param $bookSlug
-     * @return mixed
-     */
-    public function exportPlainText($bookSlug)
-    {
-        $book = $this->entityRepo->getBySlug('book', $bookSlug);
-        $textContent = $this->exportService->bookToPlainText($book);
-        return $this->downloadResponse($textContent, $bookSlug . '.txt');
-    }
 
-    /**
-     * Common actions to run on book update.
-     * Handles updating the cover image.
-     * @param Book $book
-     * @param Request $request
-     * @throws \BookStack\Exceptions\ImageUploadException
-     */
-    protected function bookUpdateActions(Book $book, Request $request)
-    {
-        // Update the cover image if in request
-        if ($request->has('image')) {
-            $this->imageRepo->destroyImage($book->cover);
-            $newImage = $request->file('image');
-            $image = $this->imageRepo->saveNew($newImage, 'cover_book', $book->id, 512, 512, true);
-            $book->image_id = $image->id;
-            $book->save();
-        }
+        $restricted = $request->get('restricted') === 'true';
+        $permissions = $request->filled('restrictions') ? collect($request->get('restrictions')) : null;
+        $this->bookRepo->updatePermissions($book, $restricted, $permissions);
 
-        if ($request->has('image_reset')) {
-            $this->imageRepo->destroyImage($book->cover);
-            $book->image_id = 0;
-            $book->save();
-        }
+        $this->showSuccessNotification(trans('entities.books_permissions_updated'));
+        return redirect($book->getUrl());
     }
 }
diff --git a/app/Http/Controllers/BookExportController.php b/app/Http/Controllers/BookExportController.php
new file mode 100644 (file)
index 0000000..cfa3d6a
--- /dev/null
@@ -0,0 +1,56 @@
+<?php
+
+namespace BookStack\Http\Controllers;
+
+use BookStack\Entities\ExportService;
+use BookStack\Entities\Repos\BookRepo;
+use Throwable;
+
+class BookExportController extends Controller
+{
+
+    protected $bookRepo;
+    protected $exportService;
+
+    /**
+     * BookExportController constructor.
+     */
+    public function __construct(BookRepo $bookRepo, ExportService $exportService)
+    {
+        $this->bookRepo = $bookRepo;
+        $this->exportService = $exportService;
+        parent::__construct();
+    }
+
+    /**
+     * Export a book as a PDF file.
+     * @throws Throwable
+     */
+    public function pdf(string $bookSlug)
+    {
+        $book = $this->bookRepo->getBySlug($bookSlug);
+        $pdfContent = $this->exportService->bookToPdf($book);
+        return $this->downloadResponse($pdfContent, $bookSlug . '.pdf');
+    }
+
+    /**
+     * Export a book as a contained HTML file.
+     * @throws Throwable
+     */
+    public function html(string $bookSlug)
+    {
+        $book = $this->bookRepo->getBySlug($bookSlug);
+        $htmlContent = $this->exportService->bookToContainedHtml($book);
+        return $this->downloadResponse($htmlContent, $bookSlug . '.html');
+    }
+
+    /**
+     * Export a book as a plain text file.
+     */
+    public function plainText(string $bookSlug)
+    {
+        $book = $this->bookRepo->getBySlug($bookSlug);
+        $textContent = $this->exportService->bookToPlainText($book);
+        return $this->downloadResponse($textContent, $bookSlug . '.txt');
+    }
+}
diff --git a/app/Http/Controllers/BookSortController.php b/app/Http/Controllers/BookSortController.php
new file mode 100644 (file)
index 0000000..f5fb6f2
--- /dev/null
@@ -0,0 +1,82 @@
+<?php
+
+namespace BookStack\Http\Controllers;
+
+use BookStack\Entities\Book;
+use BookStack\Entities\Managers\BookContents;
+use BookStack\Entities\Repos\BookRepo;
+use BookStack\Exceptions\SortOperationException;
+use BookStack\Facades\Activity;
+use Illuminate\Http\Request;
+
+class BookSortController extends Controller
+{
+
+    protected $bookRepo;
+
+    /**
+     * BookSortController constructor.
+     * @param $bookRepo
+     */
+    public function __construct(BookRepo $bookRepo)
+    {
+        $this->bookRepo = $bookRepo;
+        parent::__construct();
+    }
+
+    /**
+     * Shows the view which allows pages to be re-ordered and sorted.
+     */
+    public function show(string $bookSlug)
+    {
+        $book = $this->bookRepo->getBySlug($bookSlug);
+        $this->checkOwnablePermission('book-update', $book);
+
+        $bookChildren = (new BookContents($book))->getTree(false);
+
+        $this->setPageTitle(trans('entities.books_sort_named', ['bookName'=>$book->getShortName()]));
+        return view('books.sort', ['book' => $book, 'current' => $book, 'bookChildren' => $bookChildren]);
+    }
+
+    /**
+     * Shows the sort box for a single book.
+     * Used via AJAX when loading in extra books to a sort.
+     */
+    public function showItem(string $bookSlug)
+    {
+        $book = $this->bookRepo->getBySlug($bookSlug);
+        $bookChildren = (new BookContents($book))->getTree();
+        return view('books.sort-box', ['book' => $book, 'bookChildren' => $bookChildren]);
+    }
+
+    /**
+     * Sorts a book using a given mapping array.
+     */
+    public function update(Request $request, string $bookSlug)
+    {
+        $book = $this->bookRepo->getBySlug($bookSlug);
+        $this->checkOwnablePermission('book-update', $book);
+
+        // Return if no map sent
+        if (!$request->filled('sort-tree')) {
+            return redirect($book->getUrl());
+        }
+
+        $sortMap = collect(json_decode($request->get('sort-tree')));
+        $bookContents = new BookContents($book);
+        $booksInvolved = collect();
+
+        try {
+            $booksInvolved = $bookContents->sortUsingMap($sortMap);
+        } catch (SortOperationException $exception) {
+            $this->showPermissionError();
+        }
+
+        // Rebuild permissions and add activity for involved books.
+        $booksInvolved->each(function (Book $book) {
+            Activity::add($book, 'book_sort', $book->id);
+        });
+
+        return redirect($book->getUrl());
+    }
+}
index bcf2e12df88a67628bdded31f2db1162ce120098..57e67dc00e618b53fec9b16eec134242422dd8af 100644 (file)
@@ -1,34 +1,30 @@
 <?php namespace BookStack\Http\Controllers;
 
 use Activity;
-use BookStack\Auth\UserRepo;
-use BookStack\Entities\Bookshelf;
-use BookStack\Entities\EntityContextManager;
-use BookStack\Entities\Repos\EntityRepo;
+use BookStack\Entities\Book;
+use BookStack\Entities\Managers\EntityContext;
+use BookStack\Entities\Repos\BookshelfRepo;
+use BookStack\Exceptions\ImageUploadException;
+use BookStack\Exceptions\NotFoundException;
 use BookStack\Uploads\ImageRepo;
+use Exception;
 use Illuminate\Http\Request;
-use Illuminate\Http\Response;
+use Illuminate\Validation\ValidationException;
 use Views;
 
 class BookshelfController extends Controller
 {
 
-    protected $entityRepo;
-    protected $userRepo;
+    protected $bookshelfRepo;
     protected $entityContextManager;
     protected $imageRepo;
 
     /**
      * BookController constructor.
-     * @param EntityRepo $entityRepo
-     * @param UserRepo $userRepo
-     * @param EntityContextManager $entityContextManager
-     * @param ImageRepo $imageRepo
      */
-    public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, EntityContextManager $entityContextManager, ImageRepo $imageRepo)
+    public function __construct(BookshelfRepo $bookshelfRepo, EntityContext $entityContextManager, ImageRepo $imageRepo)
     {
-        $this->entityRepo = $entityRepo;
-        $this->userRepo = $userRepo;
+        $this->bookshelfRepo = $bookshelfRepo;
         $this->entityContextManager = $entityContextManager;
         $this->imageRepo = $imageRepo;
         parent::__construct();
@@ -36,27 +32,22 @@ class BookshelfController extends Controller
 
     /**
      * Display a listing of the book.
-     * @return Response
      */
     public function index()
     {
-        $view = setting()->getUser($this->currentUser, 'bookshelves_view_type', config('app.views.bookshelves', 'grid'));
-        $sort = setting()->getUser($this->currentUser, 'bookshelves_sort', 'name');
-        $order = setting()->getUser($this->currentUser, 'bookshelves_sort_order', 'asc');
+        $view = setting()->getForCurrentUser('bookshelves_view_type', config('app.views.bookshelves', 'grid'));
+        $sort = setting()->getForCurrentUser('bookshelves_sort', 'name');
+        $order = setting()->getForCurrentUser('bookshelves_sort_order', 'asc');
         $sortOptions = [
             'name' => trans('common.sort_name'),
             'created_at' => trans('common.sort_created_at'),
             'updated_at' => trans('common.sort_updated_at'),
         ];
 
-        $shelves = $this->entityRepo->getAllPaginated('bookshelf', 18, $sort, $order);
-        foreach ($shelves as $shelf) {
-            $shelf->books = $this->entityRepo->getBookshelfChildren($shelf);
-        }
-
-        $recents = $this->signedIn ? $this->entityRepo->getRecentlyViewed('bookshelf', 4, 0) : false;
-        $popular = $this->entityRepo->getPopular('bookshelf', 4, 0);
-        $new = $this->entityRepo->getRecentlyCreated('bookshelf', 4, 0);
+        $shelves = $this->bookshelfRepo->getAllPaginated(18, $sort, $order);
+        $recents = $this->isSignedIn() ? $this->bookshelfRepo->getRecentlyViewed(4) : false;
+        $popular = $this->bookshelfRepo->getPopular(4);
+        $new = $this->bookshelfRepo->getRecentlyCreated(4);
 
         $this->entityContextManager->clearShelfContext();
         $this->setPageTitle(trans('entities.shelves'));
@@ -74,21 +65,19 @@ class BookshelfController extends Controller
 
     /**
      * Show the form for creating a new bookshelf.
-     * @return Response
      */
     public function create()
     {
         $this->checkPermission('bookshelf-create-all');
-        $books = $this->entityRepo->getAll('book', false, 'update');
+        $books = Book::hasPermission('update')->get();
         $this->setPageTitle(trans('entities.shelves_create'));
         return view('shelves.create', ['books' => $books]);
     }
 
     /**
      * Store a newly created bookshelf in storage.
-     * @param Request $request
-     * @return Response
-     * @throws \BookStack\Exceptions\ImageUploadException
+     * @throws ValidationException
+     * @throws ImageUploadException
      */
     public function store(Request $request)
     {
@@ -96,80 +85,63 @@ class BookshelfController extends Controller
         $this->validate($request, [
             'name' => 'required|string|max:255',
             'description' => 'string|max:1000',
-            'image' => $this->imageRepo->getImageValidationRules(),
+            'image' => $this->getImageValidationRules(),
         ]);
 
-        $shelf = $this->entityRepo->createFromInput('bookshelf', $request->all());
-        $this->shelfUpdateActions($shelf, $request);
+        $bookIds = explode(',', $request->get('books', ''));
+        $shelf = $this->bookshelfRepo->create($request->all(), $bookIds);
+        $this->bookshelfRepo->updateCoverImage($shelf);
 
         Activity::add($shelf, 'bookshelf_create');
         return redirect($shelf->getUrl());
     }
 
-
     /**
-     * Display the specified bookshelf.
-     * @param String $slug
-     * @return Response
-     * @throws \BookStack\Exceptions\NotFoundException
+     * Display the bookshelf of the given slug.
+     * @throws NotFoundException
      */
     public function show(string $slug)
     {
-        /** @var Bookshelf $shelf */
-        $shelf = $this->entityRepo->getBySlug('bookshelf', $slug);
+        $shelf = $this->bookshelfRepo->getBySlug($slug);
         $this->checkOwnablePermission('book-view', $shelf);
 
-        $books = $this->entityRepo->getBookshelfChildren($shelf);
         Views::add($shelf);
         $this->entityContextManager->setShelfContext($shelf->id);
 
         $this->setPageTitle($shelf->getShortName());
-
         return view('shelves.show', [
             'shelf' => $shelf,
-            'books' => $books,
             'activity' => Activity::entityActivity($shelf, 20, 1)
         ]);
     }
 
     /**
      * Show the form for editing the specified bookshelf.
-     * @param $slug
-     * @return Response
-     * @throws \BookStack\Exceptions\NotFoundException
      */
     public function edit(string $slug)
     {
-        $shelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $shelf Bookshelf */
+        $shelf = $this->bookshelfRepo->getBySlug($slug);
         $this->checkOwnablePermission('bookshelf-update', $shelf);
 
-        $shelfBooks = $this->entityRepo->getBookshelfChildren($shelf);
-        $shelfBookIds = $shelfBooks->pluck('id');
-        $books = $this->entityRepo->getAll('book', false, 'update');
-        $books = $books->filter(function ($book) use ($shelfBookIds) {
-             return !$shelfBookIds->contains($book->id);
-        });
+        $shelfBookIds = $shelf->books()->get(['id'])->pluck('id');
+        $books = Book::hasPermission('update')->whereNotIn('id', $shelfBookIds)->get();
 
         $this->setPageTitle(trans('entities.shelves_edit_named', ['name' => $shelf->getShortName()]));
         return view('shelves.edit', [
             'shelf' => $shelf,
             'books' => $books,
-            'shelfBooks' => $shelfBooks,
         ]);
     }
 
-
     /**
      * Update the specified bookshelf in storage.
-     * @param Request $request
-     * @param string $slug
-     * @return Response
-     * @throws \BookStack\Exceptions\NotFoundException
-     * @throws \BookStack\Exceptions\ImageUploadException
+     * @throws ValidationException
+     * @throws ImageUploadException
+     * @throws NotFoundException
      */
     public function update(Request $request, string $slug)
     {
-        $shelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
+        $shelf = $this->bookshelfRepo->getBySlug($slug);
         $this->checkOwnablePermission('bookshelf-update', $shelf);
         $this->validate($request, [
             'name' => 'required|string|max:255',
@@ -177,24 +149,22 @@ class BookshelfController extends Controller
             'image' => $this->imageRepo->getImageValidationRules(),
         ]);
 
-         $shelf = $this->entityRepo->updateFromInput('bookshelf', $shelf, $request->all());
-         $this->shelfUpdateActions($shelf, $request);
 
-         Activity::add($shelf, 'bookshelf_update');
+        $bookIds = explode(',', $request->get('books', ''));
+        $shelf = $this->bookshelfRepo->update($shelf, $request->all(), $bookIds);
+        $resetCover = $request->has('image_reset');
+        $this->bookshelfRepo->updateCoverImage($shelf, $request->file('image', null), $resetCover);
+        Activity::add($shelf, 'bookshelf_update');
 
-         return redirect($shelf->getUrl());
+        return redirect($shelf->getUrl());
     }
 
-
     /**
      * Shows the page to confirm deletion
-     * @param $slug
-     * @return \Illuminate\View\View
-     * @throws \BookStack\Exceptions\NotFoundException
      */
     public function showDelete(string $slug)
     {
-        $shelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $shelf Bookshelf */
+        $shelf = $this->bookshelfRepo->getBySlug($slug);
         $this->checkOwnablePermission('bookshelf-delete', $shelf);
 
         $this->setPageTitle(trans('entities.shelves_delete_named', ['name' => $shelf->getShortName()]));
@@ -203,101 +173,58 @@ class BookshelfController extends Controller
 
     /**
      * Remove the specified bookshelf from storage.
-     * @param string $slug
-     * @return Response
-     * @throws \BookStack\Exceptions\NotFoundException
-     * @throws \Throwable
+     * @throws Exception
      */
     public function destroy(string $slug)
     {
-        $shelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $shelf Bookshelf */
+        $shelf = $this->bookshelfRepo->getBySlug($slug);
         $this->checkOwnablePermission('bookshelf-delete', $shelf);
-        Activity::addMessage('bookshelf_delete', 0, $shelf->name);
 
-        if ($shelf->cover) {
-            $this->imageRepo->destroyImage($shelf->cover);
-        }
-        $this->entityRepo->destroyBookshelf($shelf);
+        Activity::addMessage('bookshelf_delete', $shelf->name);
+        $this->bookshelfRepo->destroy($shelf);
 
         return redirect('/shelves');
     }
 
     /**
      * Show the permissions view.
-     * @param string $slug
-     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
-     * @throws \BookStack\Exceptions\NotFoundException
      */
     public function showPermissions(string $slug)
     {
-        $shelf = $this->entityRepo->getBySlug('bookshelf', $slug);
+        $shelf = $this->bookshelfRepo->getBySlug($slug);
         $this->checkOwnablePermission('restrictions-manage', $shelf);
 
-        $roles = $this->userRepo->getRestrictableRoles();
         return view('shelves.permissions', [
             'shelf' => $shelf,
-            'roles' => $roles
         ]);
     }
 
     /**
      * Set the permissions for this bookshelf.
-     * @param string $slug
-     * @param Request $request
-     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
-     * @throws \BookStack\Exceptions\NotFoundException
-     * @throws \Throwable
      */
-    public function permissions(string $slug, Request $request)
+    public function permissions(Request $request, string $slug)
     {
-        $shelf = $this->entityRepo->getBySlug('bookshelf', $slug);
+        $shelf = $this->bookshelfRepo->getBySlug($slug);
         $this->checkOwnablePermission('restrictions-manage', $shelf);
 
-        $this->entityRepo->updateEntityPermissionsFromRequest($request, $shelf);
-        session()->flash('success', trans('entities.shelves_permissions_updated'));
+        $restricted = $request->get('restricted') === 'true';
+        $permissions = $request->filled('restrictions') ? collect($request->get('restrictions')) : null;
+        $this->bookshelfRepo->updatePermissions($shelf, $restricted, $permissions);
+
+        $this->showSuccessNotification(trans('entities.shelves_permissions_updated'));
         return redirect($shelf->getUrl());
     }
 
     /**
      * Copy the permissions of a bookshelf to the child books.
-     * @param string $slug
-     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
-     * @throws \BookStack\Exceptions\NotFoundException
      */
     public function copyPermissions(string $slug)
     {
-        $shelf = $this->entityRepo->getBySlug('bookshelf', $slug);
+        $shelf = $this->bookshelfRepo->getBySlug($slug);
         $this->checkOwnablePermission('restrictions-manage', $shelf);
 
-        $updateCount = $this->entityRepo->copyBookshelfPermissions($shelf);
-        session()->flash('success', trans('entities.shelves_copy_permission_success', ['count' => $updateCount]));
+        $updateCount = $this->bookshelfRepo->copyDownPermissions($shelf);
+        $this->showSuccessNotification(trans('entities.shelves_copy_permission_success', ['count' => $updateCount]));
         return redirect($shelf->getUrl());
     }
-
-    /**
-     * Common actions to run on bookshelf update.
-     * @param Bookshelf $shelf
-     * @param Request $request
-     * @throws \BookStack\Exceptions\ImageUploadException
-     */
-    protected function shelfUpdateActions(Bookshelf $shelf, Request $request)
-    {
-        // Update the books that the shelf references
-        $this->entityRepo->updateShelfBooks($shelf, $request->get('books', ''));
-
-        // Update the cover image if in request
-        if ($request->has('image')) {
-            $newImage = $request->file('image');
-            $this->imageRepo->destroyImage($shelf->cover);
-            $image = $this->imageRepo->saveNew($newImage, 'cover_shelf', $shelf->id, 512, 512, true);
-            $shelf->image_id = $image->id;
-            $shelf->save();
-        }
-
-        if ($request->has('image_reset')) {
-            $this->imageRepo->destroyImage($shelf->cover);
-            $shelf->image_id = 0;
-            $shelf->save();
-        }
-    }
 }
index c19e45694850f7418ca8f5c98c5ae15a024847f9..1355979107eb0181d272e3610511688d5772b7b7 100644 (file)
@@ -1,83 +1,74 @@
 <?php namespace BookStack\Http\Controllers;
 
 use Activity;
-use BookStack\Auth\UserRepo;
-use BookStack\Entities\Repos\EntityRepo;
-use BookStack\Entities\ExportService;
+use BookStack\Entities\Book;
+use BookStack\Entities\Managers\BookContents;
+use BookStack\Entities\Repos\ChapterRepo;
+use BookStack\Exceptions\MoveOperationException;
+use BookStack\Exceptions\NotFoundException;
 use Illuminate\Http\Request;
-use Illuminate\Http\Response;
+use Illuminate\Validation\ValidationException;
+use Throwable;
 use Views;
 
 class ChapterController extends Controller
 {
 
-    protected $userRepo;
-    protected $entityRepo;
-    protected $exportService;
+    protected $chapterRepo;
 
     /**
      * ChapterController constructor.
-     * @param EntityRepo $entityRepo
-     * @param UserRepo $userRepo
-     * @param \BookStack\Entities\ExportService $exportService
      */
-    public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, ExportService $exportService)
+    public function __construct(ChapterRepo $chapterRepo)
     {
-        $this->entityRepo = $entityRepo;
-        $this->userRepo = $userRepo;
-        $this->exportService = $exportService;
+        $this->chapterRepo = $chapterRepo;
         parent::__construct();
     }
 
     /**
      * Show the form for creating a new chapter.
-     * @param $bookSlug
-     * @return Response
      */
-    public function create($bookSlug)
+    public function create(string $bookSlug)
     {
-        $book = $this->entityRepo->getBySlug('book', $bookSlug);
+        $book = Book::visible()->where('slug', '=', $bookSlug)->firstOrFail();
         $this->checkOwnablePermission('chapter-create', $book);
+
         $this->setPageTitle(trans('entities.chapters_create'));
         return view('chapters.create', ['book' => $book, 'current' => $book]);
     }
 
     /**
      * Store a newly created chapter in storage.
-     * @param          $bookSlug
-     * @param  Request $request
-     * @return Response
+     * @throws ValidationException
      */
-    public function store($bookSlug, Request $request)
+    public function store(Request $request, string $bookSlug)
     {
         $this->validate($request, [
             'name' => 'required|string|max:255'
         ]);
 
-        $book = $this->entityRepo->getBySlug('book', $bookSlug);
+        $book = Book::visible()->where('slug', '=', $bookSlug)->firstOrFail();
         $this->checkOwnablePermission('chapter-create', $book);
 
-        $input = $request->all();
-        $input['priority'] = $this->entityRepo->getNewBookPriority($book);
-        $chapter = $this->entityRepo->createFromInput('chapter', $input, $book);
+        $chapter = $this->chapterRepo->create($request->all(), $book);
         Activity::add($chapter, 'chapter_create', $book->id);
+
         return redirect($chapter->getUrl());
     }
 
     /**
      * Display the specified chapter.
-     * @param $bookSlug
-     * @param $chapterSlug
-     * @return Response
      */
-    public function show($bookSlug, $chapterSlug)
+    public function show(string $bookSlug, string $chapterSlug)
     {
-        $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
+        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
         $this->checkOwnablePermission('chapter-view', $chapter);
-        $sidebarTree = $this->entityRepo->getBookChildren($chapter->book);
+
+        $sidebarTree = (new BookContents($chapter->book))->getTree();
+        $pages = $chapter->getVisiblePages();
         Views::add($chapter);
+
         $this->setPageTitle($chapter->getShortName());
-        $pages = $this->entityRepo->getChapterChildren($chapter);
         return view('chapters.show', [
             'book' => $chapter->book,
             'chapter' => $chapter,
@@ -89,79 +80,71 @@ class ChapterController extends Controller
 
     /**
      * Show the form for editing the specified chapter.
-     * @param $bookSlug
-     * @param $chapterSlug
-     * @return Response
      */
-    public function edit($bookSlug, $chapterSlug)
+    public function edit(string $bookSlug, string $chapterSlug)
     {
-        $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
+        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
         $this->checkOwnablePermission('chapter-update', $chapter);
+
         $this->setPageTitle(trans('entities.chapters_edit_named', ['chapterName' => $chapter->getShortName()]));
         return view('chapters.edit', ['book' => $chapter->book, 'chapter' => $chapter, 'current' => $chapter]);
     }
 
     /**
      * Update the specified chapter in storage.
-     * @param  Request $request
-     * @param          $bookSlug
-     * @param          $chapterSlug
-     * @return Response
-     * @throws \BookStack\Exceptions\NotFoundException
+     * @throws NotFoundException
      */
-    public function update(Request $request, $bookSlug, $chapterSlug)
+    public function update(Request $request, string $bookSlug, string $chapterSlug)
     {
-        $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
+        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
         $this->checkOwnablePermission('chapter-update', $chapter);
 
-        $this->entityRepo->updateFromInput('chapter', $chapter, $request->all());
+        $this->chapterRepo->update($chapter, $request->all());
         Activity::add($chapter, 'chapter_update', $chapter->book->id);
+
         return redirect($chapter->getUrl());
     }
 
     /**
      * Shows the page to confirm deletion of this chapter.
-     * @param $bookSlug
-     * @param $chapterSlug
-     * @return \Illuminate\View\View
+     * @throws NotFoundException
      */
-    public function showDelete($bookSlug, $chapterSlug)
+    public function showDelete(string $bookSlug, string $chapterSlug)
     {
-        $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
+        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
         $this->checkOwnablePermission('chapter-delete', $chapter);
+
         $this->setPageTitle(trans('entities.chapters_delete_named', ['chapterName' => $chapter->getShortName()]));
         return view('chapters.delete', ['book' => $chapter->book, 'chapter' => $chapter, 'current' => $chapter]);
     }
 
     /**
      * Remove the specified chapter from storage.
-     * @param $bookSlug
-     * @param $chapterSlug
-     * @return Response
+     * @throws NotFoundException
+     * @throws Throwable
      */
-    public function destroy($bookSlug, $chapterSlug)
+    public function destroy(string $bookSlug, string $chapterSlug)
     {
-        $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
-        $book = $chapter->book;
+        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
         $this->checkOwnablePermission('chapter-delete', $chapter);
-        Activity::addMessage('chapter_delete', $book->id, $chapter->name);
-        $this->entityRepo->destroyChapter($chapter);
-        return redirect($book->getUrl());
+
+        Activity::addMessage('chapter_delete', $chapter->name, $chapter->book->id);
+        $this->chapterRepo->destroy($chapter);
+
+        return redirect($chapter->book->getUrl());
     }
 
     /**
      * Show the page for moving a chapter.
-     * @param $bookSlug
-     * @param $chapterSlug
-     * @return mixed
-     * @throws \BookStack\Exceptions\NotFoundException
+     * @throws NotFoundException
      */
-    public function showMove($bookSlug, $chapterSlug)
+    public function showMove(string $bookSlug, string $chapterSlug)
     {
-        $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
+        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
         $this->setPageTitle(trans('entities.chapters_move_named', ['chapterName' => $chapter->getShortName()]));
         $this->checkOwnablePermission('chapter-update', $chapter);
         $this->checkOwnablePermission('chapter-delete', $chapter);
+
         return view('chapters.move', [
             'chapter' => $chapter,
             'book' => $chapter->book
@@ -170,15 +153,11 @@ class ChapterController extends Controller
 
     /**
      * Perform the move action for a chapter.
-     * @param $bookSlug
-     * @param $chapterSlug
-     * @param Request $request
-     * @return mixed
-     * @throws \BookStack\Exceptions\NotFoundException
+     * @throws NotFoundException
      */
-    public function move($bookSlug, $chapterSlug, Request $request)
+    public function move(Request $request, string $bookSlug, string $chapterSlug)
     {
-        $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
+        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
         $this->checkOwnablePermission('chapter-update', $chapter);
         $this->checkOwnablePermission('chapter-delete', $chapter);
 
@@ -187,100 +166,47 @@ class ChapterController extends Controller
             return redirect($chapter->getUrl());
         }
 
-        $stringExploded = explode(':', $entitySelection);
-        $entityType = $stringExploded[0];
-        $entityId = intval($stringExploded[1]);
-
-        $parent = false;
-
-        if ($entityType == 'book') {
-            $parent = $this->entityRepo->getById('book', $entityId);
-        }
-
-        if ($parent === false || $parent === null) {
-            session()->flash('error', trans('errors.selected_book_not_found'));
+        try {
+            $newBook = $this->chapterRepo->move($chapter, $entitySelection);
+        } catch (MoveOperationException $exception) {
+            $this->showErrorNotification(trans('errors.selected_book_not_found'));
             return redirect()->back();
         }
 
-        $this->entityRepo->changeBook('chapter', $parent->id, $chapter, true);
-        Activity::add($chapter, 'chapter_move', $chapter->book->id);
-        session()->flash('success', trans('entities.chapter_move_success', ['bookName' => $parent->name]));
+        Activity::add($chapter, 'chapter_move', $newBook->id);
 
+        $this->showSuccessNotification(trans('entities.chapter_move_success', ['bookName' => $newBook->name]));
         return redirect($chapter->getUrl());
     }
 
     /**
      * Show the Restrictions view.
-     * @param $bookSlug
-     * @param $chapterSlug
-     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
-     * @throws \BookStack\Exceptions\NotFoundException
+     * @throws NotFoundException
      */
-    public function showPermissions($bookSlug, $chapterSlug)
+    public function showPermissions(string $bookSlug, string $chapterSlug)
     {
-        $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
+        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
         $this->checkOwnablePermission('restrictions-manage', $chapter);
-        $roles = $this->userRepo->getRestrictableRoles();
+
         return view('chapters.permissions', [
             'chapter' => $chapter,
-            'roles' => $roles
         ]);
     }
 
     /**
      * Set the restrictions for this chapter.
-     * @param $bookSlug
-     * @param $chapterSlug
-     * @param Request $request
-     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
-     * @throws \BookStack\Exceptions\NotFoundException
-     * @throws \Throwable
+     * @throws NotFoundException
      */
-    public function permissions($bookSlug, $chapterSlug, Request $request)
+    public function permissions(Request $request, string $bookSlug, string $chapterSlug)
     {
-        $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
+        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
         $this->checkOwnablePermission('restrictions-manage', $chapter);
-        $this->entityRepo->updateEntityPermissionsFromRequest($request, $chapter);
-        session()->flash('success', trans('entities.chapters_permissions_success'));
-        return redirect($chapter->getUrl());
-    }
 
-    /**
-     * Exports a chapter to pdf .
-     * @param string $bookSlug
-     * @param string $chapterSlug
-     * @return \Illuminate\Http\Response
-     */
-    public function exportPdf($bookSlug, $chapterSlug)
-    {
-        $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
-        $pdfContent = $this->exportService->chapterToPdf($chapter);
-        return $this->downloadResponse($pdfContent, $chapterSlug . '.pdf');
-    }
+        $restricted = $request->get('restricted') === 'true';
+        $permissions = $request->filled('restrictions') ? collect($request->get('restrictions')) : null;
+        $this->chapterRepo->updatePermissions($chapter, $restricted, $permissions);
 
-    /**
-     * Export a chapter to a self-contained HTML file.
-     * @param string $bookSlug
-     * @param string $chapterSlug
-     * @return \Illuminate\Http\Response
-     */
-    public function exportHtml($bookSlug, $chapterSlug)
-    {
-        $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
-        $containedHtml = $this->exportService->chapterToContainedHtml($chapter);
-        return $this->downloadResponse($containedHtml, $chapterSlug . '.html');
-    }
-
-    /**
-     * Export a chapter to a simple plaintext .txt file.
-     * @param string $bookSlug
-     * @param string $chapterSlug
-     * @return \Illuminate\Http\Response
-     */
-    public function exportPlainText($bookSlug, $chapterSlug)
-    {
-        $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
-        $chapterText = $this->exportService->chapterToPlainText($chapter);
-        return $this->downloadResponse($chapterText, $chapterSlug . '.txt');
+        $this->showSuccessNotification(trans('entities.chapters_permissions_success'));
+        return redirect($chapter->getUrl());
     }
 }
diff --git a/app/Http/Controllers/ChapterExportController.php b/app/Http/Controllers/ChapterExportController.php
new file mode 100644 (file)
index 0000000..0c86f85
--- /dev/null
@@ -0,0 +1,58 @@
+<?php namespace BookStack\Http\Controllers;
+
+use BookStack\Entities\ExportService;
+use BookStack\Entities\Repos\ChapterRepo;
+use BookStack\Exceptions\NotFoundException;
+use Throwable;
+
+class ChapterExportController extends Controller
+{
+
+    protected $chapterRepo;
+    protected $exportService;
+
+    /**
+     * ChapterExportController constructor.
+     */
+    public function __construct(ChapterRepo $chapterRepo, ExportService $exportService)
+    {
+        $this->chapterRepo = $chapterRepo;
+        $this->exportService = $exportService;
+        parent::__construct();
+    }
+
+    /**
+     * Exports a chapter to pdf.
+     * @throws NotFoundException
+     * @throws Throwable
+     */
+    public function pdf(string $bookSlug, string $chapterSlug)
+    {
+        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
+        $pdfContent = $this->exportService->chapterToPdf($chapter);
+        return $this->downloadResponse($pdfContent, $chapterSlug . '.pdf');
+    }
+
+    /**
+     * Export a chapter to a self-contained HTML file.
+     * @throws NotFoundException
+     * @throws Throwable
+     */
+    public function html(string $bookSlug, string $chapterSlug)
+    {
+        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
+        $containedHtml = $this->exportService->chapterToContainedHtml($chapter);
+        return $this->downloadResponse($containedHtml, $chapterSlug . '.html');
+    }
+
+    /**
+     * Export a chapter to a simple plaintext .txt file.
+     * @throws NotFoundException
+     */
+    public function plainText(string $bookSlug, string $chapterSlug)
+    {
+        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
+        $chapterText = $this->exportService->chapterToPlainText($chapter);
+        return $this->downloadResponse($chapterText, $chapterSlug . '.txt');
+    }
+}
index 860b507623fb6b5d09cd6a2a1f1a4aea5021fc35..068358d72d10af92de45327e0b207093c9c501fd 100644 (file)
@@ -2,44 +2,36 @@
 
 use Activity;
 use BookStack\Actions\CommentRepo;
-use BookStack\Entities\Repos\EntityRepo;
-use Illuminate\Database\Eloquent\ModelNotFoundException;
+use BookStack\Entities\Page;
 use Illuminate\Http\Request;
+use Illuminate\Validation\ValidationException;
 
 class CommentController extends Controller
 {
-    protected $entityRepo;
     protected $commentRepo;
 
     /**
      * CommentController constructor.
-     * @param \BookStack\Entities\Repos\EntityRepo $entityRepo
-     * @param \BookStack\Actions\CommentRepo $commentRepo
      */
-    public function __construct(EntityRepo $entityRepo, CommentRepo $commentRepo)
+    public function __construct(CommentRepo $commentRepo)
     {
-        $this->entityRepo = $entityRepo;
         $this->commentRepo = $commentRepo;
         parent::__construct();
     }
 
     /**
      * Save a new comment for a Page
-     * @param Request $request
-     * @param integer $pageId
-     * @param null|integer $commentId
-     * @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response
+     * @throws ValidationException
      */
-    public function savePageComment(Request $request, $pageId, $commentId = null)
+    public function savePageComment(Request $request, int $pageId, int $commentId = null)
     {
         $this->validate($request, [
             'text' => 'required|string',
             'html' => 'required|string',
         ]);
 
-        try {
-            $page = $this->entityRepo->getById('page', $pageId, true);
-        } catch (ModelNotFoundException $e) {
+        $page = Page::visible()->find($pageId);
+        if ($page === null) {
             return response('Not found', 404);
         }
 
@@ -59,11 +51,9 @@ class CommentController extends Controller
 
     /**
      * Update an existing comment.
-     * @param Request $request
-     * @param integer $commentId
-     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
+     * @throws ValidationException
      */
-    public function update(Request $request, $commentId)
+    public function update(Request $request, int $commentId)
     {
         $this->validate($request, [
             'text' => 'required|string',
@@ -80,13 +70,12 @@ class CommentController extends Controller
 
     /**
      * Delete a comment from the system.
-     * @param integer $id
-     * @return \Illuminate\Http\JsonResponse
      */
-    public function destroy($id)
+    public function destroy(int $id)
     {
         $comment = $this->commentRepo->getById($id);
         $this->checkOwnablePermission('comment-delete', $comment);
+
         $this->commentRepo->delete($comment);
         return response()->json(['message' => trans('entities.comment_deleted')]);
     }
index 5bc62c601a73bcf63913a2bac3bd63c25dbab2e1..b9576f2febd7923293b8eacb105bab6130f118c1 100644 (file)
@@ -2,7 +2,6 @@
 
 namespace BookStack\Http\Controllers;
 
-use BookStack\Auth\User;
 use BookStack\Ownable;
 use Illuminate\Foundation\Bus\DispatchesJobs;
 use Illuminate\Foundation\Validation\ValidatesRequests;
@@ -14,42 +13,27 @@ abstract class Controller extends BaseController
 {
     use DispatchesJobs, ValidatesRequests;
 
-    /**
-     * @var User static
-     */
-    protected $currentUser;
-    /**
-     * @var bool
-     */
-    protected $signedIn;
-
     /**
      * Controller constructor.
      */
     public function __construct()
     {
-        $this->middleware(function ($request, $next) {
-
-            // Get a user instance for the current user
-            $user = user();
-
-            // Share variables with controllers
-            $this->currentUser = $user;
-            $this->signedIn = auth()->check();
-
-            // Share variables with views
-            view()->share('signedIn', $this->signedIn);
-            view()->share('currentUser', $user);
+        //
+    }
 
-            return $next($request);
-        });
+    /**
+     * Check if the current user is signed in.
+     */
+    protected function isSignedIn(): bool
+    {
+        return auth()->check();
     }
 
     /**
      * Stops the application and shows a permission error if
      * the application is in demo mode.
      */
-    protected function preventAccessForDemoUsers()
+    protected function preventAccessInDemoMode()
     {
         if (config('app.env') === 'demo') {
             $this->showPermissionError();
@@ -75,7 +59,7 @@ abstract class Controller extends BaseController
             $response = response()->json(['error' => trans('errors.permissionJson')], 403);
         } else {
             $response = redirect('/');
-            session()->flash('error', trans('errors.permission'));
+            $this->showErrorNotification(trans('errors.permission'));
         }
 
         throw new HttpResponseException($response);
@@ -133,7 +117,7 @@ abstract class Controller extends BaseController
     protected function checkPermissionOrCurrentUser(string $permissionName, int $userId)
     {
         return $this->checkPermissionOr($permissionName, function () use ($userId) {
-            return $userId === $this->currentUser->id;
+            return $userId === user()->id;
         });
     }
 
@@ -145,7 +129,7 @@ abstract class Controller extends BaseController
      */
     protected function jsonError($messageText = "", $statusCode = 500)
     {
-        return response()->json(['message' => $messageText], $statusCode);
+        return response()->json(['message' => $messageText, 'status' => 'error'], $statusCode);
     }
 
     /**
@@ -178,4 +162,39 @@ abstract class Controller extends BaseController
             'Content-Disposition' => 'attachment; filename="' . $fileName . '"'
         ]);
     }
+
+    /**
+     * Show a positive, successful notification to the user on next view load.
+     * @param string $message
+     */
+    protected function showSuccessNotification(string $message)
+    {
+        session()->flash('success', $message);
+    }
+
+    /**
+     * Show a warning notification to the user on next view load.
+     * @param string $message
+     */
+    protected function showWarningNotification(string $message)
+    {
+        session()->flash('warning', $message);
+    }
+
+    /**
+     * Show an error notification to the user on next view load.
+     * @param string $message
+     */
+    protected function showErrorNotification(string $message)
+    {
+        session()->flash('error', $message);
+    }
+
+    /**
+     * Get the validation rules for image files.
+     */
+    protected function getImageValidationRules(): string
+    {
+        return 'image_extension|no_double_extension|mimes:jpeg,png,gif,bmp,webp,tiff';
+    }
 }
index ba93bfe6517ddd7a29664aa0eee45976217d98e3..260952fd16eb8e18962bc962ac3db1acc3f13445 100644 (file)
@@ -1,23 +1,16 @@
 <?php namespace BookStack\Http\Controllers;
 
 use Activity;
-use BookStack\Entities\Repos\EntityRepo;
+use BookStack\Entities\Book;
+use BookStack\Entities\Managers\PageContent;
+use BookStack\Entities\Page;
+use BookStack\Entities\Repos\BookRepo;
+use BookStack\Entities\Repos\BookshelfRepo;
 use Illuminate\Http\Response;
 use Views;
 
 class HomeController extends Controller
 {
-    protected $entityRepo;
-
-    /**
-     * HomeController constructor.
-     * @param EntityRepo $entityRepo
-     */
-    public function __construct(EntityRepo $entityRepo)
-    {
-        $this->entityRepo = $entityRepo;
-        parent::__construct();
-    }
 
     /**
      * Display the homepage.
@@ -26,10 +19,20 @@ class HomeController extends Controller
     public function index()
     {
         $activity = Activity::latest(10);
-        $draftPages = $this->signedIn ? $this->entityRepo->getUserDraftPages(6) : [];
+        $draftPages = [];
+
+        if ($this->isSignedIn()) {
+            $draftPages = Page::visible()->where('draft', '=', true)
+                ->where('created_by', '=', user()->id)
+                ->orderBy('updated_at', 'desc')->take(6)->get();
+        }
+
         $recentFactor = count($draftPages) > 0 ? 0.5 : 1;
-        $recents = $this->signedIn ? Views::getUserRecentlyViewed(12*$recentFactor, 0) : $this->entityRepo->getRecentlyCreated('book', 12*$recentFactor);
-        $recentlyUpdatedPages = $this->entityRepo->getRecentlyUpdated('page', 12);
+        $recents = $this->isSignedIn() ?
+              Views::getUserRecentlyViewed(12*$recentFactor, 0)
+            : Book::visible()->orderBy('created_at', 'desc')->take(12 * $recentFactor)->get();
+        $recentlyUpdatedPages = Page::visible()->where('draft', false)
+            ->orderBy('updated_at', 'desc')->take(12)->get();
 
         $homepageOptions = ['default', 'books', 'bookshelves', 'page'];
         $homepageOption = setting('app-homepage-type', 'default');
@@ -47,9 +50,9 @@ class HomeController extends Controller
         // Add required list ordering & sorting for books & shelves views.
         if ($homepageOption === 'bookshelves' || $homepageOption === 'books') {
             $key = $homepageOption;
-            $view = setting()->getUser($this->currentUser, $key . '_view_type', config('app.views.' . $key));
-            $sort = setting()->getUser($this->currentUser, $key . '_sort', 'name');
-            $order = setting()->getUser($this->currentUser, $key . '_sort_order', 'asc');
+            $view = setting()->getForCurrentUser($key . '_view_type', config('app.views.' . $key));
+            $sort = setting()->getForCurrentUser($key . '_sort', 'name');
+            $order = setting()->getForCurrentUser($key . '_sort_order', 'asc');
 
             $sortOptions = [
                 'name' => trans('common.sort_name'),
@@ -66,16 +69,18 @@ class HomeController extends Controller
         }
 
         if ($homepageOption === 'bookshelves') {
-            $shelves = $this->entityRepo->getAllPaginated('bookshelf', 18, $commonData['sort'], $commonData['order']);
+            $shelfRepo = app(BookshelfRepo::class);
+            $shelves = app(BookshelfRepo::class)->getAllPaginated(18, $commonData['sort'], $commonData['order']);
             foreach ($shelves as $shelf) {
-                $shelf->books = $this->entityRepo->getBookshelfChildren($shelf);
+                $shelf->books = $shelf->visibleBooks;
             }
             $data = array_merge($commonData, ['shelves' => $shelves]);
             return view('common.home-shelves', $data);
         }
 
         if ($homepageOption === 'books') {
-            $books = $this->entityRepo->getAllPaginated('book', 18, $commonData['sort'], $commonData['order']);
+            $bookRepo = app(BookRepo::class);
+            $books = $bookRepo->getAllPaginated(18, $commonData['sort'], $commonData['order']);
             $data = array_merge($commonData, ['books' => $books]);
             return view('common.home-book', $data);
         }
@@ -83,43 +88,15 @@ class HomeController extends Controller
         if ($homepageOption === 'page') {
             $homepageSetting = setting('app-homepage', '0:');
             $id = intval(explode(':', $homepageSetting)[0]);
-            $customHomepage = $this->entityRepo->getById('page', $id, false, true);
-            $this->entityRepo->renderPage($customHomepage, true);
+            $customHomepage = Page::query()->where('draft', '=', false)->findOrFail($id);
+            $pageContent = new PageContent($customHomepage);
+            $customHomepage->html = $pageContent->render(true);
             return view('common.home-custom', array_merge($commonData, ['customHomepage' => $customHomepage]));
         }
 
         return view('common.home', $commonData);
     }
 
-    /**
-     * Get a js representation of the current translations
-     * @return \Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\Response
-     * @throws \Exception
-     */
-    public function getTranslations()
-    {
-        $locale = app()->getLocale();
-        $cacheKey = 'GLOBAL_TRANSLATIONS_' . $locale;
-
-        if (cache()->has($cacheKey) && config('app.env') !== 'development') {
-            $resp = cache($cacheKey);
-        } else {
-            $translations = [
-                // Get only translations which might be used in JS
-                'common' => trans('common'),
-                'components' => trans('components'),
-                'entities' => trans('entities'),
-                'errors' => trans('errors')
-            ];
-            $resp = 'window.translations = ' . json_encode($translations);
-            cache()->put($cacheKey, $resp, 120);
-        }
-
-        return response($resp, 200, [
-            'Content-Type' => 'application/javascript'
-        ]);
-    }
-
     /**
      * Get custom head HTML, Used in ajax calls to show in editor.
      * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
index 024003f87d13c4a40611b7942ce83d3cbdcf9a43..9c67704dd339bd0b21d3471b751206625386869b 100644 (file)
@@ -1,6 +1,6 @@
 <?php namespace BookStack\Http\Controllers\Images;
 
-use BookStack\Entities\Repos\EntityRepo;
+use BookStack\Entities\Page;
 use BookStack\Exceptions\ImageUploadException;
 use BookStack\Http\Controllers\Controller;
 use BookStack\Repos\PageRepo;
@@ -47,13 +47,13 @@ class ImageController extends Controller
 
     /**
      * Update image details
-     * @param integer $id
      * @param Request $request
+     * @param integer $id
      * @return \Illuminate\Http\JsonResponse
      * @throws ImageUploadException
      * @throws \Exception
      */
-    public function update($id, Request $request)
+    public function update(Request $request, $id)
     {
         $this->validate($request, [
             'name' => 'required|min:2|string'
@@ -69,16 +69,21 @@ class ImageController extends Controller
 
     /**
      * Show the usage of an image on pages.
-     * @param \BookStack\Entities\Repos\EntityRepo $entityRepo
-     * @param $id
-     * @return \Illuminate\Http\JsonResponse
      */
-    public function usage(EntityRepo $entityRepo, $id)
+    public function usage(int $id)
     {
         $image = $this->imageRepo->getById($id);
         $this->checkImagePermission($image);
-        $pageSearch = $entityRepo->searchForImage($image->url);
-        return response()->json($pageSearch);
+
+        $pages = Page::visible()->where('html', 'like', '%' . $image->url . '%')->get(['id', 'name', 'slug', 'book_id']);
+        foreach ($pages as $page) {
+            $page->url = $page->getUrl();
+            $page->html = '';
+            $page->text = '';
+        }
+        $result = count($pages) > 0 ? $pages : false;
+
+        return response()->json($result);
     }
 
     /**
index 16a7d5a5e45df6df1094bfa14df63fb17cb278f3..630f888ed3e9dc0d16da96b622a03278b527cfb9 100644 (file)
@@ -1,61 +1,46 @@
 <?php namespace BookStack\Http\Controllers;
 
 use Activity;
-use BookStack\Auth\UserRepo;
-use BookStack\Entities\Repos\EntityRepo;
-use BookStack\Entities\ExportService;
+use BookStack\Entities\Managers\BookContents;
+use BookStack\Entities\Managers\PageContent;
+use BookStack\Entities\Managers\PageEditActivity;
+use BookStack\Entities\Page;
 use BookStack\Entities\Repos\PageRepo;
 use BookStack\Exceptions\NotFoundException;
-use GatherContent\Htmldiff\Htmldiff;
+use BookStack\Exceptions\NotifyException;
+use BookStack\Exceptions\PermissionsException;
+use Exception;
 use Illuminate\Http\Request;
-use Illuminate\Http\Response;
+use Illuminate\Validation\ValidationException;
+use Throwable;
 use Views;
 
 class PageController extends Controller
 {
 
     protected $pageRepo;
-    protected $exportService;
-    protected $userRepo;
 
     /**
      * PageController constructor.
-     * @param \BookStack\Entities\Repos\PageRepo $pageRepo
-     * @param \BookStack\Entities\ExportService $exportService
-     * @param UserRepo $userRepo
      */
-    public function __construct(PageRepo $pageRepo, ExportService $exportService, UserRepo $userRepo)
+    public function __construct(PageRepo $pageRepo)
     {
         $this->pageRepo = $pageRepo;
-        $this->exportService = $exportService;
-        $this->userRepo = $userRepo;
         parent::__construct();
     }
 
     /**
      * Show the form for creating a new page.
-     * @param string $bookSlug
-     * @param string $chapterSlug
-     * @return Response
-     * @internal param bool $pageSlug
-     * @throws NotFoundException
+     * @throws Throwable
      */
-    public function create($bookSlug, $chapterSlug = null)
+    public function create(string $bookSlug, string $chapterSlug = null)
     {
-        if ($chapterSlug !== null) {
-            $chapter = $this->pageRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
-            $book = $chapter->book;
-        } else {
-            $chapter = null;
-            $book = $this->pageRepo->getBySlug('book', $bookSlug);
-        }
-
-        $parent = $chapter ? $chapter : $book;
+        $parent = $this->pageRepo->getParentFromSlugs($bookSlug, $chapterSlug);
         $this->checkOwnablePermission('page-create', $parent);
 
         // Redirect to draft edit screen if signed in
-        if ($this->signedIn) {
-            $draft = $this->pageRepo->getDraftPage($book, $chapter);
+        if ($this->isSignedIn()) {
+            $draft = $this->pageRepo->getNewDraftPage($parent);
             return redirect($draft->getUrl());
         }
 
@@ -66,117 +51,94 @@ class PageController extends Controller
 
     /**
      * Create a new page as a guest user.
-     * @param Request $request
-     * @param string $bookSlug
-     * @param string|null $chapterSlug
-     * @return mixed
-     * @throws NotFoundException
+     * @throws ValidationException
      */
-    public function createAsGuest(Request $request, $bookSlug, $chapterSlug = null)
+    public function createAsGuest(Request $request, string $bookSlug, string $chapterSlug = null)
     {
         $this->validate($request, [
             'name' => 'required|string|max:255'
         ]);
 
-        if ($chapterSlug !== null) {
-            $chapter = $this->pageRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
-            $book = $chapter->book;
-        } else {
-            $chapter = null;
-            $book = $this->pageRepo->getBySlug('book', $bookSlug);
-        }
-
-        $parent = $chapter ? $chapter : $book;
+        $parent = $this->pageRepo->getParentFromSlugs($bookSlug, $chapterSlug);
         $this->checkOwnablePermission('page-create', $parent);
 
-        $page = $this->pageRepo->getDraftPage($book, $chapter);
-        $this->pageRepo->publishPageDraft($page, [
+        $page = $this->pageRepo->getNewDraftPage($parent);
+        $this->pageRepo->publishDraft($page, [
             'name' => $request->get('name'),
             'html' => ''
         ]);
+
         return redirect($page->getUrl('/edit'));
     }
 
     /**
      * Show form to continue editing a draft page.
-     * @param string $bookSlug
-     * @param int $pageId
-     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
+     * @throws NotFoundException
      */
-    public function editDraft($bookSlug, $pageId)
+    public function editDraft(string $bookSlug, int $pageId)
     {
-        $draft = $this->pageRepo->getById('page', $pageId, true);
-        $this->checkOwnablePermission('page-create', $draft->parent);
+        $draft = $this->pageRepo->getById($pageId);
+        $this->checkOwnablePermission('page-create', $draft->parent());
         $this->setPageTitle(trans('entities.pages_edit_draft'));
 
-        $draftsEnabled = $this->signedIn;
+        $draftsEnabled = $this->isSignedIn();
+        $templates = $this->pageRepo->getTemplates(10);
+
         return view('pages.edit', [
             'page' => $draft,
             'book' => $draft->book,
             'isDraft' => true,
-            'draftsEnabled' => $draftsEnabled
+            'draftsEnabled' => $draftsEnabled,
+            'templates' => $templates,
         ]);
     }
 
     /**
      * Store a new page by changing a draft into a page.
-     * @param  Request $request
-     * @param  string $bookSlug
-     * @param  int $pageId
-     * @return Response
+     * @throws NotFoundException
+     * @throws ValidationException
      */
-    public function store(Request $request, $bookSlug, $pageId)
+    public function store(Request $request, string $bookSlug, int $pageId)
     {
         $this->validate($request, [
             'name' => 'required|string|max:255'
         ]);
+        $draftPage = $this->pageRepo->getById($pageId);
+        $this->checkOwnablePermission('page-create', $draftPage->parent());
 
-        $input = $request->all();
-        $draftPage = $this->pageRepo->getById('page', $pageId, true);
-        $book = $draftPage->book;
-
-        $parent = $draftPage->parent;
-        $this->checkOwnablePermission('page-create', $parent);
-
-        if ($parent->isA('chapter')) {
-            $input['priority'] = $this->pageRepo->getNewChapterPriority($parent);
-        } else {
-            $input['priority'] = $this->pageRepo->getNewBookPriority($parent);
-        }
-
-        $page = $this->pageRepo->publishPageDraft($draftPage, $input);
+        $page = $this->pageRepo->publishDraft($draftPage, $request->all());
+        Activity::add($page, 'page_create', $draftPage->book->id);
 
-        Activity::add($page, 'page_create', $book->id);
         return redirect($page->getUrl());
     }
 
     /**
      * Display the specified page.
      * If the page is not found via the slug the revisions are searched for a match.
-     * @param string $bookSlug
-     * @param string $pageSlug
-     * @return Response
      * @throws NotFoundException
      */
-    public function show($bookSlug, $pageSlug)
+    public function show(string $bookSlug, string $pageSlug)
     {
         try {
-            $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
+            $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
         } catch (NotFoundException $e) {
-            $page = $this->pageRepo->getPageByOldSlug($pageSlug, $bookSlug);
+            $page = $this->pageRepo->getByOldSlug($bookSlug, $pageSlug);
+
             if ($page === null) {
                 throw $e;
             }
+
             return redirect($page->getUrl());
         }
 
         $this->checkOwnablePermission('page-view', $page);
 
-        $page->html = $this->pageRepo->renderPage($page);
-        $sidebarTree = $this->pageRepo->getBookChildren($page->book);
-        $pageNav = $this->pageRepo->getPageNav($page->html);
+        $pageContent = (new PageContent($page));
+        $page->html = $pageContent->render();
+        $sidebarTree = (new BookContents($page->book))->getTree();
+        $pageNav = $pageContent->getNavigation($page->html);
 
-        // check if the comment's are enabled
+        // Check if page comments are enabled
         $commentsEnabled = !setting('app-disable-comments');
         if ($commentsEnabled) {
             $page->load(['comments.createdBy']);
@@ -185,7 +147,8 @@ class PageController extends Controller
         Views::add($page);
         $this->setPageTitle($page->getShortName());
         return view('pages.show', [
-            'page' => $page,'book' => $page->book,
+            'page' => $page,
+            'book' => $page->book,
             'current' => $page,
             'sidebarTree' => $sidebarTree,
             'commentsEnabled' => $commentsEnabled,
@@ -195,93 +158,86 @@ class PageController extends Controller
 
     /**
      * Get page from an ajax request.
-     * @param int $pageId
-     * @return \Illuminate\Http\JsonResponse
+     * @throws NotFoundException
      */
-    public function getPageAjax($pageId)
+    public function getPageAjax(int $pageId)
     {
-        $page = $this->pageRepo->getById('page', $pageId);
+        $page = $this->pageRepo->getById($pageId);
         return response()->json($page);
     }
 
     /**
      * Show the form for editing the specified page.
-     * @param string $bookSlug
-     * @param string $pageSlug
-     * @return Response
      * @throws NotFoundException
      */
-    public function edit($bookSlug, $pageSlug)
+    public function edit(string $bookSlug, string $pageSlug)
     {
-        $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
+        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
         $this->checkOwnablePermission('page-update', $page);
-        $this->setPageTitle(trans('entities.pages_editing_named', ['pageName'=>$page->getShortName()]));
+
         $page->isDraft = false;
+        $editActivity = new PageEditActivity($page);
 
         // Check for active editing
         $warnings = [];
-        if ($this->pageRepo->isPageEditingActive($page, 60)) {
-            $warnings[] = $this->pageRepo->getPageEditingActiveMessage($page, 60);
+        if ($editActivity->hasActiveEditing()) {
+            $warnings[] = $editActivity->activeEditingMessage();
         }
 
         // Check for a current draft version for this user
-        $userPageDraft = $this->pageRepo->getUserPageDraft($page, $this->currentUser->id);
-        if ($userPageDraft !== null) {
-            $page->name = $userPageDraft->name;
-            $page->html = $userPageDraft->html;
-            $page->markdown = $userPageDraft->markdown;
+        $userDraft = $this->pageRepo->getUserDraft($page);
+        if ($userDraft !== null) {
+            $page->forceFill($userDraft->only(['name', 'html', 'markdown']));
             $page->isDraft = true;
-            $warnings [] = $this->pageRepo->getUserPageDraftMessage($userPageDraft);
+            $warnings[] = $editActivity->getEditingActiveDraftMessage($userDraft);
         }
 
         if (count($warnings) > 0) {
-            session()->flash('warning', implode("\n", $warnings));
+            $this->showWarningNotification(implode("\n", $warnings));
         }
 
-        $draftsEnabled = $this->signedIn;
+        $templates = $this->pageRepo->getTemplates(10);
+        $draftsEnabled = $this->isSignedIn();
+        $this->setPageTitle(trans('entities.pages_editing_named', ['pageName' => $page->getShortName()]));
         return view('pages.edit', [
             'page' => $page,
             'book' => $page->book,
             'current' => $page,
-            'draftsEnabled' => $draftsEnabled
+            'draftsEnabled' => $draftsEnabled,
+            'templates' => $templates,
         ]);
     }
 
     /**
      * Update the specified page in storage.
-     * @param  Request $request
-     * @param  string $bookSlug
-     * @param  string $pageSlug
-     * @return Response
+     * @throws ValidationException
+     * @throws NotFoundException
      */
-    public function update(Request $request, $bookSlug, $pageSlug)
+    public function update(Request $request, string $bookSlug, string $pageSlug)
     {
         $this->validate($request, [
             'name' => 'required|string|max:255'
         ]);
-        $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
+        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
         $this->checkOwnablePermission('page-update', $page);
-        $this->pageRepo->updatePage($page, $page->book->id, $request->all());
+
+        $this->pageRepo->update($page, $request->all());
         Activity::add($page, 'page_update', $page->book->id);
+
         return redirect($page->getUrl());
     }
 
     /**
      * Save a draft update as a revision.
-     * @param Request $request
-     * @param int $pageId
-     * @return \Illuminate\Http\JsonResponse
+     * @throws NotFoundException
      */
-    public function saveDraft(Request $request, $pageId)
+    public function saveDraft(Request $request, int $pageId)
     {
-        $page = $this->pageRepo->getById('page', $pageId, true);
+        $page = $this->pageRepo->getById($pageId);
         $this->checkOwnablePermission('page-update', $page);
 
-        if (!$this->signedIn) {
-            return response()->json([
-                'status' => 'error',
-                'message' => trans('errors.guests_cannot_save_drafts'),
-            ], 500);
+        if (!$this->isSignedIn()) {
+            return $this->jsonError(trans('errors.guests_cannot_save_drafts'), 500);
         }
 
         $draft = $this->pageRepo->updatePageDraft($page, $request->only(['name', 'html', 'markdown']));
@@ -295,253 +251,98 @@ class PageController extends Controller
     }
 
     /**
-     * Redirect from a special link url which
-     * uses the page id rather than the name.
-     * @param int $pageId
-     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
+     * Redirect from a special link url which uses the page id rather than the name.
+     * @throws NotFoundException
      */
-    public function redirectFromLink($pageId)
+    public function redirectFromLink(int $pageId)
     {
-        $page = $this->pageRepo->getById('page', $pageId);
+        $page = $this->pageRepo->getById($pageId);
         return redirect($page->getUrl());
     }
 
     /**
      * Show the deletion page for the specified page.
-     * @param string $bookSlug
-     * @param string $pageSlug
-     * @return \Illuminate\View\View
+     * @throws NotFoundException
      */
-    public function showDelete($bookSlug, $pageSlug)
+    public function showDelete(string $bookSlug, string $pageSlug)
     {
-        $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
+        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
         $this->checkOwnablePermission('page-delete', $page);
         $this->setPageTitle(trans('entities.pages_delete_named', ['pageName'=>$page->getShortName()]));
-        return view('pages.delete', ['book' => $page->book, 'page' => $page, 'current' => $page]);
+        return view('pages.delete', [
+            'book' => $page->book,
+            'page' => $page,
+            'current' => $page
+        ]);
     }
 
-
     /**
      * Show the deletion page for the specified page.
-     * @param string $bookSlug
-     * @param int $pageId
-     * @return \Illuminate\View\View
      * @throws NotFoundException
      */
-    public function showDeleteDraft($bookSlug, $pageId)
+    public function showDeleteDraft(string $bookSlug, int $pageId)
     {
-        $page = $this->pageRepo->getById('page', $pageId, true);
+        $page = $this->pageRepo->getById($pageId);
         $this->checkOwnablePermission('page-update', $page);
         $this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName'=>$page->getShortName()]));
-        return view('pages.delete', ['book' => $page->book, 'page' => $page, 'current' => $page]);
+        return view('pages.delete', [
+            'book' => $page->book,
+            'page' => $page,
+            'current' => $page
+        ]);
     }
 
     /**
      * Remove the specified page from storage.
-     * @param string $bookSlug
-     * @param string $pageSlug
-     * @return Response
-     * @internal param int $id
+     * @throws NotFoundException
+     * @throws Throwable
+     * @throws NotifyException
      */
-    public function destroy($bookSlug, $pageSlug)
+    public function destroy(string $bookSlug, string $pageSlug)
     {
-        $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
-        $book = $page->book;
+        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
         $this->checkOwnablePermission('page-delete', $page);
-        $this->pageRepo->destroyPage($page);
 
-        Activity::addMessage('page_delete', $book->id, $page->name);
-        session()->flash('success', trans('entities.pages_delete_success'));
+        $book = $page->book;
+        $this->pageRepo->destroy($page);
+        Activity::addMessage('page_delete', $page->name, $book->id);
+
+        $this->showSuccessNotification(trans('entities.pages_delete_success'));
         return redirect($book->getUrl());
     }
 
     /**
      * Remove the specified draft page from storage.
-     * @param string $bookSlug
-     * @param int $pageId
-     * @return Response
      * @throws NotFoundException
+     * @throws NotifyException
+     * @throws Throwable
      */
-    public function destroyDraft($bookSlug, $pageId)
+    public function destroyDraft(string $bookSlug, int $pageId)
     {
-        $page = $this->pageRepo->getById('page', $pageId, true);
+        $page = $this->pageRepo->getById($pageId);
         $book = $page->book;
+        $chapter = $page->chapter;
         $this->checkOwnablePermission('page-update', $page);
-        session()->flash('success', trans('entities.pages_delete_draft_success'));
-        $this->pageRepo->destroyPage($page);
-        return redirect($book->getUrl());
-    }
-
-    /**
-     * Shows the last revisions for this page.
-     * @param string $bookSlug
-     * @param string $pageSlug
-     * @return \Illuminate\View\View
-     * @throws NotFoundException
-     */
-    public function showRevisions($bookSlug, $pageSlug)
-    {
-        $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
-        $this->setPageTitle(trans('entities.pages_revisions_named', ['pageName'=>$page->getShortName()]));
-        return view('pages.revisions', ['page' => $page, 'current' => $page]);
-    }
 
-    /**
-     * Shows a preview of a single revision
-     * @param string $bookSlug
-     * @param string $pageSlug
-     * @param int $revisionId
-     * @return \Illuminate\View\View
-     */
-    public function showRevision($bookSlug, $pageSlug, $revisionId)
-    {
-        $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
-        $revision = $page->revisions()->where('id', '=', $revisionId)->first();
-        if ($revision === null) {
-            abort(404);
-        }
+        $this->pageRepo->destroy($page);
 
-        $page->fill($revision->toArray());
-        $this->setPageTitle(trans('entities.pages_revision_named', ['pageName' => $page->getShortName()]));
+        $this->showSuccessNotification(trans('entities.pages_delete_draft_success'));
 
-        return view('pages.revision', [
-            'page' => $page,
-            'book' => $page->book,
-            'diff' => null,
-            'revision' => $revision
-        ]);
-    }
-
-    /**
-     * Shows the changes of a single revision
-     * @param string $bookSlug
-     * @param string $pageSlug
-     * @param int $revisionId
-     * @return \Illuminate\View\View
-     */
-    public function showRevisionChanges($bookSlug, $pageSlug, $revisionId)
-    {
-        $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
-        $revision = $page->revisions()->where('id', '=', $revisionId)->first();
-        if ($revision === null) {
-            abort(404);
+        if ($chapter && userCan('view', $chapter)) {
+            return redirect($chapter->getUrl());
         }
-
-        $prev = $revision->getPrevious();
-        $prevContent = ($prev === null) ? '' : $prev->html;
-        $diff = (new Htmldiff)->diff($prevContent, $revision->html);
-
-        $page->fill($revision->toArray());
-        $this->setPageTitle(trans('entities.pages_revision_named', ['pageName'=>$page->getShortName()]));
-
-        return view('pages.revision', [
-            'page' => $page,
-            'book' => $page->book,
-            'diff' => $diff,
-            'revision' => $revision
-        ]);
-    }
-
-    /**
-     * Restores a page using the content of the specified revision.
-     * @param string $bookSlug
-     * @param string $pageSlug
-     * @param int $revisionId
-     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
-     */
-    public function restoreRevision($bookSlug, $pageSlug, $revisionId)
-    {
-        $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
-        $this->checkOwnablePermission('page-update', $page);
-        $page = $this->pageRepo->restorePageRevision($page, $page->book, $revisionId);
-        Activity::add($page, 'page_restore', $page->book->id);
-        return redirect($page->getUrl());
-    }
-
-
-    /**
-     * Deletes a revision using the id of the specified revision.
-     * @param string $bookSlug
-     * @param string $pageSlug
-     * @param int $revId
-     * @throws NotFoundException
-     * @throws BadRequestException
-     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
-     */
-    public function destroyRevision($bookSlug, $pageSlug, $revId)
-    {
-        $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
-        $this->checkOwnablePermission('page-delete', $page);
-
-        $revision = $page->revisions()->where('id', '=', $revId)->first();
-        if ($revision === null) {
-            throw new NotFoundException("Revision #{$revId} not found");
-        }
-
-        // Get the current revision for the page
-        $currentRevision = $page->getCurrentRevision();
-
-        // Check if its the latest revision, cannot delete latest revision.
-        if (intval($currentRevision->id) === intval($revId)) {
-            session()->flash('error', trans('entities.revision_cannot_delete_latest'));
-            return response()->view('pages.revisions', ['page' => $page, 'book' => $page->book, 'current' => $page], 400);
-        }
-
-        $revision->delete();
-        session()->flash('success', trans('entities.revision_delete_success'));
-        return view('pages.revisions', ['page' => $page, 'book' => $page->book, 'current' => $page]);
-    }
-
-    /**
-     * Exports a page to a PDF.
-     * https://github.com/barryvdh/laravel-dompdf
-     * @param string $bookSlug
-     * @param string $pageSlug
-     * @return \Illuminate\Http\Response
-     */
-    public function exportPdf($bookSlug, $pageSlug)
-    {
-        $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
-        $page->html = $this->pageRepo->renderPage($page);
-        $pdfContent = $this->exportService->pageToPdf($page);
-        return $this->downloadResponse($pdfContent, $pageSlug . '.pdf');
-    }
-
-    /**
-     * Export a page to a self-contained HTML file.
-     * @param string $bookSlug
-     * @param string $pageSlug
-     * @return \Illuminate\Http\Response
-     */
-    public function exportHtml($bookSlug, $pageSlug)
-    {
-        $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
-        $page->html = $this->pageRepo->renderPage($page);
-        $containedHtml = $this->exportService->pageToContainedHtml($page);
-        return $this->downloadResponse($containedHtml, $pageSlug . '.html');
-    }
-
-    /**
-     * Export a page to a simple plaintext .txt file.
-     * @param string $bookSlug
-     * @param string $pageSlug
-     * @return \Illuminate\Http\Response
-     */
-    public function exportPlainText($bookSlug, $pageSlug)
-    {
-        $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
-        $pageText = $this->exportService->pageToPlainText($page);
-        return $this->downloadResponse($pageText, $pageSlug . '.txt');
+        return redirect($book->getUrl());
     }
 
     /**
-     * Show a listing of recently created pages
-     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
+     * Show a listing of recently created pages.
      */
     public function showRecentlyUpdated()
     {
-        // TODO - Still exist?
-        $pages = $this->pageRepo->getRecentlyUpdatedPaginated('page', 20)->setPath(baseUrl('/pages/recently-updated'));
+        $pages = Page::visible()->orderBy('updated_at', 'desc')
+            ->paginate(20)
+            ->setPath(url('/pages/recently-updated'));
+
         return view('pages.detailed-listing', [
             'title' => trans('entities.recently_updated_pages'),
             'pages' => $pages
@@ -550,14 +351,11 @@ class PageController extends Controller
 
     /**
      * Show the view to choose a new parent to move a page into.
-     * @param string $bookSlug
-     * @param string $pageSlug
-     * @return mixed
      * @throws NotFoundException
      */
-    public function showMove($bookSlug, $pageSlug)
+    public function showMove(string $bookSlug, string $pageSlug)
     {
-        $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
+        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
         $this->checkOwnablePermission('page-update', $page);
         $this->checkOwnablePermission('page-delete', $page);
         return view('pages.move', [
@@ -567,16 +365,13 @@ class PageController extends Controller
     }
 
     /**
-     * Does the action of moving the location of a page
-     * @param string $bookSlug
-     * @param string $pageSlug
-     * @param Request $request
-     * @return mixed
+     * Does the action of moving the location of a page.
      * @throws NotFoundException
+     * @throws Throwable
      */
-    public function move($bookSlug, $pageSlug, Request $request)
+    public function move(Request $request, string $bookSlug, string $pageSlug)
     {
-        $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
+        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
         $this->checkOwnablePermission('page-update', $page);
         $this->checkOwnablePermission('page-delete', $page);
 
@@ -585,37 +380,29 @@ class PageController extends Controller
             return redirect($page->getUrl());
         }
 
-        $stringExploded = explode(':', $entitySelection);
-        $entityType = $stringExploded[0];
-        $entityId = intval($stringExploded[1]);
-
-
         try {
-            $parent = $this->pageRepo->getById($entityType, $entityId);
-        } catch (\Exception $e) {
-            session()->flash(trans('entities.selected_book_chapter_not_found'));
+            $parent = $this->pageRepo->move($page, $entitySelection);
+        } catch (Exception $exception) {
+            if ($exception instanceof  PermissionsException) {
+                $this->showPermissionError();
+            }
+
+            $this->showErrorNotification(trans('errors.selected_book_chapter_not_found'));
             return redirect()->back();
         }
 
-        $this->checkOwnablePermission('page-create', $parent);
-
-        $this->pageRepo->changePageParent($page, $parent);
         Activity::add($page, 'page_move', $page->book->id);
-        session()->flash('success', trans('entities.pages_move_success', ['parentName' => $parent->name]));
-
+        $this->showSuccessNotification(trans('entities.pages_move_success', ['parentName' => $parent->name]));
         return redirect($page->getUrl());
     }
 
     /**
      * Show the view to copy a page.
-     * @param string $bookSlug
-     * @param string $pageSlug
-     * @return mixed
      * @throws NotFoundException
      */
-    public function showCopy($bookSlug, $pageSlug)
+    public function showCopy(string $bookSlug, string $pageSlug)
     {
-        $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
+        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
         $this->checkOwnablePermission('page-view', $page);
         session()->flashInput(['name' => $page->name]);
         return view('pages.copy', [
@@ -624,78 +411,65 @@ class PageController extends Controller
         ]);
     }
 
+
     /**
      * Create a copy of a page within the requested target destination.
-     * @param string $bookSlug
-     * @param string $pageSlug
-     * @param Request $request
-     * @return mixed
      * @throws NotFoundException
+     * @throws Throwable
      */
-    public function copy($bookSlug, $pageSlug, Request $request)
+    public function copy(Request $request, string $bookSlug, string $pageSlug)
     {
-        $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
+        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
         $this->checkOwnablePermission('page-view', $page);
 
-        $entitySelection = $request->get('entity_selection', null);
-        if ($entitySelection === null || $entitySelection === '') {
-            $parent = $page->chapter ? $page->chapter : $page->book;
-        } else {
-            $stringExploded = explode(':', $entitySelection);
-            $entityType = $stringExploded[0];
-            $entityId = intval($stringExploded[1]);
-
-            try {
-                $parent = $this->pageRepo->getById($entityType, $entityId);
-            } catch (\Exception $e) {
-                session()->flash(trans('entities.selected_book_chapter_not_found'));
-                return redirect()->back();
-            }
-        }
+        $entitySelection = $request->get('entity_selection', null) ?? null;
+        $newName = $request->get('name', null);
 
-        $this->checkOwnablePermission('page-create', $parent);
+        try {
+            $pageCopy = $this->pageRepo->copy($page, $entitySelection, $newName);
+        } catch (Exception $exception) {
+            if ($exception instanceof  PermissionsException) {
+                $this->showPermissionError();
+            }
 
-        $pageCopy = $this->pageRepo->copyPage($page, $parent, $request->get('name', ''));
+            $this->showErrorNotification(trans('errors.selected_book_chapter_not_found'));
+            return redirect()->back();
+        }
 
         Activity::add($pageCopy, 'page_create', $pageCopy->book->id);
-        session()->flash('success', trans('entities.pages_copy_success'));
 
+        $this->showSuccessNotification(trans('entities.pages_copy_success'));
         return redirect($pageCopy->getUrl());
     }
 
     /**
      * Show the Permissions view.
-     * @param string $bookSlug
-     * @param string $pageSlug
-     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
      * @throws NotFoundException
      */
-    public function showPermissions($bookSlug, $pageSlug)
+    public function showPermissions(string $bookSlug, string $pageSlug)
     {
-        $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
+        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
         $this->checkOwnablePermission('restrictions-manage', $page);
-        $roles = $this->userRepo->getRestrictableRoles();
         return view('pages.permissions', [
             'page'  => $page,
-            'roles' => $roles
         ]);
     }
 
     /**
      * Set the permissions for this page.
-     * @param string $bookSlug
-     * @param string $pageSlug
-     * @param Request $request
-     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
      * @throws NotFoundException
-     * @throws \Throwable
+     * @throws Throwable
      */
-    public function permissions($bookSlug, $pageSlug, Request $request)
+    public function permissions(Request $request, string $bookSlug, string $pageSlug)
     {
-        $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
+        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
         $this->checkOwnablePermission('restrictions-manage', $page);
-        $this->pageRepo->updateEntityPermissionsFromRequest($request, $page);
-        session()->flash('success', trans('entities.pages_permissions_success'));
+
+        $restricted = $request->get('restricted') === 'true';
+        $permissions = $request->filled('restrictions') ? collect($request->get('restrictions')) : null;
+        $this->pageRepo->updatePermissions($page, $restricted, $permissions);
+
+        $this->showSuccessNotification(trans('entities.pages_permissions_success'));
         return redirect($page->getUrl());
     }
 }
diff --git a/app/Http/Controllers/PageExportController.php b/app/Http/Controllers/PageExportController.php
new file mode 100644 (file)
index 0000000..3b02ea2
--- /dev/null
@@ -0,0 +1,66 @@
+<?php
+
+namespace BookStack\Http\Controllers;
+
+use BookStack\Entities\ExportService;
+use BookStack\Entities\Managers\PageContent;
+use BookStack\Entities\Repos\PageRepo;
+use BookStack\Exceptions\NotFoundException;
+use Throwable;
+
+class PageExportController extends Controller
+{
+
+    protected $pageRepo;
+    protected $exportService;
+
+    /**
+     * PageExportController constructor.
+     * @param PageRepo $pageRepo
+     * @param ExportService $exportService
+     */
+    public function __construct(PageRepo $pageRepo, ExportService $exportService)
+    {
+        $this->pageRepo = $pageRepo;
+        $this->exportService = $exportService;
+        parent::__construct();
+    }
+
+    /**
+     * Exports a page to a PDF.
+     * https://github.com/barryvdh/laravel-dompdf
+     * @throws NotFoundException
+     * @throws Throwable
+     */
+    public function pdf(string $bookSlug, string $pageSlug)
+    {
+        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+        $page->html = (new PageContent($page))->render();
+        $pdfContent = $this->exportService->pageToPdf($page);
+        return $this->downloadResponse($pdfContent, $pageSlug . '.pdf');
+    }
+
+    /**
+     * Export a page to a self-contained HTML file.
+     * @throws NotFoundException
+     * @throws Throwable
+     */
+    public function html(string $bookSlug, string $pageSlug)
+    {
+        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+        $page->html = (new PageContent($page))->render();
+        $containedHtml = $this->exportService->pageToContainedHtml($page);
+        return $this->downloadResponse($containedHtml, $pageSlug . '.html');
+    }
+
+    /**
+     * Export a page to a simple plaintext .txt file.
+     * @throws NotFoundException
+     */
+    public function plainText(string $bookSlug, string $pageSlug)
+    {
+        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+        $pageText = $this->exportService->pageToPlainText($page);
+        return $this->downloadResponse($pageText, $pageSlug . '.txt');
+    }
+}
diff --git a/app/Http/Controllers/PageRevisionController.php b/app/Http/Controllers/PageRevisionController.php
new file mode 100644 (file)
index 0000000..3c65b50
--- /dev/null
@@ -0,0 +1,128 @@
+<?php namespace BookStack\Http\Controllers;
+
+use BookStack\Entities\Repos\PageRepo;
+use BookStack\Exceptions\NotFoundException;
+use BookStack\Facades\Activity;
+use GatherContent\Htmldiff\Htmldiff;
+
+class PageRevisionController extends Controller
+{
+
+    protected $pageRepo;
+
+    /**
+     * PageRevisionController constructor.
+     */
+    public function __construct(PageRepo $pageRepo)
+    {
+        $this->pageRepo = $pageRepo;
+        parent::__construct();
+    }
+
+    /**
+     * Shows the last revisions for this page.
+     * @throws NotFoundException
+     */
+    public function index(string $bookSlug, string $pageSlug)
+    {
+        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+        $this->setPageTitle(trans('entities.pages_revisions_named', ['pageName'=>$page->getShortName()]));
+        return view('pages.revisions', [
+            'page' => $page,
+            'current' => $page
+        ]);
+    }
+
+    /**
+     * Shows a preview of a single revision.
+     * @throws NotFoundException
+     */
+    public function show(string $bookSlug, string $pageSlug, int $revisionId)
+    {
+        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+        $revision = $page->revisions()->where('id', '=', $revisionId)->first();
+        if ($revision === null) {
+            throw new NotFoundException();
+        }
+
+        $page->fill($revision->toArray());
+
+        $this->setPageTitle(trans('entities.pages_revision_named', ['pageName' => $page->getShortName()]));
+        return view('pages.revision', [
+            'page' => $page,
+            'book' => $page->book,
+            'diff' => null,
+            'revision' => $revision
+        ]);
+    }
+
+    /**
+     * Shows the changes of a single revision.
+     * @throws NotFoundException
+     */
+    public function changes(string $bookSlug, string $pageSlug, int $revisionId)
+    {
+        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+        $revision = $page->revisions()->where('id', '=', $revisionId)->first();
+        if ($revision === null) {
+            throw new NotFoundException();
+        }
+
+        $prev = $revision->getPrevious();
+        $prevContent = $prev->html ?? '';
+        $diff = (new Htmldiff)->diff($prevContent, $revision->html);
+
+        $page->fill($revision->toArray());
+        $this->setPageTitle(trans('entities.pages_revision_named', ['pageName'=>$page->getShortName()]));
+
+        return view('pages.revision', [
+            'page' => $page,
+            'book' => $page->book,
+            'diff' => $diff,
+            'revision' => $revision
+        ]);
+    }
+
+    /**
+     * Restores a page using the content of the specified revision.
+     * @throws NotFoundException
+     */
+    public function restore(string $bookSlug, string $pageSlug, int $revisionId)
+    {
+        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+        $this->checkOwnablePermission('page-update', $page);
+
+        $page = $this->pageRepo->restoreRevision($page, $revisionId);
+
+        Activity::add($page, 'page_restore', $page->book->id);
+        return redirect($page->getUrl());
+    }
+
+    /**
+     * Deletes a revision using the id of the specified revision.
+     * @throws NotFoundException
+     */
+    public function destroy(string $bookSlug, string $pageSlug, int $revId)
+    {
+        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+        $this->checkOwnablePermission('page-delete', $page);
+
+        $revision = $page->revisions()->where('id', '=', $revId)->first();
+        if ($revision === null) {
+            throw new NotFoundException("Revision #{$revId} not found");
+        }
+
+        // Get the current revision for the page
+        $currentRevision = $page->getCurrentRevision();
+
+        // Check if its the latest revision, cannot delete latest revision.
+        if (intval($currentRevision->id) === intval($revId)) {
+            $this->showErrorNotification(trans('entities.revision_cannot_delete_latest'));
+            return redirect($page->getUrl('/revisions'));
+        }
+
+        $revision->delete();
+        $this->showSuccessNotification(trans('entities.revision_delete_success'));
+        return redirect($page->getUrl('/revisions'));
+    }
+}
diff --git a/app/Http/Controllers/PageTemplateController.php b/app/Http/Controllers/PageTemplateController.php
new file mode 100644 (file)
index 0000000..eaa1a8a
--- /dev/null
@@ -0,0 +1,57 @@
+<?php
+
+namespace BookStack\Http\Controllers;
+
+use BookStack\Entities\Repos\PageRepo;
+use BookStack\Exceptions\NotFoundException;
+use Illuminate\Http\Request;
+
+class PageTemplateController extends Controller
+{
+    protected $pageRepo;
+
+    /**
+     * PageTemplateController constructor
+     */
+    public function __construct(PageRepo $pageRepo)
+    {
+        $this->pageRepo = $pageRepo;
+        parent::__construct();
+    }
+
+    /**
+     * Fetch a list of templates from the system.
+     */
+    public function list(Request $request)
+    {
+        $page = $request->get('page', 1);
+        $search = $request->get('search', '');
+        $templates = $this->pageRepo->getTemplates(10, $page, $search);
+
+        if ($search) {
+            $templates->appends(['search' => $search]);
+        }
+
+        return view('pages.template-manager-list', [
+            'templates' => $templates
+        ]);
+    }
+
+    /**
+     * Get the content of a template.
+     * @throws NotFoundException
+     */
+    public function get(int $templateId)
+    {
+        $page = $this->pageRepo->getById($templateId);
+
+        if (!$page->template) {
+            throw new NotFoundException();
+        }
+
+        return response()->json([
+            'html' => $page->html,
+            'markdown' => $page->markdown,
+        ]);
+    }
+}
index 9893d59935ff6d284d98d5e335e2b452089de15e..148ae5cd65a3049e421ea2b331f3bfa34f31ed4d 100644 (file)
@@ -53,7 +53,7 @@ class PermissionController extends Controller
         ]);
 
         $this->permissionsRepo->saveNewRole($request->all());
-        session()->flash('success', trans('settings.role_create_success'));
+        $this->showSuccessNotification(trans('settings.role_create_success'));
         return redirect('/settings/roles');
     }
 
@@ -75,12 +75,13 @@ class PermissionController extends Controller
 
     /**
      * Updates a user role.
-     * @param $id
      * @param Request $request
+     * @param $id
      * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
      * @throws PermissionsException
+     * @throws \Illuminate\Validation\ValidationException
      */
-    public function updateRole($id, Request $request)
+    public function updateRole(Request $request, $id)
     {
         $this->checkPermission('user-roles-manage');
         $this->validate($request, [
@@ -89,7 +90,7 @@ class PermissionController extends Controller
         ]);
 
         $this->permissionsRepo->updateRole($id, $request->all());
-        session()->flash('success', trans('settings.role_update_success'));
+        $this->showSuccessNotification(trans('settings.role_update_success'));
         return redirect('/settings/roles');
     }
 
@@ -112,22 +113,22 @@ class PermissionController extends Controller
     /**
      * Delete a role from the system,
      * Migrate from a previous role if set.
-     * @param $id
      * @param Request $request
+     * @param $id
      * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
      */
-    public function deleteRole($id, Request $request)
+    public function deleteRole(Request $request, $id)
     {
         $this->checkPermission('user-roles-manage');
 
         try {
             $this->permissionsRepo->deleteRole($id, $request->get('migrate_role_id'));
         } catch (PermissionsException $e) {
-            session()->flash('error', $e->getMessage());
+            $this->showErrorNotification($e->getMessage());
             return redirect()->back();
         }
 
-        session()->flash('success', trans('settings.role_delete_success'));
+        $this->showSuccessNotification(trans('settings.role_delete_success'));
         return redirect('/settings/roles');
     }
 }
index 4bcf7b40eff4db0c207f7741438a4da9b52bd113..a5cd7ad6b82224f094173f1d76b2d4a864b48174 100644 (file)
@@ -1,35 +1,27 @@
 <?php namespace BookStack\Http\Controllers;
 
 use BookStack\Actions\ViewService;
-use BookStack\Entities\EntityContextManager;
-use BookStack\Entities\Repos\EntityRepo;
+use BookStack\Entities\Book;
+use BookStack\Entities\Bookshelf;
+use BookStack\Entities\Entity;
+use BookStack\Entities\Managers\EntityContext;
 use BookStack\Entities\SearchService;
-use BookStack\Exceptions\NotFoundException;
-use Illuminate\Contracts\View\Factory;
 use Illuminate\Http\Request;
-use Illuminate\View\View;
 
 class SearchController extends Controller
 {
-    protected $entityRepo;
     protected $viewService;
     protected $searchService;
     protected $entityContextManager;
 
     /**
      * SearchController constructor.
-     * @param EntityRepo $entityRepo
-     * @param ViewService $viewService
-     * @param SearchService $searchService
-     * @param EntityContextManager $entityContextManager
      */
     public function __construct(
-        EntityRepo $entityRepo,
         ViewService $viewService,
         SearchService $searchService,
-        EntityContextManager $entityContextManager
+        EntityContext $entityContextManager
     ) {
-        $this->entityRepo = $entityRepo;
         $this->viewService = $viewService;
         $this->searchService = $searchService;
         $this->entityContextManager = $entityContextManager;
@@ -38,9 +30,6 @@ class SearchController extends Controller
 
     /**
      * Searches all entities.
-     * @param Request $request
-     * @return View
-     * @internal param string $searchTerm
      */
     public function search(Request $request)
     {
@@ -48,7 +37,7 @@ class SearchController extends Controller
         $this->setPageTitle(trans('entities.search_for_term', ['term' => $searchTerm]));
 
         $page = intval($request->get('page', '0')) ?: 1;
-        $nextPageLink = baseUrl('/search?term=' . urlencode($searchTerm) . '&page=' . ($page+1));
+        $nextPageLink = url('/search?term=' . urlencode($searchTerm) . '&page=' . ($page+1));
 
         $results = $this->searchService->searchEntities($searchTerm, 'all', $page, 20);
 
@@ -64,12 +53,8 @@ class SearchController extends Controller
 
     /**
      * Searches all entities within a book.
-     * @param Request $request
-     * @param integer $bookId
-     * @return View
-     * @internal param string $searchTerm
      */
-    public function searchBook(Request $request, $bookId)
+    public function searchBook(Request $request, int $bookId)
     {
         $term = $request->get('term', '');
         $results = $this->searchService->searchBook($bookId, $term);
@@ -78,12 +63,8 @@ class SearchController extends Controller
 
     /**
      * Searches all entities within a chapter.
-     * @param Request $request
-     * @param integer $chapterId
-     * @return View
-     * @internal param string $searchTerm
      */
-    public function searchChapter(Request $request, $chapterId)
+    public function searchChapter(Request $request, int $chapterId)
     {
         $term = $request->get('term', '');
         $results = $this->searchService->searchChapter($chapterId, $term);
@@ -93,8 +74,6 @@ class SearchController extends Controller
     /**
      * Search for a list of entities and return a partial HTML response of matching entities.
      * Returns the most popular entities if no search is provided.
-     * @param Request $request
-     * @return mixed
      */
     public function searchEntitiesAjax(Request $request)
     {
@@ -115,15 +94,13 @@ class SearchController extends Controller
 
     /**
      * Search siblings items in the system.
-     * @param Request $request
-     * @return Factory|View|mixed
      */
     public function searchSiblings(Request $request)
     {
         $type = $request->get('entity_type', null);
         $id = $request->get('entity_id', null);
 
-        $entity = $this->entityRepo->getById($type, $id);
+        $entity = Entity::getEntityInstance($type)->newQuery()->visible()->find($id);
         if (!$entity) {
             return $this->jsonError(trans('errors.entity_not_found'), 404);
         }
@@ -132,12 +109,12 @@ class SearchController extends Controller
 
         // Page in chapter
         if ($entity->isA('page') && $entity->chapter) {
-            $entities = $this->entityRepo->getChapterChildren($entity->chapter);
+            $entities = $entity->chapter->visiblePages();
         }
 
         // Page in book or chapter
         if (($entity->isA('page') && !$entity->chapter) || $entity->isA('chapter')) {
-            $entities = $this->entityRepo->getBookDirectChildren($entity->book);
+            $entities = $entity->book->getDirectChildren();
         }
 
         // Book
@@ -145,15 +122,15 @@ class SearchController extends Controller
         if ($entity->isA('book')) {
             $contextShelf = $this->entityContextManager->getContextualShelfForBook($entity);
             if ($contextShelf) {
-                $entities = $this->entityRepo->getBookshelfChildren($contextShelf);
+                $entities = $contextShelf->visibleBooks()->get();
             } else {
-                $entities = $this->entityRepo->getAll('book');
+                $entities = Book::visible()->get();
             }
         }
 
         // Shelve
         if ($entity->isA('bookshelf')) {
-            $entities = $this->entityRepo->getAll('bookshelf');
+            $entities = Bookshelf::visible()->get();
         }
 
         return view('partials.entity-list-basic', ['entities' => $entities, 'style' => 'compact']);
index 650833c7f33bd7a9a5d5e6e97a4446688c22b775..1146f22c749ec8df442dfe6a278b3922a3a1fdea 100644 (file)
@@ -47,7 +47,7 @@ class SettingController extends Controller
      */
     public function update(Request $request)
     {
-        $this->preventAccessForDemoUsers();
+        $this->preventAccessInDemoMode();
         $this->checkPermission('settings-manage');
         $this->validate($request, [
             'app_logo' => $this->imageRepo->getImageValidationRules(),
@@ -76,7 +76,7 @@ class SettingController extends Controller
             setting()->remove('app-logo');
         }
 
-        session()->flash('success', trans('settings.settings_save_success'));
+        $this->showSuccessNotification(trans('settings.settings_save_success'));
         return redirect('/settings');
     }
 
@@ -111,14 +111,14 @@ class SettingController extends Controller
         $imagesToDelete = $imageService->deleteUnusedImages($checkRevisions, $dryRun);
         $deleteCount = count($imagesToDelete);
         if ($deleteCount === 0) {
-            session()->flash('warning', trans('settings.maint_image_cleanup_nothing_found'));
+            $this->showWarningNotification(trans('settings.maint_image_cleanup_nothing_found'));
             return redirect('/settings/maintenance')->withInput();
         }
 
         if ($dryRun) {
             session()->flash('cleanup-images-warning', trans('settings.maint_image_cleanup_warning', ['count' => $deleteCount]));
         } else {
-            session()->flash('success', trans('settings.maint_image_cleanup_success', ['count' => $deleteCount]));
+            $this->showSuccessNotification(trans('settings.maint_image_cleanup_success', ['count' => $deleteCount]));
         }
 
         return redirect('/settings/maintenance#image-cleanup')->withInput();
index 8191fbfe276226ab70bed45825d470b353905b88..b55398d2f6f644cb35cec75383df75be9dd40b71 100644 (file)
@@ -1,30 +1,35 @@
 <?php namespace BookStack\Http\Controllers;
 
 use BookStack\Auth\Access\SocialAuthService;
+use BookStack\Auth\Access\UserInviteService;
 use BookStack\Auth\User;
 use BookStack\Auth\UserRepo;
 use BookStack\Exceptions\UserUpdateException;
 use BookStack\Uploads\ImageRepo;
 use Illuminate\Http\Request;
 use Illuminate\Http\Response;
+use Illuminate\Support\Str;
 
 class UserController extends Controller
 {
 
     protected $user;
     protected $userRepo;
+    protected $inviteService;
     protected $imageRepo;
 
     /**
      * UserController constructor.
      * @param User $user
      * @param UserRepo $userRepo
+     * @param UserInviteService $inviteService
      * @param ImageRepo $imageRepo
      */
-    public function __construct(User $user, UserRepo $userRepo, ImageRepo $imageRepo)
+    public function __construct(User $user, UserRepo $userRepo, UserInviteService $inviteService, ImageRepo $imageRepo)
     {
         $this->user = $user;
         $this->userRepo = $userRepo;
+        $this->inviteService = $inviteService;
         $this->imageRepo = $imageRepo;
         parent::__construct();
     }
@@ -75,8 +80,10 @@ class UserController extends Controller
         ];
 
         $authMethod = config('auth.method');
-        if ($authMethod === 'standard') {
-            $validationRules['password'] = 'required|min:5';
+        $sendInvite = ($request->get('send_invite', 'false') === 'true');
+
+        if ($authMethod === 'standard' && !$sendInvite) {
+            $validationRules['password'] = 'required|min:6';
             $validationRules['password-confirm'] = 'required|same:password';
         } elseif ($authMethod === 'ldap') {
             $validationRules['external_auth_id'] = 'required';
@@ -86,13 +93,17 @@ class UserController extends Controller
         $user = $this->user->fill($request->all());
 
         if ($authMethod === 'standard') {
-            $user->password = bcrypt($request->get('password'));
+            $user->password = bcrypt($request->get('password', Str::random(32)));
         } elseif ($authMethod === 'ldap') {
             $user->external_auth_id = $request->get('external_auth_id');
         }
 
         $user->save();
 
+        if ($sendInvite) {
+            $this->inviteService->sendInvitation($user);
+        }
+
         if ($request->filled('roles')) {
             $roles = $request->get('roles');
             $this->userRepo->setUserRoles($user, $roles);
@@ -133,20 +144,25 @@ class UserController extends Controller
      */
     public function update(Request $request, $id)
     {
-        $this->preventAccessForDemoUsers();
+        $this->preventAccessInDemoMode();
         $this->checkPermissionOrCurrentUser('users-manage', $id);
 
         $this->validate($request, [
             'name'             => 'min:2',
             'email'            => 'min:2|email|unique:users,email,' . $id,
-            'password'         => 'min:5|required_with:password_confirm',
+            'password'         => 'min:6|required_with:password_confirm',
             'password-confirm' => 'same:password|required_with:password',
             'setting'          => 'array',
             'profile_image'    => $this->imageRepo->getImageValidationRules(),
         ]);
 
         $user = $this->userRepo->getById($id);
-        $user->fill($request->all());
+        $user->fill($request->except(['email']));
+
+        // Email updates
+        if (userCan('users-manage') && $request->filled('email')) {
+            $user->email = $request->get('email');
+        }
 
         // Role updates
         if (userCan('users-manage') && $request->filled('roles')) {
@@ -161,7 +177,7 @@ class UserController extends Controller
         }
 
         // External auth id updates
-        if ($this->currentUser->can('users-manage') && $request->filled('external_auth_id')) {
+        if (user()->can('users-manage') && $request->filled('external_auth_id')) {
             $user->external_auth_id = $request->get('external_auth_id');
         }
 
@@ -186,7 +202,7 @@ class UserController extends Controller
         }
 
         $user->save();
-        session()->flash('success', trans('settings.users_edit_success'));
+        $this->showSuccessNotification(trans('settings.users_edit_success'));
 
         $redirectUrl = userCan('users-manage') ? '/settings/users' : ('/settings/users/' . $user->id);
         return redirect($redirectUrl);
@@ -214,23 +230,23 @@ class UserController extends Controller
      */
     public function destroy($id)
     {
-        $this->preventAccessForDemoUsers();
+        $this->preventAccessInDemoMode();
         $this->checkPermissionOrCurrentUser('users-manage', $id);
 
         $user = $this->userRepo->getById($id);
 
         if ($this->userRepo->isOnlyAdmin($user)) {
-            session()->flash('error', trans('errors.users_cannot_delete_only_admin'));
+            $this->showErrorNotification(trans('errors.users_cannot_delete_only_admin'));
             return redirect($user->getEditUrl());
         }
 
         if ($user->system_name === 'public') {
-            session()->flash('error', trans('errors.users_cannot_delete_guest'));
+            $this->showErrorNotification(trans('errors.users_cannot_delete_guest'));
             return redirect($user->getEditUrl());
         }
 
         $this->userRepo->destroy($user);
-        session()->flash('success', trans('settings.users_delete_success'));
+        $this->showSuccessNotification(trans('settings.users_delete_success'));
 
         return redirect('/settings/users');
     }
@@ -245,7 +261,7 @@ class UserController extends Controller
         $user = $this->userRepo->getById($id);
 
         $userActivity = $this->userRepo->getActivity($user);
-        $recentlyCreated = $this->userRepo->getRecentlyCreated($user, 5, 0);
+        $recentlyCreated = $this->userRepo->getRecentlyCreated($user, 5);
         $assetCounts = $this->userRepo->getAssetCounts($user);
 
         return view('users.profile', [
@@ -258,22 +274,22 @@ class UserController extends Controller
 
     /**
      * Update the user's preferred book-list display setting.
-     * @param $id
      * @param Request $request
+     * @param $id
      * @return \Illuminate\Http\RedirectResponse
      */
-    public function switchBookView($id, Request $request)
+    public function switchBookView(Request $request, $id)
     {
         return $this->switchViewType($id, $request, 'books');
     }
 
     /**
      * Update the user's preferred shelf-list display setting.
-     * @param $id
      * @param Request $request
+     * @param $id
      * @return \Illuminate\Http\RedirectResponse
      */
-    public function switchShelfView($id, Request $request)
+    public function switchShelfView(Request $request, $id)
     {
         return $this->switchViewType($id, $request, 'bookshelves');
     }
@@ -303,12 +319,12 @@ class UserController extends Controller
 
     /**
      * Change the stored sort type for a particular view.
+     * @param Request $request
      * @param string $id
      * @param string $type
-     * @param Request $request
      * @return \Illuminate\Http\RedirectResponse
      */
-    public function changeSort(string $id, string $type, Request $request)
+    public function changeSort(Request $request, string $id, string $type)
     {
         $validSortTypes = ['books', 'bookshelves'];
         if (!in_array($type, $validSortTypes)) {
@@ -319,12 +335,12 @@ class UserController extends Controller
 
     /**
      * Update the stored section expansion preference for the given user.
+     * @param Request $request
      * @param string $id
      * @param string $key
-     * @param Request $request
      * @return \Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\Response
      */
-    public function updateExpansionPreference(string $id, string $key, Request $request)
+    public function updateExpansionPreference(Request $request, string $id, string $key)
     {
         $this->checkPermissionOrCurrentUser('users-manage', $id);
         $keyWhitelist = ['home-details'];
index cd894de95340471f87e73c876cac8e2a4e49a657..f9752da09d6492430dd2fe2f4c131dcac301aedf 100644 (file)
@@ -12,7 +12,7 @@ class Kernel extends HttpKernel
      * @var array
      */
     protected $middleware = [
-        \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
+        \BookStack\Http\Middleware\CheckForMaintenanceMode::class,
         \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
         \BookStack\Http\Middleware\TrimStrings::class,
         \BookStack\Http\Middleware\TrustProxies::class,
@@ -29,9 +29,11 @@ class Kernel extends HttpKernel
             \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
             \Illuminate\Session\Middleware\StartSession::class,
             \Illuminate\View\Middleware\ShareErrorsFromSession::class,
+            \Illuminate\Routing\Middleware\ThrottleRequests::class,
             \BookStack\Http\Middleware\VerifyCsrfToken::class,
             \Illuminate\Routing\Middleware\SubstituteBindings::class,
-            \BookStack\Http\Middleware\Localization::class
+            \BookStack\Http\Middleware\Localization::class,
+            \BookStack\Http\Middleware\GlobalViewData::class,
         ],
         'api' => [
             'throttle:60,1',
index 1a33843675a97266095cd6c2cb96918369652c9d..d840a9b2e05477c8fca1550dad9e9e81adaa8c2c 100644 (file)
@@ -41,7 +41,7 @@ class Authenticate
             if ($request->ajax()) {
                 return response('Unauthorized.', 401);
             } else {
-                return redirect()->guest(baseUrl('/login'));
+                return redirect()->guest(url('/login'));
             }
         }
 
diff --git a/app/Http/Middleware/CheckForMaintenanceMode.php b/app/Http/Middleware/CheckForMaintenanceMode.php
new file mode 100644 (file)
index 0000000..0c76838
--- /dev/null
@@ -0,0 +1,17 @@
+<?php
+
+namespace BookStack\Http\Middleware;
+
+use Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode as Middleware;
+
+class CheckForMaintenanceMode extends Middleware
+{
+    /**
+     * The URIs that should be reachable while maintenance mode is enabled.
+     *
+     * @var array
+     */
+    protected $except = [
+        //
+    ];
+}
diff --git a/app/Http/Middleware/GlobalViewData.php b/app/Http/Middleware/GlobalViewData.php
new file mode 100644 (file)
index 0000000..bc132df
--- /dev/null
@@ -0,0 +1,27 @@
+<?php namespace BookStack\Http\Middleware;
+
+use Closure;
+use Illuminate\Http\Request;
+
+/**
+ * Class GlobalViewData
+ * Sets up data that is accessible to any view rendered by the web routes.
+ */
+class GlobalViewData
+{
+
+    /**
+     * Handle an incoming request.
+     *
+     * @param Request $request
+     * @param Closure $next
+     * @return mixed
+     */
+    public function handle(Request $request, Closure $next)
+    {
+        view()->share('signedIn', auth()->check());
+        view()->share('currentUser', user());
+
+        return $next($request);
+    }
+}
index ff5526cc70d61f6fbdcde2aa35a63c034d74ff1a..8fe4ca9906c7f62ab61ece1df2f18ce5d0150f08 100644 (file)
@@ -31,14 +31,13 @@ class Localization
         'nl' => 'nl_NL',
         'pl' => 'pl_PL',
         'pt_BR' => 'pt_BR',
-        'pt_BR' => 'pt_BR',
         'ru' => 'ru',
         'sk' => 'sk_SK',
         'sv' => 'sv_SE',
         'uk' => 'uk_UA',
-        'uk' => 'uk_UA',
         'zh_CN' => 'zh_CN',
         'zh_TW' => 'zh_TW',
+        'tr' => 'tr_TR',
     ];
 
     /**
@@ -59,6 +58,8 @@ class Localization
             $locale = setting()->getUser(user(), 'language', $defaultLang);
         }
 
+        config()->set('app.lang', str_replace('_', '-', $this->getLocaleIso($locale)));
+
         // Set text direction
         if (in_array($locale, $this->rtlLocales)) {
             config()->set('app.rtl', true);
@@ -88,6 +89,16 @@ class Localization
         return $default;
     }
 
+    /**
+     * Get the ISO version of a BookStack language name
+     * @param  string $locale
+     * @return string
+     */
+    public function getLocaleIso(string $locale)
+    {
+        return $this->localeMap[$locale] ?? $locale;
+    }
+
     /**
      * Set the system date locale for localized date formatting.
      * Will try both the standard locale name and the UTF8 variant.
@@ -95,7 +106,7 @@ class Localization
      */
     protected function setSystemDateLocale(string $locale)
     {
-        $systemLocale = $this->localeMap[$locale] ?? $locale;
+        $systemLocale = $this->getLocaleIso($locale);
         $set = setlocale(LC_TIME, $systemLocale);
         if ($set === false) {
             setlocale(LC_TIME, $systemLocale . '.utf8');
index 73c11a82769d861ed0ab1793bd7eb7473466d74b..878c2f1647e968cfdeb91ce985cb7d5f7f088c78 100644 (file)
@@ -16,17 +16,11 @@ class TrustProxies extends Middleware
     protected $proxies;
 
     /**
-     * The current proxy header mappings.
+     * The headers that should be used to detect proxies.
      *
-     * @var array
+     * @var int
      */
-    protected $headers = [
-        Request::HEADER_FORWARDED => 'FORWARDED',
-        Request::HEADER_X_FORWARDED_FOR => 'X_FORWARDED_FOR',
-        Request::HEADER_X_FORWARDED_HOST => 'X_FORWARDED_HOST',
-        Request::HEADER_X_FORWARDED_PORT => 'X_FORWARDED_PORT',
-        Request::HEADER_X_FORWARDED_PROTO => 'X_FORWARDED_PROTO',
-    ];
+    protected $headers = Request::HEADER_X_FORWARDED_ALL;
 
     /**
      * Handle the request, Set the correct user-configured proxy information.
index 291b8326f36d4657e46197117e6d61bfc84762a6..1a29a2b1d121f35f56dfd9e2eeb5611c84c4b42a 100644 (file)
@@ -6,6 +6,13 @@ use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
 
 class VerifyCsrfToken extends Middleware
 {
+    /**
+     * Indicates whether the XSRF-TOKEN cookie should be set on the response.
+     *
+     * @var bool
+     */
+    protected $addHttpCookie = true;
+
     /**
      * The URIs that should be excluded from CSRF verification.
      *
diff --git a/app/Http/Request.php b/app/Http/Request.php
new file mode 100644 (file)
index 0000000..183686f
--- /dev/null
@@ -0,0 +1,25 @@
+<?php namespace BookStack\Http;
+
+use Illuminate\Http\Request as LaravelRequest;
+
+class Request extends LaravelRequest
+{
+
+    /**
+     * Override the default request methods to get the scheme and host
+     * to set the custom APP_URL, if set.
+     * @return \Illuminate\Config\Repository|mixed|string
+     */
+    public function getSchemeAndHttpHost()
+    {
+        $base = config('app.url', null);
+
+        if ($base) {
+            $base = trim($base, '/');
+        } else {
+            $base = $this->getScheme().'://'.$this->getHttpHost();
+        }
+
+        return $base;
+    }
+}
index 7ecadc298f1f4042a7b887584ca6820bec7bb926..229408f5cf9827533a8307727beac5e4768185c7 100644 (file)
@@ -26,6 +26,6 @@ class ConfirmEmail extends MailNotification
                 ->subject(trans('auth.email_confirm_subject', $appName))
                 ->greeting(trans('auth.email_confirm_greeting', $appName))
                 ->line(trans('auth.email_confirm_text'))
-                ->action(trans('auth.email_confirm_action'), baseUrl('/register/confirm/' . $this->token));
+                ->action(trans('auth.email_confirm_action'), url('/register/confirm/' . $this->token));
     }
 }
index 305a7da72dc50236dd82cc8bbd935f891cf8fb27..20875276400f0403d1a9a063459e37195a2cd475 100644 (file)
@@ -29,7 +29,7 @@ class ResetPassword extends MailNotification
             return $this->newMailMessage()
             ->subject(trans('auth.email_reset_subject', ['appName' => setting('app-name')]))
             ->line(trans('auth.email_reset_text'))
-            ->action(trans('auth.reset_password'), baseUrl('password/reset/' . $this->token))
+            ->action(trans('auth.reset_password'), url('password/reset/' . $this->token))
             ->line(trans('auth.email_reset_not_requested'));
     }
 }
diff --git a/app/Notifications/UserInvite.php b/app/Notifications/UserInvite.php
new file mode 100644 (file)
index 0000000..b01911b
--- /dev/null
@@ -0,0 +1,31 @@
+<?php namespace BookStack\Notifications;
+
+class UserInvite extends MailNotification
+{
+    public $token;
+
+    /**
+     * Create a new notification instance.
+     * @param string $token
+     */
+    public function __construct($token)
+    {
+        $this->token = $token;
+    }
+
+    /**
+     * Get the mail representation of the notification.
+     *
+     * @param  mixed  $notifiable
+     * @return \Illuminate\Notifications\Messages\MailMessage
+     */
+    public function toMail($notifiable)
+    {
+        $appName = ['appName' => setting('app-name')];
+        return $this->newMailMessage()
+                ->subject(trans('auth.user_invite_email_subject', $appName))
+                ->greeting(trans('auth.user_invite_email_greeting', $appName))
+                ->line(trans('auth.user_invite_email_text'))
+                ->action(trans('auth.user_invite_email_action'), url('/register/invite/' . $this->token));
+    }
+}
index 9b91ba126c5878801a82f71f201a9dbbb753706a..3a1b4f42ee63e02118a72d798d8e837f12499018 100644 (file)
@@ -9,10 +9,10 @@ use BookStack\Entities\Page;
 use BookStack\Settings\Setting;
 use BookStack\Settings\SettingService;
 use Illuminate\Database\Eloquent\Relations\Relation;
-use Illuminate\Http\UploadedFile;
 use Illuminate\Support\Facades\View;
 use Illuminate\Support\ServiceProvider;
 use Schema;
+use URL;
 use Validator;
 
 class AppServiceProvider extends ServiceProvider
@@ -24,6 +24,14 @@ class AppServiceProvider extends ServiceProvider
      */
     public function boot()
     {
+        // Set root URL
+        $appUrl = config('app.url');
+        if ($appUrl) {
+            $isHttps = (strpos($appUrl, 'https://') === 0);
+            URL::forceRootUrl($appUrl);
+            URL::forceScheme($isHttps ? 'https' : 'http');
+        }
+
         // Custom validation methods
         Validator::extend('image_extension', function ($attribute, $value, $parameters, $validator) {
             $validImageExtensions = ['png', 'jpg', 'jpeg', 'bmp', 'gif', 'tiff', 'webp'];
@@ -40,6 +48,14 @@ class AppServiceProvider extends ServiceProvider
             return "<?php echo icon($expression); ?>";
         });
 
+        Blade::directive('exposeTranslations', function ($expression) {
+            return "<?php \$__env->startPush('translations'); ?>" .
+                "<?php foreach({$expression} as \$key): ?>" .
+                '<meta name="translation" key="<?php echo e($key); ?>" value="<?php echo e(trans($key)); ?>">' . "\n" .
+                "<?php endforeach; ?>" .
+                '<?php $__env->stopPush(); ?>';
+        });
+
         // Allow longer string lengths after upgrade to utf8mb4
         Schema::defaultStringLength(191);
 
index e7bde5290490fc5d1fc06bf01c0c91c6de2afb55..b4158187cd5fe5225b68aa013d033d67d05edb30 100644 (file)
@@ -4,6 +4,7 @@ namespace BookStack\Providers;
 
 use BookStack\Actions\ActivityService;
 use BookStack\Actions\ViewService;
+use BookStack\Auth\Permissions\PermissionService;
 use BookStack\Settings\SettingService;
 use BookStack\Uploads\ImageService;
 use Illuminate\Support\ServiceProvider;
@@ -27,20 +28,24 @@ class CustomFacadeProvider extends ServiceProvider
      */
     public function register()
     {
-        $this->app->bind('activity', function () {
+        $this->app->singleton('activity', function () {
             return $this->app->make(ActivityService::class);
         });
 
-        $this->app->bind('views', function () {
+        $this->app->singleton('views', function () {
             return $this->app->make(ViewService::class);
         });
 
-        $this->app->bind('setting', function () {
+        $this->app->singleton('setting', function () {
             return $this->app->make(SettingService::class);
         });
 
-        $this->app->bind('images', function () {
+        $this->app->singleton('images', function () {
             return $this->app->make(ImageService::class);
         });
+
+        $this->app->singleton('permissions', function () {
+            return $this->app->make(PermissionService::class);
+        });
     }
 }
index 3a695c5e3dd285c327b74cbdca521e4522ae30b0..1c982b82eacd26610861d49cfdcf71bc1e5a02c3 100644 (file)
@@ -18,7 +18,7 @@ class PaginationServiceProvider extends IlluminatePaginationServiceProvider
         });
 
         Paginator::currentPathResolver(function () {
-            return baseUrl($this->app['request']->path());
+            return url($this->app['request']->path());
         });
 
         Paginator::currentPageResolver(function ($pageName = 'page') {
index eb9a0fe68e02b60eb605911e12d6c8ca40e3fa87..3f0b447df7388a573602396d052bce2e5c5bca7a 100644 (file)
@@ -13,7 +13,7 @@ class Attachment extends Ownable
      */
     public function getFileName()
     {
-        if (str_contains($this->name, '.')) {
+        if (strpos($this->name, '.') !== false) {
             return $this->name;
         }
         return $this->name . '.' . $this->extension;
@@ -37,6 +37,6 @@ class Attachment extends Ownable
         if ($this->external && strpos($this->path, 'http') !== 0) {
             return $this->path;
         }
-        return baseUrl('/attachments/' . $this->id);
+        return url('/attachments/' . $this->id);
     }
 }
index e613642c4f3dd8699a7c3f0c37b2c5394e468700..ae4fb6e967160787e1c46dde0e442228386f9af9 100644 (file)
@@ -2,6 +2,7 @@
 
 use BookStack\Exceptions\FileUploadException;
 use Exception;
+use Illuminate\Support\Str;
 use Symfony\Component\HttpFoundation\File\UploadedFile;
 
 class AttachmentService extends UploadService
@@ -13,7 +14,7 @@ class AttachmentService extends UploadService
      */
     protected function getStorage()
     {
-        $storageType = config('filesystems.default');
+        $storageType = config('filesystems.attachments');
 
         // Override default location if set to local public to ensure not visible.
         if ($storageType === 'local') {
@@ -185,9 +186,9 @@ class AttachmentService extends UploadService
         $storage = $this->getStorage();
         $basePath = 'uploads/files/' . Date('Y-m-M') . '/';
 
-        $uploadFileName = str_random(16) . '.' . $uploadedFile->getClientOriginalExtension();
+        $uploadFileName = Str::random(16) . '.' . $uploadedFile->getClientOriginalExtension();
         while ($storage->exists($basePath . $uploadFileName)) {
-            $uploadFileName = str_random(3) . $uploadFileName;
+            $uploadFileName = Str::random(3) . $uploadFileName;
         }
 
         $attachmentPath = $basePath . $uploadFileName;
index 8eefbaf9dd3468521d388addb292c7285ffd9b16..e7668471bd64c174c858eef0ac6c9c36a16e8880 100644 (file)
@@ -7,6 +7,7 @@ use DB;
 use Exception;
 use Illuminate\Contracts\Cache\Repository as Cache;
 use Illuminate\Contracts\Filesystem\Factory as FileSystem;
+use Illuminate\Support\Str;
 use Intervention\Image\Exception\NotSupportedException;
 use Intervention\Image\ImageManager;
 use phpDocumentor\Reflection\Types\Integer;
@@ -45,9 +46,9 @@ class ImageService extends UploadService
      */
     protected function getStorage($type = '')
     {
-        $storageType = config('filesystems.default');
+        $storageType = config('filesystems.images');
 
-        // Override default location if set to local public to ensure not visible.
+        // Ensure system images (App logo) are uploaded to a public space
         if ($type === 'system' && $storageType === 'local_secure') {
             $storageType = 'local';
         }
@@ -140,12 +141,12 @@ class ImageService extends UploadService
         $imagePath = '/uploads/images/' . $type . '/' . Date('Y-m') . '/';
 
         while ($storage->exists($imagePath . $imageName)) {
-            $imageName = str_random(3) . $imageName;
+            $imageName = Str::random(3) . $imageName;
         }
 
         $fullPath = $imagePath . $imageName;
         if ($secureUploads) {
-            $fullPath = $imagePath . str_random(16) . '-' . $imageName;
+            $fullPath = $imagePath . Str::random(16) . '-' . $imageName;
         }
 
         try {
@@ -220,7 +221,7 @@ class ImageService extends UploadService
 
         $storage->put($thumbFilePath, $thumbData);
         $storage->setVisibility($thumbFilePath, 'public');
-        $this->cache->put('images-' . $image->id . '-' . $thumbFilePath, $thumbFilePath, 60 * 72);
+        $this->cache->put('images-' . $image->id . '-' . $thumbFilePath, $thumbFilePath, 60 * 60 * 72);
 
         return $this->getPublicUrl($thumbFilePath);
     }
@@ -417,7 +418,7 @@ class ImageService extends UploadService
         $isLocal = strpos(trim($uri), 'http') !== 0;
 
         // Attempt to find local files even if url not absolute
-        $base = baseUrl('/');
+        $base = url('/');
         if (!$isLocal && strpos($uri, $base) === 0) {
             $isLocal = true;
             $uri = str_replace($base, '', $uri);
@@ -442,7 +443,12 @@ class ImageService extends UploadService
             return null;
         }
 
-        return 'data:image/' . pathinfo($uri, PATHINFO_EXTENSION) . ';base64,' . base64_encode($imageData);
+        $extension = pathinfo($uri, PATHINFO_EXTENSION);
+        if ($extension === 'svg') {
+            $extension = 'svg+xml';
+        }
+
+        return 'data:image/' . $extension . ';base64,' . base64_encode($imageData);
     }
 
     /**
@@ -458,7 +464,7 @@ class ImageService extends UploadService
             // Get the standard public s3 url if s3 is set as storage type
             // Uses the nice, short URL if bucket name has no periods in otherwise the longer
             // region-based url will be used to prevent http issues.
-            if ($storageUrl == false && config('filesystems.default') === 's3') {
+            if ($storageUrl == false && config('filesystems.images') === 's3') {
                 $storageDetails = config('filesystems.disks.s3');
                 if (strpos($storageDetails['bucket'], '.') === false) {
                     $storageUrl = 'https://' . $storageDetails['bucket'] . '.s3.amazonaws.com';
@@ -469,7 +475,7 @@ class ImageService extends UploadService
             $this->storageUrl = $storageUrl;
         }
 
-        $basePath = ($this->storageUrl == false) ? baseUrl('/') : $this->storageUrl;
+        $basePath = ($this->storageUrl == false) ? url('/') : $this->storageUrl;
         return rtrim($basePath, '/') . $filePath;
     }
 }
index 0fedf2e8db30a009a805635b09544d1b2e0c1e0c..6211f41be0c4ef1a866a4c043287b2d76c1f3f60 100644 (file)
@@ -1,8 +1,9 @@
 <?php
 
 use BookStack\Auth\Permissions\PermissionService;
-use BookStack\Entities\Entity;
+use BookStack\Auth\User;
 use BookStack\Ownable;
+use BookStack\Settings\SettingService;
 
 /**
  * Get the path to a versioned file.
@@ -11,7 +12,7 @@ use BookStack\Ownable;
  * @return string
  * @throws Exception
  */
-function versioned_asset($file = '')
+function versioned_asset(string $file = ''): string
 {
     static $version = null;
 
@@ -26,24 +27,24 @@ function versioned_asset($file = '')
     }
 
     $path = $file . '?version=' . urlencode($version) . $additional;
-    return baseUrl($path);
+    return url($path);
 }
 
 /**
  * Helper method to get the current User.
  * Defaults to public 'Guest' user if not logged in.
- * @return \BookStack\Auth\User
+ * @return User
  */
-function user()
+function user(): User
 {
-    return auth()->user() ?: \BookStack\Auth\User::getDefault();
+    return auth()->user() ?: User::getDefault();
 }
 
 /**
  * Check if current user is a signed in user.
  * @return bool
  */
-function signedInUser() : bool
+function signedInUser(): bool
 {
     return auth()->user() && !auth()->user()->isDefault();
 }
@@ -52,7 +53,7 @@ function signedInUser() : bool
  * Check if the current user has general access.
  * @return bool
  */
-function hasAppAccess() : bool
+function hasAppAccess(): bool
 {
     return !auth()->guest() || setting('app-public');
 }
@@ -63,9 +64,9 @@ function hasAppAccess() : bool
  * that particular item.
  * @param string $permission
  * @param Ownable $ownable
- * @return mixed
+ * @return bool
  */
-function userCan(string $permission, Ownable $ownable = null)
+function userCan(string $permission, Ownable $ownable = null): bool
 {
     if ($ownable === null) {
         return user() && user()->can($permission);
@@ -83,7 +84,7 @@ function userCan(string $permission, Ownable $ownable = null)
  * @param string|null $entityClass
  * @return bool
  */
-function userCanOnAny(string $permission, string $entityClass = null)
+function userCanOnAny(string $permission, string $entityClass = null): bool
 {
     $permissionService = app(PermissionService::class);
     return $permissionService->checkUserHasPermissionOnAnything($permission, $entityClass);
@@ -91,85 +92,29 @@ function userCanOnAny(string $permission, string $entityClass = null)
 
 /**
  * Helper to access system settings.
- * @param $key
- * @param bool $default
- * @return bool|string|\BookStack\Settings\SettingService
+ * @param string $key
+ * @param $default
+ * @return bool|string|SettingService
  */
-function setting($key = null, $default = false)
+function setting(string $key = null, $default = false)
 {
-    $settingService = resolve(\BookStack\Settings\SettingService::class);
+    $settingService = resolve(SettingService::class);
     if (is_null($key)) {
         return $settingService;
     }
     return $settingService->get($key, $default);
 }
 
-/**
- * Helper to create url's relative to the applications root path.
- * @param string $path
- * @param bool $forceAppDomain
- * @return string
- */
-function baseUrl($path, $forceAppDomain = false)
-{
-    $isFullUrl = strpos($path, 'http') === 0;
-    if ($isFullUrl && !$forceAppDomain) {
-        return $path;
-    }
-
-    $path = trim($path, '/');
-    $base = rtrim(config('app.url'), '/');
-
-    // Remove non-specified domain if forced and we have a domain
-    if ($isFullUrl && $forceAppDomain) {
-        if (!empty($base) && strpos($path, $base) === 0) {
-            $path = substr($path, strlen($base));
-        } else {
-            $explodedPath = explode('/', $path);
-            $path = implode('/', array_splice($explodedPath, 3));
-        }
-    }
-
-    // Return normal url path if not specified in config
-    if (config('app.url') === '') {
-        return url($path);
-    }
-
-    return $base . '/' . ltrim($path, '/');
-}
-
-/**
- * Get an instance of the redirector.
- * Overrides the default laravel redirect helper.
- * Ensures it redirects even when the app is in a subdirectory.
- *
- * @param  string|null  $to
- * @param  int     $status
- * @param  array   $headers
- * @param  bool    $secure
- * @return \Illuminate\Routing\Redirector|\Illuminate\Http\RedirectResponse
- */
-function redirect($to = null, $status = 302, $headers = [], $secure = null)
-{
-    if (is_null($to)) {
-        return app('redirect');
-    }
-
-    $to = baseUrl($to);
-
-    return app('redirect')->to($to, $status, $headers, $secure);
-}
-
 /**
  * Get a path to a theme resource.
  * @param string $path
- * @return string|boolean
+ * @return string
  */
-function theme_path($path = '')
+function theme_path(string $path = ''): string
 {
     $theme = config('view.theme');
     if (!$theme) {
-        return false;
+        return '';
     }
 
     return base_path('themes/' . $theme .($path ? DIRECTORY_SEPARATOR.$path : $path));
@@ -185,18 +130,19 @@ function theme_path($path = '')
  * @param array $attrs
  * @return mixed
  */
-function icon($name, $attrs = [])
+function icon(string $name, array $attrs = []): string
 {
     $attrs = array_merge([
-        'class' => 'svg-icon',
-        'data-icon' => $name
+        'class'     => 'svg-icon',
+        'data-icon' => $name,
+        'role'      => 'presentation',
     ], $attrs);
     $attrString = ' ';
     foreach ($attrs as $attrName => $attr) {
         $attrString .=  $attrName . '="' . $attr . '" ';
     }
 
-    $iconPath = resource_path('assets/icons/' . $name . '.svg');
+    $iconPath = resource_path('icons/' . $name . '.svg');
     $themeIconPath = theme_path('icons/' . $name . '.svg');
     if ($themeIconPath && file_exists($themeIconPath)) {
         $iconPath = $themeIconPath;
@@ -212,12 +158,12 @@ function icon($name, $attrs = [])
  * Generate a url with multiple parameters for sorting purposes.
  * Works out the logic to set the correct sorting direction
  * Discards empty parameters and allows overriding.
- * @param $path
+ * @param string $path
  * @param array $data
  * @param array $overrideData
  * @return string
  */
-function sortUrl($path, $data, $overrideData = [])
+function sortUrl(string $path, array $data, array $overrideData = []): string
 {
     $queryStringSections = [];
     $queryData = array_merge($data, $overrideData);
@@ -241,5 +187,5 @@ function sortUrl($path, $data, $overrideData = [])
         return $path;
     }
 
-    return baseUrl($path . '?' . implode('&', $queryStringSections));
+    return url($path . '?' . implode('&', $queryStringSections));
 }
index 371f93913b7a409ce03e33cf084b286d54e009ff..6538aa81c50789b32194f77ddb4e7108dafe407c 100644 (file)
@@ -11,8 +11,8 @@
 |
 */
 
-$app = new Illuminate\Foundation\Application(
-    realpath(__DIR__.'/../')
+$app = new BookStack\Application(
+    dirname(__DIR__)
 );
 
 /*
index 61bb8509e9b87717b3eaf6257ddd82624e81876d..a8b9456a1856c01fb2bf65e14ea32de0f7c55a15 100644 (file)
@@ -5,46 +5,47 @@
     "license": "MIT",
     "type": "project",
     "require": {
-        "php": ">=7.0.5",
+        "php": "^7.2",
+        "ext-curl": "*",
+        "ext-dom": "*",
+        "ext-gd": "*",
         "ext-json": "*",
+        "ext-mbstring": "*",
         "ext-tidy": "*",
-        "ext-dom": "*",
         "ext-xml": "*",
-        "ext-mbstring": "*",
-        "ext-gd": "*",
-        "ext-curl": "*",
-        "laravel/framework": "~5.5.44",
-        "fideloper/proxy": "~3.3",
-        "intervention/image": "^2.4",
-        "laravel/socialite": "3.0.x-dev",
+        "barryvdh/laravel-dompdf": "^0.8.5",
+        "barryvdh/laravel-snappy": "^0.4.5",
+        "doctrine/dbal": "^2.9",
+        "fideloper/proxy": "^4.0",
+        "gathercontent/htmldiff": "^0.2.1",
+        "intervention/image": "^2.5",
+        "laravel/framework": "^6.0",
+        "laravel/socialite": "^4.2",
         "league/flysystem-aws-s3-v3": "^1.0",
-        "barryvdh/laravel-dompdf": "^0.8.1",
         "predis/predis": "^1.1",
-        "gathercontent/htmldiff": "^0.2.1",
-        "barryvdh/laravel-snappy": "^0.4.0",
-        "socialiteproviders/slack": "^3.0",
+        "socialiteproviders/discord": "^2.0",
+        "socialiteproviders/gitlab": "^3.0",
         "socialiteproviders/microsoft-azure": "^3.0",
         "socialiteproviders/okta": "^1.0",
-        "socialiteproviders/gitlab": "^3.0",
-        "socialiteproviders/twitch": "^3.0",
-        "socialiteproviders/discord": "^2.0",
-        "doctrine/dbal": "^2.5"
+        "socialiteproviders/slack": "^3.0",
+        "socialiteproviders/twitch": "^5.0"
     },
     "require-dev": {
-        "filp/whoops": "~2.0",
-        "fzaninotto/faker": "~1.4",
-        "mockery/mockery": "~1.0",
-        "phpunit/phpunit": "~6.0",
-        "symfony/css-selector": "3.1.*",
-        "symfony/dom-crawler": "3.1.*",
-        "laravel/browser-kit-testing": "^2.0",
-        "barryvdh/laravel-ide-helper": "^2.4.1",
-        "barryvdh/laravel-debugbar": "^3.1.0",
-        "squizlabs/php_codesniffer": "^3.2"
+        "barryvdh/laravel-debugbar": "^3.2.8",
+        "barryvdh/laravel-ide-helper": "^2.6.4",
+        "facade/ignition": "^1.4",
+        "fzaninotto/faker": "^1.4",
+        "laravel/browser-kit-testing": "^5.1",
+        "mockery/mockery": "^1.0",
+        "nunomaduro/collision": "^3.0",
+        "phpunit/phpunit": "^8.0",
+        "squizlabs/php_codesniffer": "^3.4",
+        "wnx/laravel-stats": "^2.0"
     },
     "autoload": {
         "classmap": [
-            "database"
+            "database/seeds",
+            "database/factories"
         ],
         "psr-4": {
             "BookStack\\": "app/"
     },
     "scripts": {
         "post-root-package-install": [
-            "php -r \"file_exists('.env') || copy('.env.example', '.env');\""
+            "@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
         ],
         "post-create-project-cmd": [
-            "php artisan key:generate"
+            "@php artisan key:generate --ansi"
         ],
         "pre-update-cmd": [
-            "php -r \"!file_exists('bootstrap/cache/services.php') || @unlink('bootstrap/cache/services.php');\"",
-            "php -r \"!file_exists('bootstrap/cache/compiled.php') || @unlink('bootstrap/cache/compiled.php');\""
+            "@php -r \"!file_exists('bootstrap/cache/services.php') || @unlink('bootstrap/cache/services.php');\"",
+            "@php -r \"!file_exists('bootstrap/cache/compiled.php') || @unlink('bootstrap/cache/compiled.php');\""
         ],
         "pre-install-cmd": [
-            "php -r \"!file_exists('bootstrap/cache/services.php') || @unlink('bootstrap/cache/services.php');\"",
-            "php -r \"!file_exists('bootstrap/cache/compiled.php') || @unlink('bootstrap/cache/compiled.php');\""
+            "@php -r \"!file_exists('bootstrap/cache/services.php') || @unlink('bootstrap/cache/services.php');\"",
+            "@php -r \"!file_exists('bootstrap/cache/compiled.php') || @unlink('bootstrap/cache/compiled.php');\""
         ],
         "post-install-cmd": [
-            "php artisan cache:clear",
-            "php artisan view:clear"
+            "@php artisan cache:clear",
+            "@php artisan view:clear"
         ],
         "post-autoload-dump": [
             "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
-            "@php artisan package:discover"
+            "@php artisan package:discover --ansi"
         ],
         "refresh-test-database": [
-            "php artisan migrate:refresh --database=mysql_testing",
-            "php artisan db:seed --class=DummyContentSeeder --database=mysql_testing"
+            "@php artisan migrate:refresh --database=mysql_testing",
+            "@php artisan db:seed --class=DummyContentSeeder --database=mysql_testing"
         ]
     },
     "config": {
         "optimize-autoloader": true,
         "preferred-install": "dist",
+        "sort-packages": true,
         "platform": {
-            "php": "7.0.5"
+            "php": "7.2.0"
+        }
+    },
+    "extra": {
+        "laravel": {
+            "dont-discover": []
         }
-    }
+    },
+    "minimum-stability": "dev",
+    "prefer-stable": true
 }
index d7734ce1a95c565dda65f40742bb52a97d4db992..3ec106ded4f0c57c9ca3556222dff16874606bc8 100644 (file)
@@ -4,27 +4,26 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
         "This file is @generated automatically"
     ],
-    "content-hash": "0946a07729a7a1bfef9bac185a870afd",
+    "content-hash": "c156e1738dbab2a57f9a926d9a9a5a6a",
     "packages": [
         {
             "name": "aws/aws-sdk-php",
-            "version": "3.86.2",
+            "version": "3.112.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/aws/aws-sdk-php.git",
-                "reference": "50224232ac7a4e2a6fa4ebbe0281e5b7503acf76"
+                "reference": "1e21446c6780a3b9b5e4315bd6d4347d2c3381eb"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/50224232ac7a4e2a6fa4ebbe0281e5b7503acf76",
-                "reference": "50224232ac7a4e2a6fa4ebbe0281e5b7503acf76",
+                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/1e21446c6780a3b9b5e4315bd6d4347d2c3381eb",
+                "reference": "1e21446c6780a3b9b5e4315bd6d4347d2c3381eb",
                 "shasum": ""
             },
             "require": {
                 "ext-json": "*",
                 "ext-pcre": "*",
                 "ext-simplexml": "*",
-                "ext-spl": "*",
                 "guzzlehttp/guzzle": "^5.3.3|^6.2.1",
                 "guzzlehttp/promises": "~1.0",
                 "guzzlehttp/psr7": "^1.4.1",
@@ -42,7 +41,8 @@
                 "ext-sockets": "*",
                 "nette/neon": "^2.3",
                 "phpunit/phpunit": "^4.8.35|^5.4.3",
-                "psr/cache": "^1.0"
+                "psr/cache": "^1.0",
+                "psr/simple-cache": "^1.0"
             },
             "suggest": {
                 "aws/aws-php-sns-message-validator": "To validate incoming SNS notifications",
                 "s3",
                 "sdk"
             ],
-            "time": "2019-01-18T21:10:44+00:00"
+            "time": "2019-09-12T18:09:53+00:00"
         },
         {
             "name": "barryvdh/laravel-dompdf",
-            "version": "v0.8.3",
+            "version": "v0.8.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/barryvdh/laravel-dompdf.git",
-                "reference": "46781d0304277845a19c09c169bc595fd182cce4"
+                "reference": "7393732b2f3a3ee357974cbb0c46c9b65b84dad1"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/barryvdh/laravel-dompdf/zipball/46781d0304277845a19c09c169bc595fd182cce4",
-                "reference": "46781d0304277845a19c09c169bc595fd182cce4",
+                "url": "https://api.github.com/repos/barryvdh/laravel-dompdf/zipball/7393732b2f3a3ee357974cbb0c46c9b65b84dad1",
+                "reference": "7393732b2f3a3ee357974cbb0c46c9b65b84dad1",
                 "shasum": ""
             },
             "require": {
                 "dompdf/dompdf": "^0.8",
-                "illuminate/support": "5.5.x|5.6.x|5.7.x",
+                "illuminate/support": "^5.5|^6",
                 "php": ">=7"
             },
             "type": "library",
                 "laravel",
                 "pdf"
             ],
-            "time": "2018-08-31T13:25:44+00:00"
+            "time": "2019-08-23T14:30:33+00:00"
         },
         {
             "name": "barryvdh/laravel-snappy",
-            "version": "v0.4.3",
+            "version": "v0.4.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/barryvdh/laravel-snappy.git",
-                "reference": "62bb5017b7004bf3e48bfed3d5c00d3dc6e60478"
+                "reference": "9be767fc7a082665a84945f36c70b0cbead91ce9"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/barryvdh/laravel-snappy/zipball/62bb5017b7004bf3e48bfed3d5c00d3dc6e60478",
-                "reference": "62bb5017b7004bf3e48bfed3d5c00d3dc6e60478",
+                "url": "https://api.github.com/repos/barryvdh/laravel-snappy/zipball/9be767fc7a082665a84945f36c70b0cbead91ce9",
+                "reference": "9be767fc7a082665a84945f36c70b0cbead91ce9",
                 "shasum": ""
             },
             "require": {
-                "illuminate/filesystem": "5.5.x|5.6.x|5.7.x",
-                "illuminate/support": "5.5.x|5.6.x|5.7.x",
+                "illuminate/filesystem": "5.5.x|5.6.x|5.7.x|5.8.x|6.0.*",
+                "illuminate/support": "5.5.x|5.6.x|5.7.x|5.8.x|6.0.*",
                 "knplabs/knp-snappy": "^1",
                 "php": ">=7"
             },
                     "email": "[email protected]"
                 }
             ],
-            "description": "Snappy PDF/Image for Laravel 4",
+            "description": "Snappy PDF/Image for Laravel",
             "keywords": [
                 "image",
                 "laravel",
                 "wkhtmltoimage",
                 "wkhtmltopdf"
             ],
-            "time": "2018-09-06T10:14:15+00:00"
+            "time": "2019-08-30T16:12:23+00:00"
         },
         {
             "name": "cogpowered/finediff",
             ],
             "time": "2014-05-19T10:25:02+00:00"
         },
-        {
-            "name": "doctrine/annotations",
-            "version": "v1.4.0",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/doctrine/annotations.git",
-                "reference": "54cacc9b81758b14e3ce750f205a393d52339e97"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/doctrine/annotations/zipball/54cacc9b81758b14e3ce750f205a393d52339e97",
-                "reference": "54cacc9b81758b14e3ce750f205a393d52339e97",
-                "shasum": ""
-            },
-            "require": {
-                "doctrine/lexer": "1.*",
-                "php": "^5.6 || ^7.0"
-            },
-            "require-dev": {
-                "doctrine/cache": "1.*",
-                "phpunit/phpunit": "^5.7"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "1.4.x-dev"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Roman Borschel",
-                    "email": "[email protected]"
-                },
-                {
-                    "name": "Benjamin Eberlei",
-                    "email": "[email protected]"
-                },
-                {
-                    "name": "Guilherme Blanco",
-                    "email": "[email protected]"
-                },
-                {
-                    "name": "Jonathan Wage",
-                    "email": "[email protected]"
-                },
-                {
-                    "name": "Johannes Schmitt",
-                    "email": "[email protected]"
-                }
-            ],
-            "description": "Docblock Annotations Parser",
-            "homepage": "http://www.doctrine-project.org",
-            "keywords": [
-                "annotations",
-                "docblock",
-                "parser"
-            ],
-            "time": "2017-02-24T16:22:25+00:00"
-        },
         {
             "name": "doctrine/cache",
-            "version": "v1.6.2",
+            "version": "v1.8.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/doctrine/cache.git",
-                "reference": "eb152c5100571c7a45470ff2a35095ab3f3b900b"
+                "reference": "d768d58baee9a4862ca783840eca1b9add7a7f57"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/doctrine/cache/zipball/eb152c5100571c7a45470ff2a35095ab3f3b900b",
-                "reference": "eb152c5100571c7a45470ff2a35095ab3f3b900b",
+                "url": "https://api.github.com/repos/doctrine/cache/zipball/d768d58baee9a4862ca783840eca1b9add7a7f57",
+                "reference": "d768d58baee9a4862ca783840eca1b9add7a7f57",
                 "shasum": ""
             },
             "require": {
-                "php": "~5.5|~7.0"
+                "php": "~7.1"
             },
             "conflict": {
                 "doctrine/common": ">2.2,<2.4"
             },
             "require-dev": {
-                "phpunit/phpunit": "~4.8|~5.0",
-                "predis/predis": "~1.0",
-                "satooshi/php-coveralls": "~0.6"
+                "alcaeus/mongo-php-adapter": "^1.1",
+                "doctrine/coding-standard": "^4.0",
+                "mongodb/mongodb": "^1.1",
+                "phpunit/phpunit": "^7.0",
+                "predis/predis": "~1.0"
+            },
+            "suggest": {
+                "alcaeus/mongo-php-adapter": "Required to use legacy MongoDB driver"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.6.x-dev"
+                    "dev-master": "1.8.x-dev"
                 }
             },
             "autoload": {
                 }
             ],
             "description": "Caching library offering an object-oriented API for many cache backends",
-            "homepage": "http://www.doctrine-project.org",
+            "homepage": "https://www.doctrine-project.org",
             "keywords": [
                 "cache",
                 "caching"
             ],
-            "time": "2017-07-22T12:49:21+00:00"
+            "time": "2018-08-21T18:01:43+00:00"
         },
         {
-            "name": "doctrine/collections",
-            "version": "v1.4.0",
+            "name": "doctrine/dbal",
+            "version": "v2.9.2",
             "source": {
                 "type": "git",
-                "url": "https://github.com/doctrine/collections.git",
-                "reference": "1a4fb7e902202c33cce8c55989b945612943c2ba"
+                "url": "https://github.com/doctrine/dbal.git",
+                "reference": "22800bd651c1d8d2a9719e2a3dc46d5108ebfcc9"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/doctrine/collections/zipball/1a4fb7e902202c33cce8c55989b945612943c2ba",
-                "reference": "1a4fb7e902202c33cce8c55989b945612943c2ba",
+                "url": "https://api.github.com/repos/doctrine/dbal/zipball/22800bd651c1d8d2a9719e2a3dc46d5108ebfcc9",
+                "reference": "22800bd651c1d8d2a9719e2a3dc46d5108ebfcc9",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.6 || ^7.0"
+                "doctrine/cache": "^1.0",
+                "doctrine/event-manager": "^1.0",
+                "ext-pdo": "*",
+                "php": "^7.1"
             },
             "require-dev": {
-                "doctrine/coding-standard": "~0.1@dev",
-                "phpunit/phpunit": "^5.7"
+                "doctrine/coding-standard": "^5.0",
+                "jetbrains/phpstorm-stubs": "^2018.1.2",
+                "phpstan/phpstan": "^0.10.1",
+                "phpunit/phpunit": "^7.4",
+                "symfony/console": "^2.0.5|^3.0|^4.0",
+                "symfony/phpunit-bridge": "^3.4.5|^4.0.5"
+            },
+            "suggest": {
+                "symfony/console": "For helpful console commands such as SQL execution and import of files."
             },
+            "bin": [
+                "bin/doctrine-dbal"
+            ],
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.3.x-dev"
+                    "dev-master": "2.9.x-dev",
+                    "dev-develop": "3.0.x-dev"
                 }
             },
             "autoload": {
-                "psr-0": {
-                    "Doctrine\\Common\\Collections\\": "lib/"
+                "psr-4": {
+                    "Doctrine\\DBAL\\": "lib/Doctrine/DBAL"
                 }
             },
             "notification-url": "https://packagist.org/downloads/",
                 {
                     "name": "Jonathan Wage",
                     "email": "[email protected]"
-                },
-                {
-                    "name": "Johannes Schmitt",
-                    "email": "[email protected]"
                 }
             ],
-            "description": "Collections Abstraction library",
-            "homepage": "http://www.doctrine-project.org",
+            "description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.",
+            "homepage": "https://www.doctrine-project.org/projects/dbal.html",
             "keywords": [
-                "array",
-                "collections",
-                "iterator"
+                "abstraction",
+                "database",
+                "dbal",
+                "mysql",
+                "persistence",
+                "pgsql",
+                "php",
+                "queryobject"
             ],
-            "time": "2017-01-03T10:49:41+00:00"
+            "time": "2018-12-31T03:27:51+00:00"
         },
         {
-            "name": "doctrine/common",
-            "version": "v2.7.3",
+            "name": "doctrine/event-manager",
+            "version": "v1.0.0",
             "source": {
                 "type": "git",
-                "url": "https://github.com/doctrine/common.git",
-                "reference": "4acb8f89626baafede6ee5475bc5844096eba8a9"
+                "url": "https://github.com/doctrine/event-manager.git",
+                "reference": "a520bc093a0170feeb6b14e9d83f3a14452e64b3"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/doctrine/common/zipball/4acb8f89626baafede6ee5475bc5844096eba8a9",
-                "reference": "4acb8f89626baafede6ee5475bc5844096eba8a9",
+                "url": "https://api.github.com/repos/doctrine/event-manager/zipball/a520bc093a0170feeb6b14e9d83f3a14452e64b3",
+                "reference": "a520bc093a0170feeb6b14e9d83f3a14452e64b3",
                 "shasum": ""
             },
             "require": {
-                "doctrine/annotations": "1.*",
-                "doctrine/cache": "1.*",
-                "doctrine/collections": "1.*",
-                "doctrine/inflector": "1.*",
-                "doctrine/lexer": "1.*",
-                "php": "~5.6|~7.0"
+                "php": "^7.1"
+            },
+            "conflict": {
+                "doctrine/common": "<2.9@dev"
             },
             "require-dev": {
-                "phpunit/phpunit": "^5.4.6"
+                "doctrine/coding-standard": "^4.0",
+                "phpunit/phpunit": "^7.0"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "2.7.x-dev"
+                    "dev-master": "1.0.x-dev"
                 }
             },
             "autoload": {
                 {
                     "name": "Johannes Schmitt",
                     "email": "[email protected]"
-                }
-            ],
-            "description": "Common Library for Doctrine projects",
-            "homepage": "http://www.doctrine-project.org",
-            "keywords": [
-                "annotations",
-                "collections",
-                "eventmanager",
-                "persistence",
-                "spl"
-            ],
-            "time": "2017-07-22T08:35:12+00:00"
-        },
-        {
-            "name": "doctrine/dbal",
-            "version": "v2.5.13",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/doctrine/dbal.git",
-                "reference": "729340d8d1eec8f01bff708e12e449a3415af873"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/doctrine/dbal/zipball/729340d8d1eec8f01bff708e12e449a3415af873",
-                "reference": "729340d8d1eec8f01bff708e12e449a3415af873",
-                "shasum": ""
-            },
-            "require": {
-                "doctrine/common": ">=2.4,<2.8-dev",
-                "php": ">=5.3.2"
-            },
-            "require-dev": {
-                "phpunit/phpunit": "4.*",
-                "symfony/console": "2.*||^3.0"
-            },
-            "suggest": {
-                "symfony/console": "For helpful console commands such as SQL execution and import of files."
-            },
-            "bin": [
-                "bin/doctrine-dbal"
-            ],
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "2.5.x-dev"
-                }
-            },
-            "autoload": {
-                "psr-0": {
-                    "Doctrine\\DBAL\\": "lib/"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Roman Borschel",
-                    "email": "[email protected]"
-                },
-                {
-                    "name": "Benjamin Eberlei",
-                    "email": "[email protected]"
-                },
-                {
-                    "name": "Guilherme Blanco",
-                    "email": "[email protected]"
                 },
                 {
-                    "name": "Jonathan Wage",
-                    "email": "jonwage@gmail.com"
+                    "name": "Marco Pivetta",
+                    "email": "ocramius@gmail.com"
                 }
             ],
-            "description": "Database Abstraction Layer",
-            "homepage": "http://www.doctrine-project.org",
+            "description": "Doctrine Event Manager component",
+            "homepage": "https://www.doctrine-project.org/projects/event-manager.html",
             "keywords": [
-                "database",
-                "dbal",
-                "persistence",
-                "queryobject"
+                "event",
+                "eventdispatcher",
+                "eventmanager"
             ],
-            "time": "2017-07-22T20:44:48+00:00"
+            "time": "2018-06-11T11:59:03+00:00"
         },
         {
             "name": "doctrine/inflector",
-            "version": "v1.2.0",
+            "version": "v1.3.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/doctrine/inflector.git",
-                "reference": "e11d84c6e018beedd929cff5220969a3c6d1d462"
+                "reference": "5527a48b7313d15261292c149e55e26eae771b0a"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/doctrine/inflector/zipball/e11d84c6e018beedd929cff5220969a3c6d1d462",
-                "reference": "e11d84c6e018beedd929cff5220969a3c6d1d462",
+                "url": "https://api.github.com/repos/doctrine/inflector/zipball/5527a48b7313d15261292c149e55e26eae771b0a",
+                "reference": "5527a48b7313d15261292c149e55e26eae771b0a",
                 "shasum": ""
             },
             "require": {
-                "php": "^7.0"
+                "php": "^7.1"
             },
             "require-dev": {
                 "phpunit/phpunit": "^6.2"
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.2.x-dev"
+                    "dev-master": "1.3.x-dev"
                 }
             },
             "autoload": {
                 "singularize",
                 "string"
             ],
-            "time": "2017-07-22T12:18:28+00:00"
+            "time": "2018-01-09T20:05:19+00:00"
         },
         {
             "name": "doctrine/lexer",
-            "version": "v1.0.1",
+            "version": "1.1.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/doctrine/lexer.git",
-                "reference": "83893c552fd2045dd78aef794c31e694c37c0b8c"
+                "reference": "e17f069ede36f7534b95adec71910ed1b49c74ea"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/doctrine/lexer/zipball/83893c552fd2045dd78aef794c31e694c37c0b8c",
-                "reference": "83893c552fd2045dd78aef794c31e694c37c0b8c",
+                "url": "https://api.github.com/repos/doctrine/lexer/zipball/e17f069ede36f7534b95adec71910ed1b49c74ea",
+                "reference": "e17f069ede36f7534b95adec71910ed1b49c74ea",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.3.2"
+                "php": "^7.2"
+            },
+            "require-dev": {
+                "doctrine/coding-standard": "^6.0",
+                "phpstan/phpstan": "^0.11.8",
+                "phpunit/phpunit": "^8.2"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.0.x-dev"
+                    "dev-master": "1.1.x-dev"
                 }
             },
             "autoload": {
-                "psr-0": {
-                    "Doctrine\\Common\\Lexer\\": "lib/"
+                "psr-4": {
+                    "Doctrine\\Common\\Lexer\\": "lib/Doctrine/Common/Lexer"
                 }
             },
             "notification-url": "https://packagist.org/downloads/",
                 "MIT"
             ],
             "authors": [
-                {
-                    "name": "Roman Borschel",
-                    "email": "[email protected]"
-                },
                 {
                     "name": "Guilherme Blanco",
                     "email": "[email protected]"
                 },
+                {
+                    "name": "Roman Borschel",
+                    "email": "[email protected]"
+                },
                 {
                     "name": "Johannes Schmitt",
                     "email": "[email protected]"
                 }
             ],
-            "description": "Base library for a lexer that can be used in Top-Down, Recursive Descent Parsers.",
-            "homepage": "http://www.doctrine-project.org",
+            "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.",
+            "homepage": "https://www.doctrine-project.org/projects/lexer.html",
             "keywords": [
+                "annotations",
+                "docblock",
                 "lexer",
-                "parser"
+                "parser",
+                "php"
             ],
-            "time": "2014-09-09T13:34:57+00:00"
+            "time": "2019-07-30T19:33:28+00:00"
         },
         {
             "name": "dompdf/dompdf",
             "homepage": "https://github.com/dompdf/dompdf",
             "time": "2018-12-14T02:40:31+00:00"
         },
+        {
+            "name": "dragonmantank/cron-expression",
+            "version": "v2.3.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/dragonmantank/cron-expression.git",
+                "reference": "72b6fbf76adb3cf5bc0db68559b33d41219aba27"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/72b6fbf76adb3cf5bc0db68559b33d41219aba27",
+                "reference": "72b6fbf76adb3cf5bc0db68559b33d41219aba27",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^6.4|^7.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.3-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Cron\\": "src/Cron/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Michael Dowling",
+                    "email": "[email protected]",
+                    "homepage": "https://github.com/mtdowling"
+                },
+                {
+                    "name": "Chris Tankersley",
+                    "email": "[email protected]",
+                    "homepage": "https://github.com/dragonmantank"
+                }
+            ],
+            "description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due",
+            "keywords": [
+                "cron",
+                "schedule"
+            ],
+            "time": "2019-03-31T00:38:28+00:00"
+        },
         {
             "name": "egulias/email-validator",
-            "version": "2.1.7",
+            "version": "2.1.11",
             "source": {
                 "type": "git",
                 "url": "https://github.com/egulias/EmailValidator.git",
-                "reference": "709f21f92707308cdf8f9bcfa1af4cb26586521e"
+                "reference": "92dd169c32f6f55ba570c309d83f5209cefb5e23"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/709f21f92707308cdf8f9bcfa1af4cb26586521e",
-                "reference": "709f21f92707308cdf8f9bcfa1af4cb26586521e",
+                "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/92dd169c32f6f55ba570c309d83f5209cefb5e23",
+                "reference": "92dd169c32f6f55ba570c309d83f5209cefb5e23",
                 "shasum": ""
             },
             "require": {
             "require-dev": {
                 "dominicsayers/isemail": "dev-master",
                 "phpunit/phpunit": "^4.8.35||^5.7||^6.0",
-                "satooshi/php-coveralls": "^1.0.1"
+                "satooshi/php-coveralls": "^1.0.1",
+                "symfony/phpunit-bridge": "^4.4@dev"
             },
             "suggest": {
                 "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation"
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "2.0.x-dev"
+                    "dev-master": "2.1.x-dev"
                 }
             },
             "autoload": {
                 "validation",
                 "validator"
             ],
-            "time": "2018-12-04T22:38:24+00:00"
+            "time": "2019-08-13T17:33:27+00:00"
         },
         {
             "name": "erusev/parsedown",
-            "version": "1.7.1",
+            "version": "1.7.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/erusev/parsedown.git",
-                "reference": "92e9c27ba0e74b8b028b111d1b6f956a15c01fc1"
+                "reference": "6d893938171a817f4e9bc9e86f2da1e370b7bcd7"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/erusev/parsedown/zipball/92e9c27ba0e74b8b028b111d1b6f956a15c01fc1",
-                "reference": "92e9c27ba0e74b8b028b111d1b6f956a15c01fc1",
+                "url": "https://api.github.com/repos/erusev/parsedown/zipball/6d893938171a817f4e9bc9e86f2da1e370b7bcd7",
+                "reference": "6d893938171a817f4e9bc9e86f2da1e370b7bcd7",
                 "shasum": ""
             },
             "require": {
                 "markdown",
                 "parser"
             ],
-            "time": "2018-03-08T01:11:30+00:00"
+            "time": "2019-03-17T18:48:37+00:00"
         },
         {
             "name": "fideloper/proxy",
-            "version": "3.3.4",
+            "version": "4.2.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/fideloper/TrustedProxy.git",
-                "reference": "9cdf6f118af58d89764249bbcc7bb260c132924f"
+                "reference": "03085e58ec7bee24773fa5a8850751a6e61a7e8a"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/fideloper/TrustedProxy/zipball/9cdf6f118af58d89764249bbcc7bb260c132924f",
-                "reference": "9cdf6f118af58d89764249bbcc7bb260c132924f",
+                "url": "https://api.github.com/repos/fideloper/TrustedProxy/zipball/03085e58ec7bee24773fa5a8850751a6e61a7e8a",
+                "reference": "03085e58ec7bee24773fa5a8850751a6e61a7e8a",
                 "shasum": ""
             },
             "require": {
-                "illuminate/contracts": "~5.0",
+                "illuminate/contracts": "^5.0|^6.0|^7.0",
                 "php": ">=5.4.0"
             },
             "require-dev": {
-                "illuminate/http": "~5.0",
-                "mockery/mockery": "~0.9.3",
-                "phpunit/phpunit": "^5.7"
+                "illuminate/http": "^5.0|^6.0|^7.0",
+                "mockery/mockery": "^1.0",
+                "phpunit/phpunit": "^6.0"
             },
             "type": "library",
             "extra": {
-                "branch-alias": {
-                    "dev-master": "3.3-dev"
-                },
                 "laravel": {
                     "providers": [
                         "Fideloper\\Proxy\\TrustedProxyServiceProvider"
                 "proxy",
                 "trusted proxy"
             ],
-            "time": "2017-06-15T17:19:42+00:00"
+            "time": "2019-09-03T16:45:42+00:00"
         },
         {
             "name": "gathercontent/htmldiff",
         },
         {
             "name": "guzzlehttp/psr7",
-            "version": "1.5.2",
+            "version": "1.6.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/guzzle/psr7.git",
-                "reference": "9f83dded91781a01c63574e387eaa769be769115"
+                "reference": "239400de7a173fe9901b9ac7c06497751f00727a"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/guzzle/psr7/zipball/9f83dded91781a01c63574e387eaa769be769115",
-                "reference": "9f83dded91781a01c63574e387eaa769be769115",
+                "url": "https://api.github.com/repos/guzzle/psr7/zipball/239400de7a173fe9901b9ac7c06497751f00727a",
+                "reference": "239400de7a173fe9901b9ac7c06497751f00727a",
                 "shasum": ""
             },
             "require": {
                 "php": ">=5.4.0",
                 "psr/http-message": "~1.0",
-                "ralouphie/getallheaders": "^2.0.5"
+                "ralouphie/getallheaders": "^2.0.5 || ^3.0.0"
             },
             "provide": {
                 "psr/http-message-implementation": "1.0"
             },
             "require-dev": {
+                "ext-zlib": "*",
                 "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.8"
             },
+            "suggest": {
+                "zendframework/zend-httphandlerrunner": "Emit PSR-7 responses"
+            },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.5-dev"
+                    "dev-master": "1.6-dev"
                 }
             },
             "autoload": {
                 "uri",
                 "url"
             ],
-            "time": "2018-12-04T20:46:45+00:00"
+            "time": "2019-07-01T23:21:34+00:00"
         },
         {
             "name": "intervention/image",
-            "version": "2.4.2",
+            "version": "2.5.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/Intervention/image.git",
-                "reference": "e82d274f786e3d4b866a59b173f42e716f0783eb"
+                "reference": "39eaef720d082ecc54c64bf54541c55f10db546d"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/Intervention/image/zipball/e82d274f786e3d4b866a59b173f42e716f0783eb",
-                "reference": "e82d274f786e3d4b866a59b173f42e716f0783eb",
+                "url": "https://api.github.com/repos/Intervention/image/zipball/39eaef720d082ecc54c64bf54541c55f10db546d",
+                "reference": "39eaef720d082ecc54c64bf54541c55f10db546d",
                 "shasum": ""
             },
             "require": {
                 "thumbnail",
                 "watermark"
             ],
-            "time": "2018-05-29T14:19:03+00:00"
+            "time": "2019-06-24T14:06:31+00:00"
         },
         {
             "name": "knplabs/knp-snappy",
-            "version": "v1.0.4",
+            "version": "v1.1.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/KnpLabs/snappy.git",
-                "reference": "144c4ecd1ccaeda936bf832b93079efc490e6850"
+                "reference": "ea037298d3c613454da77ecb9588cf0397d695e1"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/KnpLabs/snappy/zipball/144c4ecd1ccaeda936bf832b93079efc490e6850",
-                "reference": "144c4ecd1ccaeda936bf832b93079efc490e6850",
+                "url": "https://api.github.com/repos/KnpLabs/snappy/zipball/ea037298d3c613454da77ecb9588cf0397d695e1",
+                "reference": "ea037298d3c613454da77ecb9588cf0397d695e1",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.6",
+                "php": ">=7.1",
                 "psr/log": "^1.0",
-                "symfony/process": "~2.3 || ~3.0 || ~4.0"
+                "symfony/process": "~3.4||~4.1"
             },
             "require-dev": {
-                "phpunit/phpunit": "~4.8.36"
+                "phpunit/phpunit": "~7.4"
             },
             "suggest": {
                 "h4cc/wkhtmltoimage-amd64": "Provides wkhtmltoimage-amd64 binary for Linux-compatible machines, use version `~0.12` as dependency",
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.0.x-dev"
+                    "dev-master": "1.x-dev"
                 }
             },
             "autoload": {
                 "thumbnail",
                 "wkhtmltopdf"
             ],
-            "time": "2018-01-22T19:40:51+00:00"
+            "time": "2018-12-14T14:59:37+00:00"
         },
         {
             "name": "laravel/framework",
-            "version": "v5.5.44",
+            "version": "v6.0.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/laravel/framework.git",
-                "reference": "00615aa27eb98f0ee6fb9f2160c6c60ae04abd1b"
+                "reference": "56789e9dec750e0fbe8e9e6ae90a01a4e6887902"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/laravel/framework/zipball/00615aa27eb98f0ee6fb9f2160c6c60ae04abd1b",
-                "reference": "00615aa27eb98f0ee6fb9f2160c6c60ae04abd1b",
+                "url": "https://api.github.com/repos/laravel/framework/zipball/56789e9dec750e0fbe8e9e6ae90a01a4e6887902",
+                "reference": "56789e9dec750e0fbe8e9e6ae90a01a4e6887902",
                 "shasum": ""
             },
             "require": {
-                "doctrine/inflector": "~1.1",
-                "erusev/parsedown": "~1.7",
+                "doctrine/inflector": "^1.1",
+                "dragonmantank/cron-expression": "^2.0",
+                "egulias/email-validator": "^2.1.10",
+                "erusev/parsedown": "^1.7",
+                "ext-json": "*",
                 "ext-mbstring": "*",
                 "ext-openssl": "*",
                 "league/flysystem": "^1.0.8",
-                "monolog/monolog": "~1.12",
-                "mtdowling/cron-expression": "~1.0",
-                "nesbot/carbon": "^1.24.1",
-                "php": ">=7.0",
-                "psr/container": "~1.0",
+                "monolog/monolog": "^1.12|^2.0",
+                "nesbot/carbon": "^2.0",
+                "opis/closure": "^3.1",
+                "php": "^7.2",
+                "psr/container": "^1.0",
                 "psr/simple-cache": "^1.0",
-                "ramsey/uuid": "~3.0",
-                "swiftmailer/swiftmailer": "~6.0",
-                "symfony/console": "~3.3",
-                "symfony/debug": "~3.3",
-                "symfony/finder": "~3.3",
-                "symfony/http-foundation": "~3.3",
-                "symfony/http-kernel": "~3.3",
-                "symfony/process": "~3.3",
-                "symfony/routing": "~3.3",
-                "symfony/var-dumper": "~3.3",
-                "tijsverkoyen/css-to-inline-styles": "~2.2",
-                "vlucas/phpdotenv": "~2.2"
+                "ramsey/uuid": "^3.7",
+                "swiftmailer/swiftmailer": "^6.0",
+                "symfony/console": "^4.3.4",
+                "symfony/debug": "^4.3.4",
+                "symfony/finder": "^4.3.4",
+                "symfony/http-foundation": "^4.3.4",
+                "symfony/http-kernel": "^4.3.4",
+                "symfony/process": "^4.3.4",
+                "symfony/routing": "^4.3.4",
+                "symfony/var-dumper": "^4.3.4",
+                "tijsverkoyen/css-to-inline-styles": "^2.2.1",
+                "vlucas/phpdotenv": "^3.3"
+            },
+            "conflict": {
+                "tightenco/collect": "<5.5.33"
             },
             "replace": {
                 "illuminate/auth": "self.version",
                 "illuminate/support": "self.version",
                 "illuminate/translation": "self.version",
                 "illuminate/validation": "self.version",
-                "illuminate/view": "self.version",
-                "tightenco/collect": "<5.5.33"
+                "illuminate/view": "self.version"
             },
             "require-dev": {
-                "aws/aws-sdk-php": "~3.0",
-                "doctrine/dbal": "~2.5",
-                "filp/whoops": "^2.1.4",
-                "mockery/mockery": "~1.0",
-                "orchestra/testbench-core": "3.5.*",
-                "pda/pheanstalk": "~3.0",
-                "phpunit/phpunit": "~6.0",
+                "aws/aws-sdk-php": "^3.0",
+                "doctrine/dbal": "^2.6",
+                "filp/whoops": "^2.4",
+                "guzzlehttp/guzzle": "^6.3",
+                "league/flysystem-cached-adapter": "^1.0",
+                "mockery/mockery": "^1.2.3",
+                "moontoast/math": "^1.1",
+                "orchestra/testbench-core": "^4.0",
+                "pda/pheanstalk": "^4.0",
+                "phpunit/phpunit": "^8.3",
                 "predis/predis": "^1.1.1",
-                "symfony/css-selector": "~3.3",
-                "symfony/dom-crawler": "~3.3"
+                "symfony/cache": "^4.3",
+                "true/punycode": "^2.1"
             },
             "suggest": {
-                "aws/aws-sdk-php": "Required to use the SQS queue driver and SES mail driver (~3.0).",
-                "doctrine/dbal": "Required to rename columns and drop SQLite columns (~2.5).",
+                "aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage and SES mail driver (^3.0).",
+                "doctrine/dbal": "Required to rename columns and drop SQLite columns (^2.6).",
+                "ext-gd": "Required to use Illuminate\\Http\\Testing\\FileFactory::image().",
+                "ext-memcached": "Required to use the memcache cache driver.",
                 "ext-pcntl": "Required to use all features of the queue worker.",
                 "ext-posix": "Required to use all features of the queue worker.",
-                "fzaninotto/faker": "Required to use the eloquent factory builder (~1.4).",
-                "guzzlehttp/guzzle": "Required to use the Mailgun and Mandrill mail drivers and the ping methods on schedules (~6.0).",
-                "laravel/tinker": "Required to use the tinker console command (~1.0).",
-                "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (~1.0).",
-                "league/flysystem-cached-adapter": "Required to use Flysystem caching (~1.0).",
-                "league/flysystem-rackspace": "Required to use the Flysystem Rackspace driver (~1.0).",
-                "nexmo/client": "Required to use the Nexmo transport (~1.0).",
-                "pda/pheanstalk": "Required to use the beanstalk queue driver (~3.0).",
-                "predis/predis": "Required to use the redis cache and queue drivers (~1.0).",
-                "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (~3.0).",
-                "symfony/css-selector": "Required to use some of the crawler integration testing tools (~3.3).",
-                "symfony/dom-crawler": "Required to use most of the crawler integration testing tools (~3.3).",
-                "symfony/psr-http-message-bridge": "Required to psr7 bridging features (~1.0)."
+                "ext-redis": "Required to use the Redis cache and queue drivers.",
+                "filp/whoops": "Required for friendly error pages in development (^2.4).",
+                "fzaninotto/faker": "Required to use the eloquent factory builder (^1.4).",
+                "guzzlehttp/guzzle": "Required to use the Mailgun mail driver and the ping methods on schedules (^6.0).",
+                "laravel/tinker": "Required to use the tinker console command (^1.0).",
+                "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^1.0).",
+                "league/flysystem-cached-adapter": "Required to use the Flysystem cache (^1.0).",
+                "league/flysystem-sftp": "Required to use the Flysystem SFTP driver (^1.0).",
+                "moontoast/math": "Required to use ordered UUIDs (^1.1).",
+                "pda/pheanstalk": "Required to use the beanstalk queue driver (^4.0).",
+                "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^3.0).",
+                "symfony/cache": "Required to PSR-6 cache bridge (^4.3.4).",
+                "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^1.2).",
+                "wildbit/swiftmailer-postmark": "Required to use Postmark mail driver (^3.0)."
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "5.5-dev"
+                    "dev-master": "6.x-dev"
                 }
             },
             "autoload": {
                 "framework",
                 "laravel"
             ],
-            "time": "2018-10-04T14:51:24+00:00"
+            "time": "2019-09-10T18:46:24+00:00"
         },
         {
             "name": "laravel/socialite",
-            "version": "3.0.x-dev",
+            "version": "v4.2.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/laravel/socialite.git",
-                "reference": "79316f36641f1916a50ab14d368acdf1d97e46de"
+                "reference": "f509d06e1e7323997b804c5152874f8aad4508e9"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/laravel/socialite/zipball/79316f36641f1916a50ab14d368acdf1d97e46de",
-                "reference": "79316f36641f1916a50ab14d368acdf1d97e46de",
+                "url": "https://api.github.com/repos/laravel/socialite/zipball/f509d06e1e7323997b804c5152874f8aad4508e9",
+                "reference": "f509d06e1e7323997b804c5152874f8aad4508e9",
                 "shasum": ""
             },
             "require": {
+                "ext-json": "*",
                 "guzzlehttp/guzzle": "~6.0",
-                "illuminate/contracts": "~5.4",
-                "illuminate/http": "~5.4",
-                "illuminate/support": "~5.4",
+                "illuminate/http": "~5.7.0|~5.8.0|^6.0|^7.0",
+                "illuminate/support": "~5.7.0|~5.8.0|^6.0|^7.0",
                 "league/oauth1-client": "~1.0",
-                "php": ">=5.6.4"
+                "php": "^7.1.3"
             },
             "require-dev": {
-                "mockery/mockery": "~0.9",
-                "phpunit/phpunit": "~4.0|~5.0"
+                "illuminate/contracts": "~5.7.0|~5.8.0|^6.0|^7.0",
+                "mockery/mockery": "^1.0",
+                "phpunit/phpunit": "^7.0|^8.0"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.0-dev"
+                    "dev-master": "4.0-dev"
                 },
                 "laravel": {
                     "providers": [
                 "laravel",
                 "oauth"
             ],
-            "time": "2018-12-21T14:06:32+00:00"
+            "time": "2019-09-03T15:27:17+00:00"
         },
         {
             "name": "league/flysystem",
-            "version": "1.0.49",
+            "version": "1.0.55",
             "source": {
                 "type": "git",
                 "url": "https://github.com/thephpleague/flysystem.git",
-                "reference": "a63cc83d8a931b271be45148fa39ba7156782ffd"
+                "reference": "33c91155537c6dc899eacdc54a13ac6303f156e6"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/a63cc83d8a931b271be45148fa39ba7156782ffd",
-                "reference": "a63cc83d8a931b271be45148fa39ba7156782ffd",
+                "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/33c91155537c6dc899eacdc54a13ac6303f156e6",
+                "reference": "33c91155537c6dc899eacdc54a13ac6303f156e6",
                 "shasum": ""
             },
             "require": {
                 "sftp",
                 "storage"
             ],
-            "time": "2018-11-23T23:41:29+00:00"
+            "time": "2019-08-24T11:17:19+00:00"
         },
         {
             "name": "league/flysystem-aws-s3-v3",
-            "version": "1.0.21",
+            "version": "1.0.23",
             "source": {
                 "type": "git",
                 "url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git",
-                "reference": "43523fec10a831ea48bedb3277e3f3fa218f4e49"
+                "reference": "15b0cdeab7240bf8e8bffa85ae5275bbc3692bf4"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/43523fec10a831ea48bedb3277e3f3fa218f4e49",
-                "reference": "43523fec10a831ea48bedb3277e3f3fa218f4e49",
+                "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/15b0cdeab7240bf8e8bffa85ae5275bbc3692bf4",
+                "reference": "15b0cdeab7240bf8e8bffa85ae5275bbc3692bf4",
                 "shasum": ""
             },
             "require": {
                 }
             ],
             "description": "Flysystem adapter for the AWS S3 SDK v3.x",
-            "time": "2018-10-08T07:53:55+00:00"
+            "time": "2019-06-05T17:18:29+00:00"
         },
         {
             "name": "league/oauth1-client",
         },
         {
             "name": "monolog/monolog",
-            "version": "1.24.0",
+            "version": "2.0.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/Seldaek/monolog.git",
-                "reference": "bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266"
+                "reference": "68545165e19249013afd1d6f7485aecff07a2d22"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/Seldaek/monolog/zipball/bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266",
-                "reference": "bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266",
+                "url": "https://api.github.com/repos/Seldaek/monolog/zipball/68545165e19249013afd1d6f7485aecff07a2d22",
+                "reference": "68545165e19249013afd1d6f7485aecff07a2d22",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.3.0",
-                "psr/log": "~1.0"
+                "php": "^7.2",
+                "psr/log": "^1.0.1"
             },
             "provide": {
                 "psr/log-implementation": "1.0.0"
             "require-dev": {
                 "aws/aws-sdk-php": "^2.4.9 || ^3.0",
                 "doctrine/couchdb": "~1.0@dev",
-                "graylog2/gelf-php": "~1.0",
-                "jakub-onderka/php-parallel-lint": "0.9",
+                "elasticsearch/elasticsearch": "^6.0",
+                "graylog2/gelf-php": "^1.4.2",
+                "jakub-onderka/php-parallel-lint": "^0.9",
                 "php-amqplib/php-amqplib": "~2.4",
                 "php-console/php-console": "^3.1.3",
-                "phpunit/phpunit": "~4.5",
-                "phpunit/phpunit-mock-objects": "2.3.0",
+                "phpspec/prophecy": "^1.6.1",
+                "phpunit/phpunit": "^8.3",
+                "predis/predis": "^1.1",
+                "rollbar/rollbar": "^1.3",
                 "ruflin/elastica": ">=0.90 <3.0",
-                "sentry/sentry": "^0.13",
                 "swiftmailer/swiftmailer": "^5.3|^6.0"
             },
             "suggest": {
                 "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB",
                 "doctrine/couchdb": "Allow sending log messages to a CouchDB server",
+                "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client",
                 "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)",
-                "ext-mongo": "Allow sending log messages to a MongoDB server",
+                "ext-mbstring": "Allow to work properly with unicode symbols",
+                "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)",
                 "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server",
-                "mongodb/mongodb": "Allow sending log messages to a MongoDB server via PHP Driver",
+                "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)",
                 "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib",
                 "php-console/php-console": "Allow sending log messages to Google Chrome",
                 "rollbar/rollbar": "Allow sending log messages to Rollbar",
-                "ruflin/elastica": "Allow sending log messages to an Elastic Search server",
-                "sentry/sentry": "Allow sending log messages to a Sentry server"
+                "ruflin/elastica": "Allow sending log messages to an Elastic Search server"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "2.0.x-dev"
+                    "dev-master": "2.x-dev"
                 }
             },
             "autoload": {
                 "logging",
                 "psr-3"
             ],
-            "time": "2018-11-05T09:00:11+00:00"
+            "time": "2019-08-30T09:56:44+00:00"
         },
         {
-            "name": "mtdowling/cron-expression",
-            "version": "v1.2.1",
+            "name": "mtdowling/jmespath.php",
+            "version": "2.4.0",
             "source": {
                 "type": "git",
-                "url": "https://github.com/mtdowling/cron-expression.git",
-                "reference": "9504fa9ea681b586028adaaa0877db4aecf32bad"
+                "url": "https://github.com/jmespath/jmespath.php.git",
+                "reference": "adcc9531682cf87dfda21e1fd5d0e7a41d292fac"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/mtdowling/cron-expression/zipball/9504fa9ea681b586028adaaa0877db4aecf32bad",
-                "reference": "9504fa9ea681b586028adaaa0877db4aecf32bad",
+                "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/adcc9531682cf87dfda21e1fd5d0e7a41d292fac",
+                "reference": "adcc9531682cf87dfda21e1fd5d0e7a41d292fac",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.3.2"
+                "php": ">=5.4.0"
             },
             "require-dev": {
-                "phpunit/phpunit": "~4.0|~5.0"
-            },
-            "type": "library",
-            "autoload": {
-                "psr-4": {
-                    "Cron\\": "src/Cron/"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Michael Dowling",
-                    "email": "[email protected]",
-                    "homepage": "https://github.com/mtdowling"
-                }
-            ],
-            "description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due",
-            "keywords": [
-                "cron",
-                "schedule"
-            ],
-            "time": "2017-01-23T04:29:33+00:00"
-        },
-        {
-            "name": "mtdowling/jmespath.php",
-            "version": "2.4.0",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/jmespath/jmespath.php.git",
-                "reference": "adcc9531682cf87dfda21e1fd5d0e7a41d292fac"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/adcc9531682cf87dfda21e1fd5d0e7a41d292fac",
-                "reference": "adcc9531682cf87dfda21e1fd5d0e7a41d292fac",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=5.4.0"
-            },
-            "require-dev": {
-                "phpunit/phpunit": "~4.0"
+                "phpunit/phpunit": "~4.0"
             },
             "bin": [
                 "bin/jp.php"
         },
         {
             "name": "nesbot/carbon",
-            "version": "1.36.2",
+            "version": "2.24.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/briannesbitt/Carbon.git",
-                "reference": "cd324b98bc30290f233dd0e75e6ce49f7ab2a6c9"
+                "reference": "934459c5ac0658bc765ad1e53512c7c77adcac29"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/cd324b98bc30290f233dd0e75e6ce49f7ab2a6c9",
-                "reference": "cd324b98bc30290f233dd0e75e6ce49f7ab2a6c9",
+                "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/934459c5ac0658bc765ad1e53512c7c77adcac29",
+                "reference": "934459c5ac0658bc765ad1e53512c7c77adcac29",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.3.9",
-                "symfony/translation": "~2.6 || ~3.0 || ~4.0"
+                "ext-json": "*",
+                "php": "^7.1.8 || ^8.0",
+                "symfony/translation": "^3.4 || ^4.0"
             },
             "require-dev": {
-                "phpunit/phpunit": "^4.8.35 || ^5.7"
-            },
-            "suggest": {
-                "friendsofphp/php-cs-fixer": "Needed for the `composer phpcs` command. Allow to automatically fix code style.",
-                "phpstan/phpstan": "Needed for the `composer phpstan` command. Allow to detect potential errors."
+                "friendsofphp/php-cs-fixer": "^2.14 || ^3.0",
+                "kylekatarnls/multi-tester": "^1.1",
+                "phpmd/phpmd": "dev-php-7.1-compatibility",
+                "phpstan/phpstan": "^0.11",
+                "phpunit/phpunit": "^7.5 || ^8.0",
+                "squizlabs/php_codesniffer": "^3.4"
             },
+            "bin": [
+                "bin/carbon"
+            ],
             "type": "library",
             "extra": {
                 "laravel": {
             },
             "autoload": {
                 "psr-4": {
-                    "": "src/"
+                    "Carbon\\": "src/Carbon/"
                 }
             },
             "notification-url": "https://packagist.org/downloads/",
                     "name": "Brian Nesbitt",
                     "email": "[email protected]",
                     "homepage": "http://nesbot.com"
+                },
+                {
+                    "name": "kylekatarnls",
+                    "homepage": "http://github.com/kylekatarnls"
                 }
             ],
-            "description": "A simple API extension for DateTime.",
+            "description": "A API extension for DateTime that supports 281 different languages.",
             "homepage": "http://carbon.nesbot.com",
             "keywords": [
                 "date",
                 "datetime",
                 "time"
             ],
-            "time": "2018-12-28T10:07:33+00:00"
+            "time": "2019-08-31T16:37:55+00:00"
+        },
+        {
+            "name": "opis/closure",
+            "version": "3.4.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/opis/closure.git",
+                "reference": "60a97fff133b1669a5b1776aa8ab06db3f3962b7"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/opis/closure/zipball/60a97fff133b1669a5b1776aa8ab06db3f3962b7",
+                "reference": "60a97fff133b1669a5b1776aa8ab06db3f3962b7",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^5.4 || ^7.0"
+            },
+            "require-dev": {
+                "jeremeamia/superclosure": "^2.0",
+                "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "3.3.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Opis\\Closure\\": "src/"
+                },
+                "files": [
+                    "functions.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Marius Sarca",
+                    "email": "[email protected]"
+                },
+                {
+                    "name": "Sorin Sarca",
+                    "email": "[email protected]"
+                }
+            ],
+            "description": "A library that can be used to serialize closures (anonymous functions) and arbitrary objects.",
+            "homepage": "https://opis.io/closure",
+            "keywords": [
+                "anonymous functions",
+                "closure",
+                "function",
+                "serializable",
+                "serialization",
+                "serialize"
+            ],
+            "time": "2019-09-02T21:07:33+00:00"
         },
         {
             "name": "paragonie/random_compat",
         },
         {
             "name": "phenx/php-svg-lib",
-            "version": "v0.3.2",
+            "version": "v0.3.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/PhenX/php-svg-lib.git",
-                "reference": "ccc46ef6340d4b8a4a68047e68d8501ea961442c"
+                "reference": "5fa61b65e612ce1ae15f69b3d223cb14ecc60e32"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/PhenX/php-svg-lib/zipball/ccc46ef6340d4b8a4a68047e68d8501ea961442c",
-                "reference": "ccc46ef6340d4b8a4a68047e68d8501ea961442c",
+                "url": "https://api.github.com/repos/PhenX/php-svg-lib/zipball/5fa61b65e612ce1ae15f69b3d223cb14ecc60e32",
+                "reference": "5fa61b65e612ce1ae15f69b3d223cb14ecc60e32",
                 "shasum": ""
             },
             "require": {
-                "sabberworm/php-css-parser": "8.1.*"
+                "sabberworm/php-css-parser": "^8.3"
             },
             "require-dev": {
-                "phpunit/phpunit": "~5.0"
+                "phpunit/phpunit": "^5.5|^6.5"
             },
             "type": "library",
             "autoload": {
-                "psr-0": {
-                    "Svg\\": "src/"
+                "psr-4": {
+                    "Svg\\": "src/Svg"
                 }
             },
             "notification-url": "https://packagist.org/downloads/",
             ],
             "description": "A library to read, parse and export to PDF SVG files.",
             "homepage": "https://github.com/PhenX/php-svg-lib",
-            "time": "2018-06-03T10:10:03+00:00"
+            "time": "2019-09-11T20:02:13+00:00"
+        },
+        {
+            "name": "phpoption/phpoption",
+            "version": "1.5.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/schmittjoh/php-option.git",
+                "reference": "94e644f7d2051a5f0fcf77d81605f152eecff0ed"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/94e644f7d2051a5f0fcf77d81605f152eecff0ed",
+                "reference": "94e644f7d2051a5f0fcf77d81605f152eecff0ed",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "4.7.*"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.3-dev"
+                }
+            },
+            "autoload": {
+                "psr-0": {
+                    "PhpOption\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "Apache2"
+            ],
+            "authors": [
+                {
+                    "name": "Johannes M. Schmitt",
+                    "email": "[email protected]"
+                }
+            ],
+            "description": "Option Type for PHP",
+            "keywords": [
+                "language",
+                "option",
+                "php",
+                "type"
+            ],
+            "time": "2015-07-25T16:39:46+00:00"
         },
         {
             "name": "predis/predis",
         },
         {
             "name": "ralouphie/getallheaders",
-            "version": "2.0.5",
+            "version": "3.0.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/ralouphie/getallheaders.git",
-                "reference": "5601c8a83fbba7ef674a7369456d12f1e0d0eafa"
+                "reference": "120b605dfeb996808c31b6477290a714d356e822"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/5601c8a83fbba7ef674a7369456d12f1e0d0eafa",
-                "reference": "5601c8a83fbba7ef674a7369456d12f1e0d0eafa",
+                "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822",
+                "reference": "120b605dfeb996808c31b6477290a714d356e822",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.3"
+                "php": ">=5.6"
             },
             "require-dev": {
-                "phpunit/phpunit": "~3.7.0",
-                "satooshi/php-coveralls": ">=1.0"
+                "php-coveralls/php-coveralls": "^2.1",
+                "phpunit/phpunit": "^5 || ^6.5"
             },
             "type": "library",
             "autoload": {
                 }
             ],
             "description": "A polyfill for getallheaders.",
-            "time": "2016-02-11T07:05:27+00:00"
+            "time": "2019-03-08T08:55:37+00:00"
         },
         {
             "name": "ramsey/uuid",
         },
         {
             "name": "sabberworm/php-css-parser",
-            "version": "8.1.0",
+            "version": "8.3.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sabberworm/PHP-CSS-Parser.git",
-                "reference": "850cbbcbe7fbb155387a151ea562897a67e242ef"
+                "reference": "91bcc3e3fdb7386c9a2e0e0aa09ca75cc43f121f"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sabberworm/PHP-CSS-Parser/zipball/850cbbcbe7fbb155387a151ea562897a67e242ef",
-                "reference": "850cbbcbe7fbb155387a151ea562897a67e242ef",
+                "url": "https://api.github.com/repos/sabberworm/PHP-CSS-Parser/zipball/91bcc3e3fdb7386c9a2e0e0aa09ca75cc43f121f",
+                "reference": "91bcc3e3fdb7386c9a2e0e0aa09ca75cc43f121f",
                 "shasum": ""
             },
             "require": {
                 "php": ">=5.3.2"
             },
             "require-dev": {
-                "phpunit/phpunit": "*"
+                "codacy/coverage": "^1.4",
+                "phpunit/phpunit": "~4.8"
             },
             "type": "library",
             "autoload": {
                 "parser",
                 "stylesheet"
             ],
-            "time": "2016-07-19T19:14:21+00:00"
+            "time": "2019-02-22T07:42:52+00:00"
         },
         {
             "name": "socialiteproviders/discord",
         },
         {
             "name": "socialiteproviders/manager",
-            "version": "v3.3.4",
+            "version": "v3.4.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/SocialiteProviders/Manager.git",
-                "reference": "58b72a667da292a1d0a0b1e6e9aeda4053617030"
+                "reference": "e3e8e78b9a3060801cd008941a0894a0a0c479e1"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/SocialiteProviders/Manager/zipball/58b72a667da292a1d0a0b1e6e9aeda4053617030",
-                "reference": "58b72a667da292a1d0a0b1e6e9aeda4053617030",
+                "url": "https://api.github.com/repos/SocialiteProviders/Manager/zipball/e3e8e78b9a3060801cd008941a0894a0a0c479e1",
+                "reference": "e3e8e78b9a3060801cd008941a0894a0a0c479e1",
                 "shasum": ""
             },
             "require": {
+                "illuminate/support": "~5.4|~5.7.0|~5.8.0|^6.0",
                 "laravel/socialite": "~3.0|~4.0",
                 "php": "^5.6 || ^7.0"
             },
                 {
                     "name": "Anton Komarev",
                     "email": "[email protected]"
+                },
+                {
+                    "name": "Miguel Piedrafita",
+                    "email": "[email protected]"
                 }
             ],
             "description": "Easily add new or override built-in providers in Laravel Socialite.",
-            "time": "2019-01-16T07:58:54+00:00"
+            "time": "2019-09-09T03:07:52+00:00"
         },
         {
             "name": "socialiteproviders/microsoft-azure",
         },
         {
             "name": "socialiteproviders/twitch",
-            "version": "v3.0.0",
+            "version": "v5.0.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/SocialiteProviders/Twitch.git",
-                "reference": "a7ad148c0b42d0c607d8a034b6e47faf5fc85e93"
+                "reference": "8c19b26ff24c40cc019413042a5492c5ed21a658"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/SocialiteProviders/Twitch/zipball/a7ad148c0b42d0c607d8a034b6e47faf5fc85e93",
-                "reference": "a7ad148c0b42d0c607d8a034b6e47faf5fc85e93",
+                "url": "https://api.github.com/repos/SocialiteProviders/Twitch/zipball/8c19b26ff24c40cc019413042a5492c5ed21a658",
+                "reference": "8c19b26ff24c40cc019413042a5492c5ed21a658",
                 "shasum": ""
             },
             "require": {
                 "php": "^5.6 || ^7.0",
-                "socialiteproviders/manager": "~3.0"
+                "socialiteproviders/manager": "~2.0 || ~3.0"
             },
             "type": "library",
             "autoload": {
                 }
             ],
             "description": "Twitch OAuth2 Provider for Laravel Socialite",
-            "time": "2017-01-25T09:48:29+00:00"
+            "time": "2018-06-20T10:59:51+00:00"
         },
         {
             "name": "swiftmailer/swiftmailer",
-            "version": "v6.1.3",
+            "version": "v6.2.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/swiftmailer/swiftmailer.git",
-                "reference": "8ddcb66ac10c392d3beb54829eef8ac1438595f4"
+                "reference": "5397cd05b0a0f7937c47b0adcb4c60e5ab936b6a"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/8ddcb66ac10c392d3beb54829eef8ac1438595f4",
-                "reference": "8ddcb66ac10c392d3beb54829eef8ac1438595f4",
+                "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/5397cd05b0a0f7937c47b0adcb4c60e5ab936b6a",
+                "reference": "5397cd05b0a0f7937c47b0adcb4c60e5ab936b6a",
                 "shasum": ""
             },
             "require": {
                 "egulias/email-validator": "~2.0",
-                "php": ">=7.0.0"
+                "php": ">=7.0.0",
+                "symfony/polyfill-iconv": "^1.0",
+                "symfony/polyfill-intl-idn": "^1.10",
+                "symfony/polyfill-mbstring": "^1.0"
             },
             "require-dev": {
                 "mockery/mockery": "~0.9.1",
-                "symfony/phpunit-bridge": "~3.3@dev"
+                "symfony/phpunit-bridge": "^3.4.19|^4.1.8"
             },
             "suggest": {
                 "ext-intl": "Needed to support internationalized email addresses",
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "6.1-dev"
+                    "dev-master": "6.2-dev"
                 }
             },
             "autoload": {
                 "mail",
                 "mailer"
             ],
-            "time": "2018-09-11T07:12:52+00:00"
+            "time": "2019-04-21T09:21:45+00:00"
         },
         {
             "name": "symfony/console",
-            "version": "v3.3.6",
+            "version": "v4.3.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/console.git",
-                "reference": "b0878233cb5c4391347e5495089c7af11b8e6201"
+                "reference": "de63799239b3881b8a08f8481b22348f77ed7b36"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/console/zipball/b0878233cb5c4391347e5495089c7af11b8e6201",
-                "reference": "b0878233cb5c4391347e5495089c7af11b8e6201",
+                "url": "https://api.github.com/repos/symfony/console/zipball/de63799239b3881b8a08f8481b22348f77ed7b36",
+                "reference": "de63799239b3881b8a08f8481b22348f77ed7b36",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.5.9",
-                "symfony/debug": "~2.8|~3.0",
-                "symfony/polyfill-mbstring": "~1.0"
+                "php": "^7.1.3",
+                "symfony/polyfill-mbstring": "~1.0",
+                "symfony/polyfill-php73": "^1.8",
+                "symfony/service-contracts": "^1.1"
             },
             "conflict": {
-                "symfony/dependency-injection": "<3.3"
+                "symfony/dependency-injection": "<3.4",
+                "symfony/event-dispatcher": "<4.3",
+                "symfony/process": "<3.3"
+            },
+            "provide": {
+                "psr/log-implementation": "1.0"
             },
             "require-dev": {
                 "psr/log": "~1.0",
-                "symfony/config": "~3.3",
-                "symfony/dependency-injection": "~3.3",
-                "symfony/event-dispatcher": "~2.8|~3.0",
-                "symfony/filesystem": "~2.8|~3.0",
-                "symfony/http-kernel": "~2.8|~3.0",
-                "symfony/process": "~2.8|~3.0"
+                "symfony/config": "~3.4|~4.0",
+                "symfony/dependency-injection": "~3.4|~4.0",
+                "symfony/event-dispatcher": "^4.3",
+                "symfony/lock": "~3.4|~4.0",
+                "symfony/process": "~3.4|~4.0",
+                "symfony/var-dumper": "^4.3"
             },
             "suggest": {
                 "psr/log": "For using the console logger",
                 "symfony/event-dispatcher": "",
-                "symfony/filesystem": "",
+                "symfony/lock": "",
                 "symfony/process": ""
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.3-dev"
+                    "dev-master": "4.3-dev"
                 }
             },
             "autoload": {
             ],
             "description": "Symfony Console Component",
             "homepage": "https://symfony.com",
-            "time": "2017-07-29T21:27:59+00:00"
+            "time": "2019-08-26T08:26:39+00:00"
         },
         {
             "name": "symfony/css-selector",
-            "version": "v3.1.10",
+            "version": "v4.3.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/css-selector.git",
-                "reference": "722a87478a72d95dc2a3bcf41dc9c2d13fd4cb2d"
+                "reference": "c6e5e2a00db768c92c3ae131532af4e1acc7bd03"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/css-selector/zipball/722a87478a72d95dc2a3bcf41dc9c2d13fd4cb2d",
-                "reference": "722a87478a72d95dc2a3bcf41dc9c2d13fd4cb2d",
+                "url": "https://api.github.com/repos/symfony/css-selector/zipball/c6e5e2a00db768c92c3ae131532af4e1acc7bd03",
+                "reference": "c6e5e2a00db768c92c3ae131532af4e1acc7bd03",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.5.9"
+                "php": "^7.1.3"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.1-dev"
+                    "dev-master": "4.3-dev"
                 }
             },
             "autoload": {
                 "MIT"
             ],
             "authors": [
-                {
-                    "name": "Jean-François Simon",
-                    "email": "[email protected]"
-                },
                 {
                     "name": "Fabien Potencier",
                     "email": "[email protected]"
                 },
+                {
+                    "name": "Jean-François Simon",
+                    "email": "[email protected]"
+                },
                 {
                     "name": "Symfony Community",
                     "homepage": "https://symfony.com/contributors"
             ],
             "description": "Symfony CssSelector Component",
             "homepage": "https://symfony.com",
-            "time": "2017-01-02T20:31:54+00:00"
+            "time": "2019-08-20T14:07:54+00:00"
         },
         {
             "name": "symfony/debug",
-            "version": "v3.3.6",
+            "version": "v4.3.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/debug.git",
-                "reference": "7c13ae8ce1e2adbbd574fc39de7be498e1284e13"
+                "reference": "afcdea44a2e399c1e4b52246ec8d54c715393ced"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/debug/zipball/7c13ae8ce1e2adbbd574fc39de7be498e1284e13",
-                "reference": "7c13ae8ce1e2adbbd574fc39de7be498e1284e13",
+                "url": "https://api.github.com/repos/symfony/debug/zipball/afcdea44a2e399c1e4b52246ec8d54c715393ced",
+                "reference": "afcdea44a2e399c1e4b52246ec8d54c715393ced",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.5.9",
+                "php": "^7.1.3",
                 "psr/log": "~1.0"
             },
             "conflict": {
-                "symfony/http-kernel": ">=2.3,<2.3.24|~2.4.0|>=2.5,<2.5.9|>=2.6,<2.6.2"
+                "symfony/http-kernel": "<3.4"
             },
             "require-dev": {
-                "symfony/http-kernel": "~2.8|~3.0"
+                "symfony/http-kernel": "~3.4|~4.0"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.3-dev"
+                    "dev-master": "4.3-dev"
                 }
             },
             "autoload": {
             ],
             "description": "Symfony Debug Component",
             "homepage": "https://symfony.com",
-            "time": "2017-07-28T15:27:31+00:00"
+            "time": "2019-08-20T14:27:59+00:00"
         },
         {
             "name": "symfony/event-dispatcher",
-            "version": "v3.3.6",
+            "version": "v4.3.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/event-dispatcher.git",
-                "reference": "67535f1e3fd662bdc68d7ba317c93eecd973617e"
+                "reference": "429d0a1451d4c9c4abe1959b2986b88794b9b7d2"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/67535f1e3fd662bdc68d7ba317c93eecd973617e",
-                "reference": "67535f1e3fd662bdc68d7ba317c93eecd973617e",
+                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/429d0a1451d4c9c4abe1959b2986b88794b9b7d2",
+                "reference": "429d0a1451d4c9c4abe1959b2986b88794b9b7d2",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.5.9"
+                "php": "^7.1.3",
+                "symfony/event-dispatcher-contracts": "^1.1"
             },
             "conflict": {
-                "symfony/dependency-injection": "<3.3"
+                "symfony/dependency-injection": "<3.4"
+            },
+            "provide": {
+                "psr/event-dispatcher-implementation": "1.0",
+                "symfony/event-dispatcher-implementation": "1.1"
             },
             "require-dev": {
                 "psr/log": "~1.0",
-                "symfony/config": "~2.8|~3.0",
-                "symfony/dependency-injection": "~3.3",
-                "symfony/expression-language": "~2.8|~3.0",
-                "symfony/stopwatch": "~2.8|~3.0"
+                "symfony/config": "~3.4|~4.0",
+                "symfony/dependency-injection": "~3.4|~4.0",
+                "symfony/expression-language": "~3.4|~4.0",
+                "symfony/http-foundation": "^3.4|^4.0",
+                "symfony/service-contracts": "^1.1",
+                "symfony/stopwatch": "~3.4|~4.0"
             },
             "suggest": {
                 "symfony/dependency-injection": "",
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.3-dev"
+                    "dev-master": "4.3-dev"
                 }
             },
             "autoload": {
             ],
             "description": "Symfony EventDispatcher Component",
             "homepage": "https://symfony.com",
-            "time": "2017-06-09T14:53:08+00:00"
+            "time": "2019-08-26T08:55:16+00:00"
+        },
+        {
+            "name": "symfony/event-dispatcher-contracts",
+            "version": "v1.1.5",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/event-dispatcher-contracts.git",
+                "reference": "c61766f4440ca687de1084a5c00b08e167a2575c"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/c61766f4440ca687de1084a5c00b08e167a2575c",
+                "reference": "c61766f4440ca687de1084a5c00b08e167a2575c",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.1.3"
+            },
+            "suggest": {
+                "psr/event-dispatcher": "",
+                "symfony/event-dispatcher-implementation": ""
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.1-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Contracts\\EventDispatcher\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "[email protected]"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Generic abstractions related to dispatching event",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "abstractions",
+                "contracts",
+                "decoupling",
+                "interfaces",
+                "interoperability",
+                "standards"
+            ],
+            "time": "2019-06-20T06:46:26+00:00"
         },
         {
             "name": "symfony/finder",
-            "version": "v3.3.6",
+            "version": "v4.3.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/finder.git",
-                "reference": "baea7f66d30854ad32988c11a09d7ffd485810c4"
+                "reference": "86c1c929f0a4b24812e1eb109262fc3372c8e9f2"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/finder/zipball/baea7f66d30854ad32988c11a09d7ffd485810c4",
-                "reference": "baea7f66d30854ad32988c11a09d7ffd485810c4",
+                "url": "https://api.github.com/repos/symfony/finder/zipball/86c1c929f0a4b24812e1eb109262fc3372c8e9f2",
+                "reference": "86c1c929f0a4b24812e1eb109262fc3372c8e9f2",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.5.9"
+                "php": "^7.1.3"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.3-dev"
+                    "dev-master": "4.3-dev"
                 }
             },
             "autoload": {
             ],
             "description": "Symfony Finder Component",
             "homepage": "https://symfony.com",
-            "time": "2017-06-01T21:01:25+00:00"
+            "time": "2019-08-14T12:26:46+00:00"
         },
         {
             "name": "symfony/http-foundation",
-            "version": "v3.3.6",
+            "version": "v4.3.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/http-foundation.git",
-                "reference": "49e8cd2d59a7aa9bfab19e46de680c76e500a031"
+                "reference": "d804bea118ff340a12e22a79f9c7e7eb56b35adc"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/http-foundation/zipball/49e8cd2d59a7aa9bfab19e46de680c76e500a031",
-                "reference": "49e8cd2d59a7aa9bfab19e46de680c76e500a031",
+                "url": "https://api.github.com/repos/symfony/http-foundation/zipball/d804bea118ff340a12e22a79f9c7e7eb56b35adc",
+                "reference": "d804bea118ff340a12e22a79f9c7e7eb56b35adc",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.5.9",
+                "php": "^7.1.3",
+                "symfony/mime": "^4.3",
                 "symfony/polyfill-mbstring": "~1.1"
             },
             "require-dev": {
-                "symfony/expression-language": "~2.8|~3.0"
+                "predis/predis": "~1.0",
+                "symfony/expression-language": "~3.4|~4.0"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.3-dev"
+                    "dev-master": "4.3-dev"
                 }
             },
             "autoload": {
             ],
             "description": "Symfony HttpFoundation Component",
             "homepage": "https://symfony.com",
-            "time": "2017-07-21T11:04:46+00:00"
+            "time": "2019-08-26T08:55:16+00:00"
         },
         {
             "name": "symfony/http-kernel",
-            "version": "v3.3.6",
+            "version": "v4.3.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/http-kernel.git",
-                "reference": "db10d05f1d95e4168e638db7a81c79616f568ea5"
+                "reference": "5e0fc71be03d52cd00c423061cfd300bd6f92a52"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/http-kernel/zipball/db10d05f1d95e4168e638db7a81c79616f568ea5",
-                "reference": "db10d05f1d95e4168e638db7a81c79616f568ea5",
+                "url": "https://api.github.com/repos/symfony/http-kernel/zipball/5e0fc71be03d52cd00c423061cfd300bd6f92a52",
+                "reference": "5e0fc71be03d52cd00c423061cfd300bd6f92a52",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.5.9",
+                "php": "^7.1.3",
                 "psr/log": "~1.0",
-                "symfony/debug": "~2.8|~3.0",
-                "symfony/event-dispatcher": "~2.8|~3.0",
-                "symfony/http-foundation": "~3.3"
+                "symfony/debug": "~3.4|~4.0",
+                "symfony/event-dispatcher": "^4.3",
+                "symfony/http-foundation": "^4.1.1",
+                "symfony/polyfill-ctype": "~1.8",
+                "symfony/polyfill-php73": "^1.9"
             },
             "conflict": {
-                "symfony/config": "<2.8",
-                "symfony/dependency-injection": "<3.3",
-                "symfony/var-dumper": "<3.3",
+                "symfony/browser-kit": "<4.3",
+                "symfony/config": "<3.4",
+                "symfony/dependency-injection": "<4.3",
+                "symfony/translation": "<4.2",
+                "symfony/var-dumper": "<4.1.1",
                 "twig/twig": "<1.34|<2.4,>=2"
             },
+            "provide": {
+                "psr/log-implementation": "1.0"
+            },
             "require-dev": {
                 "psr/cache": "~1.0",
-                "symfony/browser-kit": "~2.8|~3.0",
-                "symfony/class-loader": "~2.8|~3.0",
-                "symfony/config": "~2.8|~3.0",
-                "symfony/console": "~2.8|~3.0",
-                "symfony/css-selector": "~2.8|~3.0",
-                "symfony/dependency-injection": "~3.3",
-                "symfony/dom-crawler": "~2.8|~3.0",
-                "symfony/expression-language": "~2.8|~3.0",
-                "symfony/finder": "~2.8|~3.0",
-                "symfony/process": "~2.8|~3.0",
-                "symfony/routing": "~2.8|~3.0",
-                "symfony/stopwatch": "~2.8|~3.0",
-                "symfony/templating": "~2.8|~3.0",
-                "symfony/translation": "~2.8|~3.0",
-                "symfony/var-dumper": "~3.3"
+                "symfony/browser-kit": "^4.3",
+                "symfony/config": "~3.4|~4.0",
+                "symfony/console": "~3.4|~4.0",
+                "symfony/css-selector": "~3.4|~4.0",
+                "symfony/dependency-injection": "^4.3",
+                "symfony/dom-crawler": "~3.4|~4.0",
+                "symfony/expression-language": "~3.4|~4.0",
+                "symfony/finder": "~3.4|~4.0",
+                "symfony/process": "~3.4|~4.0",
+                "symfony/routing": "~3.4|~4.0",
+                "symfony/stopwatch": "~3.4|~4.0",
+                "symfony/templating": "~3.4|~4.0",
+                "symfony/translation": "~4.2",
+                "symfony/translation-contracts": "^1.1",
+                "symfony/var-dumper": "^4.1.1",
+                "twig/twig": "^1.34|^2.4"
             },
             "suggest": {
                 "symfony/browser-kit": "",
-                "symfony/class-loader": "",
                 "symfony/config": "",
                 "symfony/console": "",
                 "symfony/dependency-injection": "",
-                "symfony/finder": "",
                 "symfony/var-dumper": ""
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.3-dev"
+                    "dev-master": "4.3-dev"
                 }
             },
             "autoload": {
             ],
             "description": "Symfony HttpKernel Component",
             "homepage": "https://symfony.com",
-            "time": "2017-08-01T10:25:59+00:00"
+            "time": "2019-08-26T16:47:42+00:00"
         },
         {
-            "name": "symfony/polyfill-ctype",
-            "version": "v1.10.0",
+            "name": "symfony/mime",
+            "version": "v4.3.4",
             "source": {
                 "type": "git",
-                "url": "https://github.com/symfony/polyfill-ctype.git",
-                "reference": "e3d826245268269cd66f8326bd8bc066687b4a19"
+                "url": "https://github.com/symfony/mime.git",
+                "reference": "987a05df1c6ac259b34008b932551353f4f408df"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/e3d826245268269cd66f8326bd8bc066687b4a19",
-                "reference": "e3d826245268269cd66f8326bd8bc066687b4a19",
+                "url": "https://api.github.com/repos/symfony/mime/zipball/987a05df1c6ac259b34008b932551353f4f408df",
+                "reference": "987a05df1c6ac259b34008b932551353f4f408df",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.3.3"
+                "php": "^7.1.3",
+                "symfony/polyfill-intl-idn": "^1.10",
+                "symfony/polyfill-mbstring": "^1.0"
             },
-            "suggest": {
-                "ext-ctype": "For best performance"
+            "require-dev": {
+                "egulias/email-validator": "^2.1.10",
+                "symfony/dependency-injection": "~3.4|^4.1"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.9-dev"
+                    "dev-master": "4.3-dev"
                 }
             },
             "autoload": {
                 "psr-4": {
-                    "Symfony\\Polyfill\\Ctype\\": ""
+                    "Symfony\\Component\\Mime\\": ""
                 },
-                "files": [
-                    "bootstrap.php"
+                "exclude-from-classmap": [
+                    "/Tests/"
                 ]
             },
             "notification-url": "https://packagist.org/downloads/",
             ],
             "authors": [
                 {
-                    "name": "Symfony Community",
-                    "homepage": "https://symfony.com/contributors"
+                    "name": "Fabien Potencier",
+                    "email": "[email protected]"
                 },
                 {
-                    "name": "Gert de Pagter",
-                    "email": "[email protected]"
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
                 }
             ],
-            "description": "Symfony polyfill for ctype functions",
+            "description": "A library to manipulate MIME messages",
             "homepage": "https://symfony.com",
             "keywords": [
-                "compatibility",
-                "ctype",
-                "polyfill",
-                "portable"
+                "mime",
+                "mime-type"
             ],
-            "time": "2018-08-06T14:22:27+00:00"
+            "time": "2019-08-22T08:16:11+00:00"
         },
         {
-            "name": "symfony/polyfill-mbstring",
-            "version": "v1.10.0",
+            "name": "symfony/polyfill-ctype",
+            "version": "v1.12.0",
             "source": {
                 "type": "git",
-                "url": "https://github.com/symfony/polyfill-mbstring.git",
-                "reference": "c79c051f5b3a46be09205c73b80b346e4153e494"
+                "url": "https://github.com/symfony/polyfill-ctype.git",
+                "reference": "550ebaac289296ce228a706d0867afc34687e3f4"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/c79c051f5b3a46be09205c73b80b346e4153e494",
-                "reference": "c79c051f5b3a46be09205c73b80b346e4153e494",
+                "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/550ebaac289296ce228a706d0867afc34687e3f4",
+                "reference": "550ebaac289296ce228a706d0867afc34687e3f4",
                 "shasum": ""
             },
             "require": {
                 "php": ">=5.3.3"
             },
             "suggest": {
-                "ext-mbstring": "For best performance"
+                "ext-ctype": "For best performance"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.9-dev"
+                    "dev-master": "1.12-dev"
                 }
             },
             "autoload": {
                 "psr-4": {
-                    "Symfony\\Polyfill\\Mbstring\\": ""
+                    "Symfony\\Polyfill\\Ctype\\": ""
                 },
                 "files": [
                     "bootstrap.php"
             ],
             "authors": [
                 {
-                    "name": "Nicolas Grekas",
-                    "email": "p@tchwork.com"
+                    "name": "Gert de Pagter",
+                    "email": "BackEndTea@gmail.com"
                 },
                 {
                     "name": "Symfony Community",
                     "homepage": "https://symfony.com/contributors"
                 }
             ],
-            "description": "Symfony polyfill for the Mbstring extension",
+            "description": "Symfony polyfill for ctype functions",
             "homepage": "https://symfony.com",
             "keywords": [
                 "compatibility",
-                "mbstring",
+                "ctype",
                 "polyfill",
-                "portable",
-                "shim"
+                "portable"
             ],
-            "time": "2018-09-21T13:07:52+00:00"
+            "time": "2019-08-06T08:03:45+00:00"
         },
         {
-            "name": "symfony/process",
-            "version": "v3.3.6",
+            "name": "symfony/polyfill-iconv",
+            "version": "v1.12.0",
             "source": {
                 "type": "git",
-                "url": "https://github.com/symfony/process.git",
-                "reference": "07432804942b9f6dd7b7377faf9920af5f95d70a"
+                "url": "https://github.com/symfony/polyfill-iconv.git",
+                "reference": "685968b11e61a347c18bf25db32effa478be610f"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/process/zipball/07432804942b9f6dd7b7377faf9920af5f95d70a",
-                "reference": "07432804942b9f6dd7b7377faf9920af5f95d70a",
+                "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/685968b11e61a347c18bf25db32effa478be610f",
+                "reference": "685968b11e61a347c18bf25db32effa478be610f",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.5.9"
+                "php": ">=5.3.3"
+            },
+            "suggest": {
+                "ext-iconv": "For best performance"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.3-dev"
+                    "dev-master": "1.12-dev"
                 }
             },
             "autoload": {
                 "psr-4": {
-                    "Symfony\\Component\\Process\\": ""
+                    "Symfony\\Polyfill\\Iconv\\": ""
                 },
-                "exclude-from-classmap": [
-                    "/Tests/"
+                "files": [
+                    "bootstrap.php"
                 ]
             },
             "notification-url": "https://packagist.org/downloads/",
             ],
             "authors": [
                 {
-                    "name": "Fabien Potencier",
-                    "email": "fabien@symfony.com"
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
                 },
                 {
                     "name": "Symfony Community",
                     "homepage": "https://symfony.com/contributors"
                 }
             ],
-            "description": "Symfony Process Component",
+            "description": "Symfony polyfill for the Iconv extension",
             "homepage": "https://symfony.com",
-            "time": "2017-07-13T13:05:09+00:00"
+            "keywords": [
+                "compatibility",
+                "iconv",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "time": "2019-08-06T08:03:45+00:00"
+        },
+        {
+            "name": "symfony/polyfill-intl-idn",
+            "version": "v1.12.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-intl-idn.git",
+                "reference": "6af626ae6fa37d396dc90a399c0ff08e5cfc45b2"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/6af626ae6fa37d396dc90a399c0ff08e5cfc45b2",
+                "reference": "6af626ae6fa37d396dc90a399c0ff08e5cfc45b2",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.3",
+                "symfony/polyfill-mbstring": "^1.3",
+                "symfony/polyfill-php72": "^1.9"
+            },
+            "suggest": {
+                "ext-intl": "For best performance"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.12-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Polyfill\\Intl\\Idn\\": ""
+                },
+                "files": [
+                    "bootstrap.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Laurent Bassin",
+                    "email": "[email protected]"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "idn",
+                "intl",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "time": "2019-08-06T08:03:45+00:00"
+        },
+        {
+            "name": "symfony/polyfill-mbstring",
+            "version": "v1.12.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-mbstring.git",
+                "reference": "b42a2f66e8f1b15ccf25652c3424265923eb4f17"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/b42a2f66e8f1b15ccf25652c3424265923eb4f17",
+                "reference": "b42a2f66e8f1b15ccf25652c3424265923eb4f17",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.3"
+            },
+            "suggest": {
+                "ext-mbstring": "For best performance"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.12-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Polyfill\\Mbstring\\": ""
+                },
+                "files": [
+                    "bootstrap.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "[email protected]"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill for the Mbstring extension",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "mbstring",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "time": "2019-08-06T08:03:45+00:00"
+        },
+        {
+            "name": "symfony/polyfill-php72",
+            "version": "v1.12.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-php72.git",
+                "reference": "04ce3335667451138df4307d6a9b61565560199e"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/04ce3335667451138df4307d6a9b61565560199e",
+                "reference": "04ce3335667451138df4307d6a9b61565560199e",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.3"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.12-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Polyfill\\Php72\\": ""
+                },
+                "files": [
+                    "bootstrap.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "[email protected]"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "time": "2019-08-06T08:03:45+00:00"
+        },
+        {
+            "name": "symfony/polyfill-php73",
+            "version": "v1.12.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-php73.git",
+                "reference": "2ceb49eaccb9352bff54d22570276bb75ba4a188"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/2ceb49eaccb9352bff54d22570276bb75ba4a188",
+                "reference": "2ceb49eaccb9352bff54d22570276bb75ba4a188",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.3"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.12-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Polyfill\\Php73\\": ""
+                },
+                "files": [
+                    "bootstrap.php"
+                ],
+                "classmap": [
+                    "Resources/stubs"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "[email protected]"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "time": "2019-08-06T08:03:45+00:00"
+        },
+        {
+            "name": "symfony/process",
+            "version": "v4.3.4",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/process.git",
+                "reference": "e89969c00d762349f078db1128506f7f3dcc0d4a"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/process/zipball/e89969c00d762349f078db1128506f7f3dcc0d4a",
+                "reference": "e89969c00d762349f078db1128506f7f3dcc0d4a",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.1.3"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "4.3-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Component\\Process\\": ""
+                },
+                "exclude-from-classmap": [
+                    "/Tests/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Fabien Potencier",
+                    "email": "[email protected]"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony Process Component",
+            "homepage": "https://symfony.com",
+            "time": "2019-08-26T08:26:39+00:00"
         },
         {
             "name": "symfony/routing",
-            "version": "v3.3.6",
+            "version": "v4.3.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/routing.git",
-                "reference": "4aee1a917fd4859ff8b51b9fd1dfb790a5ecfa26"
+                "reference": "ff1049f6232dc5b6023b1ff1c6de56f82bcd264f"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/routing/zipball/4aee1a917fd4859ff8b51b9fd1dfb790a5ecfa26",
-                "reference": "4aee1a917fd4859ff8b51b9fd1dfb790a5ecfa26",
+                "url": "https://api.github.com/repos/symfony/routing/zipball/ff1049f6232dc5b6023b1ff1c6de56f82bcd264f",
+                "reference": "ff1049f6232dc5b6023b1ff1c6de56f82bcd264f",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.5.9"
+                "php": "^7.1.3"
             },
             "conflict": {
-                "symfony/config": "<2.8",
-                "symfony/dependency-injection": "<3.3",
-                "symfony/yaml": "<3.3"
+                "symfony/config": "<4.2",
+                "symfony/dependency-injection": "<3.4",
+                "symfony/yaml": "<3.4"
             },
             "require-dev": {
-                "doctrine/annotations": "~1.0",
-                "doctrine/common": "~2.2",
+                "doctrine/annotations": "~1.2",
                 "psr/log": "~1.0",
-                "symfony/config": "~2.8|~3.0",
-                "symfony/dependency-injection": "~3.3",
-                "symfony/expression-language": "~2.8|~3.0",
-                "symfony/http-foundation": "~2.8|~3.0",
-                "symfony/yaml": "~3.3"
+                "symfony/config": "~4.2",
+                "symfony/dependency-injection": "~3.4|~4.0",
+                "symfony/expression-language": "~3.4|~4.0",
+                "symfony/http-foundation": "~3.4|~4.0",
+                "symfony/yaml": "~3.4|~4.0"
             },
             "suggest": {
                 "doctrine/annotations": "For using the annotation loader",
                 "symfony/config": "For using the all-in-one router or any loader",
-                "symfony/dependency-injection": "For loading routes from a service",
                 "symfony/expression-language": "For using expression matching",
                 "symfony/http-foundation": "For using a Symfony Request object",
                 "symfony/yaml": "For using the YAML loader"
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.3-dev"
+                    "dev-master": "4.3-dev"
                 }
             },
             "autoload": {
                 "uri",
                 "url"
             ],
-            "time": "2017-07-21T17:43:13+00:00"
+            "time": "2019-08-26T08:26:39+00:00"
+        },
+        {
+            "name": "symfony/service-contracts",
+            "version": "v1.1.6",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/service-contracts.git",
+                "reference": "ea7263d6b6d5f798b56a45a5b8d686725f2719a3"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/service-contracts/zipball/ea7263d6b6d5f798b56a45a5b8d686725f2719a3",
+                "reference": "ea7263d6b6d5f798b56a45a5b8d686725f2719a3",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.1.3",
+                "psr/container": "^1.0"
+            },
+            "suggest": {
+                "symfony/service-implementation": ""
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.1-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Contracts\\Service\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "[email protected]"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Generic abstractions related to writing services",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "abstractions",
+                "contracts",
+                "decoupling",
+                "interfaces",
+                "interoperability",
+                "standards"
+            ],
+            "time": "2019-08-20T14:44:19+00:00"
         },
         {
             "name": "symfony/translation",
-            "version": "v3.3.6",
+            "version": "v4.3.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/translation.git",
-                "reference": "35dd5fb003c90e8bd4d8cabdf94bf9c96d06fdc3"
+                "reference": "28498169dd334095fa981827992f3a24d50fed0f"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/translation/zipball/35dd5fb003c90e8bd4d8cabdf94bf9c96d06fdc3",
-                "reference": "35dd5fb003c90e8bd4d8cabdf94bf9c96d06fdc3",
+                "url": "https://api.github.com/repos/symfony/translation/zipball/28498169dd334095fa981827992f3a24d50fed0f",
+                "reference": "28498169dd334095fa981827992f3a24d50fed0f",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.5.9",
-                "symfony/polyfill-mbstring": "~1.0"
+                "php": "^7.1.3",
+                "symfony/polyfill-mbstring": "~1.0",
+                "symfony/translation-contracts": "^1.1.6"
             },
             "conflict": {
-                "symfony/config": "<2.8",
-                "symfony/yaml": "<3.3"
+                "symfony/config": "<3.4",
+                "symfony/dependency-injection": "<3.4",
+                "symfony/yaml": "<3.4"
+            },
+            "provide": {
+                "symfony/translation-implementation": "1.0"
             },
             "require-dev": {
                 "psr/log": "~1.0",
-                "symfony/config": "~2.8|~3.0",
-                "symfony/intl": "^2.8.18|^3.2.5",
-                "symfony/yaml": "~3.3"
+                "symfony/config": "~3.4|~4.0",
+                "symfony/console": "~3.4|~4.0",
+                "symfony/dependency-injection": "~3.4|~4.0",
+                "symfony/finder": "~2.8|~3.0|~4.0",
+                "symfony/http-kernel": "~3.4|~4.0",
+                "symfony/intl": "~3.4|~4.0",
+                "symfony/service-contracts": "^1.1.2",
+                "symfony/var-dumper": "~3.4|~4.0",
+                "symfony/yaml": "~3.4|~4.0"
             },
             "suggest": {
-                "psr/log": "To use logging capability in translator",
+                "psr/log-implementation": "To use logging capability in translator",
                 "symfony/config": "",
                 "symfony/yaml": ""
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.3-dev"
+                    "dev-master": "4.3-dev"
                 }
             },
             "autoload": {
             ],
             "description": "Symfony Translation Component",
             "homepage": "https://symfony.com",
-            "time": "2017-06-24T16:45:30+00:00"
+            "time": "2019-08-26T08:55:16+00:00"
+        },
+        {
+            "name": "symfony/translation-contracts",
+            "version": "v1.1.6",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/translation-contracts.git",
+                "reference": "325b17c24f3ee23cbecfa63ba809c6d89b5fa04a"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/325b17c24f3ee23cbecfa63ba809c6d89b5fa04a",
+                "reference": "325b17c24f3ee23cbecfa63ba809c6d89b5fa04a",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.1.3"
+            },
+            "suggest": {
+                "symfony/translation-implementation": ""
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.1-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Contracts\\Translation\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "[email protected]"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Generic abstractions related to translation",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "abstractions",
+                "contracts",
+                "decoupling",
+                "interfaces",
+                "interoperability",
+                "standards"
+            ],
+            "time": "2019-08-02T12:15:04+00:00"
         },
         {
             "name": "symfony/var-dumper",
-            "version": "v3.3.6",
+            "version": "v4.3.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/var-dumper.git",
-                "reference": "b2623bccb969ad595c2090f9be498b74670d0663"
+                "reference": "641043e0f3e615990a0f29479f9c117e8a6698c6"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/var-dumper/zipball/b2623bccb969ad595c2090f9be498b74670d0663",
-                "reference": "b2623bccb969ad595c2090f9be498b74670d0663",
+                "url": "https://api.github.com/repos/symfony/var-dumper/zipball/641043e0f3e615990a0f29479f9c117e8a6698c6",
+                "reference": "641043e0f3e615990a0f29479f9c117e8a6698c6",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.5.9",
-                "symfony/polyfill-mbstring": "~1.0"
+                "php": "^7.1.3",
+                "symfony/polyfill-mbstring": "~1.0",
+                "symfony/polyfill-php72": "~1.5"
             },
             "conflict": {
-                "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0"
+                "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0",
+                "symfony/console": "<3.4"
             },
             "require-dev": {
                 "ext-iconv": "*",
+                "symfony/console": "~3.4|~4.0",
+                "symfony/process": "~3.4|~4.0",
                 "twig/twig": "~1.34|~2.4"
             },
             "suggest": {
                 "ext-iconv": "To convert non-UTF-8 strings to UTF-8 (or symfony/polyfill-iconv in case ext-iconv cannot be used).",
-                "ext-symfony_debug": ""
+                "ext-intl": "To show region name in time zone dump",
+                "symfony/console": "To use the ServerDumpCommand and/or the bin/var-dump-server script"
             },
+            "bin": [
+                "Resources/bin/var-dump-server"
+            ],
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.3-dev"
+                    "dev-master": "4.3-dev"
                 }
             },
             "autoload": {
                 "debug",
                 "dump"
             ],
-            "time": "2017-07-28T06:06:09+00:00"
+            "time": "2019-08-26T08:26:39+00:00"
         },
         {
             "name": "tijsverkoyen/css-to-inline-styles",
         },
         {
             "name": "vlucas/phpdotenv",
-            "version": "v2.5.2",
+            "version": "v3.6.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/vlucas/phpdotenv.git",
-                "reference": "cfd5dc225767ca154853752abc93aeec040fcf36"
+                "reference": "1bdf24f065975594f6a117f0f1f6cabf1333b156"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/cfd5dc225767ca154853752abc93aeec040fcf36",
-                "reference": "cfd5dc225767ca154853752abc93aeec040fcf36",
+                "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/1bdf24f065975594f6a117f0f1f6cabf1333b156",
+                "reference": "1bdf24f065975594f6a117f0f1f6cabf1333b156",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.3.9"
+                "php": "^5.4 || ^7.0",
+                "phpoption/phpoption": "^1.5",
+                "symfony/polyfill-ctype": "^1.9"
             },
             "require-dev": {
-                "phpunit/phpunit": "^4.8.35 || ^5.0"
+                "phpunit/phpunit": "^4.8.35 || ^5.0 || ^6.0 || ^7.0"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "2.5-dev"
+                    "dev-master": "3.6-dev"
                 }
             },
             "autoload": {
                 "BSD-3-Clause"
             ],
             "authors": [
+                {
+                    "name": "Graham Campbell",
+                    "email": "[email protected]",
+                    "homepage": "https://gjcampbell.co.uk/"
+                },
                 {
                     "name": "Vance Lucas",
                     "email": "[email protected]",
-                    "homepage": "http://www.vancelucas.com"
+                    "homepage": "https://vancelucas.com/"
                 }
             ],
             "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.",
                 "env",
                 "environment"
             ],
-            "time": "2018-10-30T17:29:25+00:00"
+            "time": "2019-09-10T21:37:39+00:00"
         }
     ],
     "packages-dev": [
         {
             "name": "barryvdh/laravel-debugbar",
-            "version": "v3.2.1",
+            "version": "v3.2.8",
             "source": {
                 "type": "git",
                 "url": "https://github.com/barryvdh/laravel-debugbar.git",
-                "reference": "9d5caf43c5f3a3aea2178942f281054805872e7c"
+                "reference": "18208d64897ab732f6c04a19b319fe8f1d57a9c0"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/9d5caf43c5f3a3aea2178942f281054805872e7c",
-                "reference": "9d5caf43c5f3a3aea2178942f281054805872e7c",
+                "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/18208d64897ab732f6c04a19b319fe8f1d57a9c0",
+                "reference": "18208d64897ab732f6c04a19b319fe8f1d57a9c0",
                 "shasum": ""
             },
             "require": {
-                "illuminate/routing": "5.5.x|5.6.x|5.7.x",
-                "illuminate/session": "5.5.x|5.6.x|5.7.x",
-                "illuminate/support": "5.5.x|5.6.x|5.7.x",
+                "illuminate/routing": "^5.5|^6",
+                "illuminate/session": "^5.5|^6",
+                "illuminate/support": "^5.5|^6",
                 "maximebf/debugbar": "~1.15.0",
                 "php": ">=7.0",
                 "symfony/debug": "^3|^4",
                 "profiler",
                 "webprofiler"
             ],
-            "time": "2018-11-09T08:37:55+00:00"
+            "time": "2019-08-29T07:01:03+00:00"
         },
         {
             "name": "barryvdh/laravel-ide-helper",
-            "version": "v2.5.3",
+            "version": "v2.6.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/barryvdh/laravel-ide-helper.git",
-                "reference": "3d7f1240896a075aa23b13f82dfcbe165dadeef2"
+                "reference": "8740a9a158d3dd5cfc706a9d4cc1bf7a518f99f3"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/3d7f1240896a075aa23b13f82dfcbe165dadeef2",
-                "reference": "3d7f1240896a075aa23b13f82dfcbe165dadeef2",
+                "url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/8740a9a158d3dd5cfc706a9d4cc1bf7a518f99f3",
+                "reference": "8740a9a158d3dd5cfc706a9d4cc1bf7a518f99f3",
                 "shasum": ""
             },
             "require": {
                 "barryvdh/reflection-docblock": "^2.0.6",
                 "composer/composer": "^1.6",
-                "illuminate/console": "^5.5,<5.8",
-                "illuminate/filesystem": "^5.5,<5.8",
-                "illuminate/support": "^5.5,<5.8",
+                "doctrine/dbal": "~2.3",
+                "illuminate/console": "^5.5|^6",
+                "illuminate/filesystem": "^5.5|^6",
+                "illuminate/support": "^5.5|^6",
                 "php": ">=7"
             },
             "require-dev": {
-                "doctrine/dbal": "~2.3",
-                "illuminate/config": "^5.1,<5.8",
-                "illuminate/view": "^5.1,<5.8",
+                "illuminate/config": "^5.5|^6",
+                "illuminate/view": "^5.5|^6",
                 "phpro/grumphp": "^0.14",
                 "phpunit/phpunit": "4.*",
                 "scrutinizer/ocular": "~1.1",
                 "squizlabs/php_codesniffer": "^3"
             },
-            "suggest": {
-                "doctrine/dbal": "Load information from the database about models for phpdocs (~2.3)"
-            },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "2.5-dev"
+                    "dev-master": "2.6-dev"
                 },
                 "laravel": {
                     "providers": [
                 "phpstorm",
                 "sublime"
             ],
-            "time": "2018-12-19T12:12:05+00:00"
+            "time": "2019-09-08T09:56:38+00:00"
         },
         {
             "name": "barryvdh/reflection-docblock",
         },
         {
             "name": "composer/ca-bundle",
-            "version": "1.1.3",
+            "version": "1.2.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/composer/ca-bundle.git",
-                "reference": "8afa52cd417f4ec417b4bfe86b68106538a87660"
+                "reference": "10bb96592168a0f8e8f6dcde3532d9fa50b0b527"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/composer/ca-bundle/zipball/8afa52cd417f4ec417b4bfe86b68106538a87660",
-                "reference": "8afa52cd417f4ec417b4bfe86b68106538a87660",
+                "url": "https://api.github.com/repos/composer/ca-bundle/zipball/10bb96592168a0f8e8f6dcde3532d9fa50b0b527",
+                "reference": "10bb96592168a0f8e8f6dcde3532d9fa50b0b527",
                 "shasum": ""
             },
             "require": {
                 "ext-openssl": "*",
                 "ext-pcre": "*",
-                "php": "^5.3.2 || ^7.0"
+                "php": "^5.3.2 || ^7.0 || ^8.0"
             },
             "require-dev": {
-                "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5",
+                "phpunit/phpunit": "^4.8.35 || ^5.7 || 6.5 - 8",
                 "psr/log": "^1.0",
                 "symfony/process": "^2.5 || ^3.0 || ^4.0"
             },
                 "ssl",
                 "tls"
             ],
-            "time": "2018-10-18T06:09:13+00:00"
+            "time": "2019-08-30T08:44:50+00:00"
         },
         {
             "name": "composer/composer",
-            "version": "1.8.0",
+            "version": "1.9.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/composer/composer.git",
-                "reference": "d8aef3af866b28786ce9b8647e52c42496436669"
+                "reference": "314aa57fdcfc942065996f59fb73a8b3f74f3fa5"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/composer/composer/zipball/d8aef3af866b28786ce9b8647e52c42496436669",
-                "reference": "d8aef3af866b28786ce9b8647e52c42496436669",
+                "url": "https://api.github.com/repos/composer/composer/zipball/314aa57fdcfc942065996f59fb73a8b3f74f3fa5",
+                "reference": "314aa57fdcfc942065996f59fb73a8b3f74f3fa5",
                 "shasum": ""
             },
             "require": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.8-dev"
+                    "dev-master": "1.9-dev"
                 }
             },
             "autoload": {
                     "homepage": "http://seld.be"
                 }
             ],
-            "description": "Composer helps you declare, manage and install dependencies of PHP projects, ensuring you have the right stack everywhere.",
+            "description": "Composer helps you declare, manage and install dependencies of PHP projects. It ensures you have the right stack everywhere.",
             "homepage": "https://getcomposer.org/",
             "keywords": [
                 "autoload",
                 "dependency",
                 "package"
             ],
-            "time": "2018-12-03T09:31:16+00:00"
+            "time": "2019-08-02T18:55:33+00:00"
         },
         {
             "name": "composer/semver",
-            "version": "1.4.2",
+            "version": "1.5.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/composer/semver.git",
-                "reference": "c7cb9a2095a074d131b65a8a0cd294479d785573"
+                "reference": "46d9139568ccb8d9e7cdd4539cab7347568a5e2e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/composer/semver/zipball/c7cb9a2095a074d131b65a8a0cd294479d785573",
-                "reference": "c7cb9a2095a074d131b65a8a0cd294479d785573",
+                "url": "https://api.github.com/repos/composer/semver/zipball/46d9139568ccb8d9e7cdd4539cab7347568a5e2e",
+                "reference": "46d9139568ccb8d9e7cdd4539cab7347568a5e2e",
                 "shasum": ""
             },
             "require": {
                     "homepage": "http://robbast.nl"
                 }
             ],
-            "description": "Semver library that offers utilities, version constraint parsing and validation.",
+            "description": "Semver library that offers utilities, version constraint parsing and validation.",
+            "keywords": [
+                "semantic",
+                "semver",
+                "validation",
+                "versioning"
+            ],
+            "time": "2019-03-19T17:25:45+00:00"
+        },
+        {
+            "name": "composer/spdx-licenses",
+            "version": "1.5.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/composer/spdx-licenses.git",
+                "reference": "7ac1e6aec371357df067f8a688c3d6974df68fa5"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/composer/spdx-licenses/zipball/7ac1e6aec371357df067f8a688c3d6974df68fa5",
+                "reference": "7ac1e6aec371357df067f8a688c3d6974df68fa5",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^5.3.2 || ^7.0 || ^8.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^4.8.35 || ^5.7 || 6.5 - 7"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Composer\\Spdx\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nils Adermann",
+                    "email": "[email protected]",
+                    "homepage": "http://www.naderman.de"
+                },
+                {
+                    "name": "Jordi Boggiano",
+                    "email": "[email protected]",
+                    "homepage": "http://seld.be"
+                },
+                {
+                    "name": "Rob Bast",
+                    "email": "[email protected]",
+                    "homepage": "http://robbast.nl"
+                }
+            ],
+            "description": "SPDX licenses list and validation library.",
+            "keywords": [
+                "license",
+                "spdx",
+                "validator"
+            ],
+            "time": "2019-07-29T10:31:59+00:00"
+        },
+        {
+            "name": "composer/xdebug-handler",
+            "version": "1.3.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/composer/xdebug-handler.git",
+                "reference": "46867cbf8ca9fb8d60c506895449eb799db1184f"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/46867cbf8ca9fb8d60c506895449eb799db1184f",
+                "reference": "46867cbf8ca9fb8d60c506895449eb799db1184f",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^5.3.2 || ^7.0",
+                "psr/log": "^1.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Composer\\XdebugHandler\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "John Stevenson",
+                    "email": "[email protected]"
+                }
+            ],
+            "description": "Restarts a process without xdebug.",
+            "keywords": [
+                "Xdebug",
+                "performance"
+            ],
+            "time": "2019-05-27T17:52:04+00:00"
+        },
+        {
+            "name": "doctrine/instantiator",
+            "version": "1.2.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/doctrine/instantiator.git",
+                "reference": "a2c590166b2133a4633738648b6b064edae0814a"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/doctrine/instantiator/zipball/a2c590166b2133a4633738648b6b064edae0814a",
+                "reference": "a2c590166b2133a4633738648b6b064edae0814a",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.1"
+            },
+            "require-dev": {
+                "doctrine/coding-standard": "^6.0",
+                "ext-pdo": "*",
+                "ext-phar": "*",
+                "phpbench/phpbench": "^0.13",
+                "phpstan/phpstan-phpunit": "^0.11",
+                "phpstan/phpstan-shim": "^0.11",
+                "phpunit/phpunit": "^7.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.2.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Marco Pivetta",
+                    "email": "[email protected]",
+                    "homepage": "http://ocramius.github.com/"
+                }
+            ],
+            "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors",
+            "homepage": "https://www.doctrine-project.org/projects/instantiator.html",
             "keywords": [
-                "semantic",
-                "semver",
-                "validation",
-                "versioning"
+                "constructor",
+                "instantiate"
             ],
-            "time": "2016-08-30T16:08:34+00:00"
+            "time": "2019-03-17T17:37:11+00:00"
         },
         {
-            "name": "composer/spdx-licenses",
-            "version": "1.5.0",
+            "name": "facade/flare-client-php",
+            "version": "1.0.4",
             "source": {
                 "type": "git",
-                "url": "https://github.com/composer/spdx-licenses.git",
-                "reference": "7a9556b22bd9d4df7cad89876b00af58ef20d3a2"
+                "url": "https://github.com/facade/flare-client-php.git",
+                "reference": "7128b251b48f24ef64e5cddd7f8d40cc3a06fd3e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/composer/spdx-licenses/zipball/7a9556b22bd9d4df7cad89876b00af58ef20d3a2",
-                "reference": "7a9556b22bd9d4df7cad89876b00af58ef20d3a2",
+                "url": "https://api.github.com/repos/facade/flare-client-php/zipball/7128b251b48f24ef64e5cddd7f8d40cc3a06fd3e",
+                "reference": "7128b251b48f24ef64e5cddd7f8d40cc3a06fd3e",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.3.2 || ^7.0"
+                "facade/ignition-contracts": "~1.0",
+                "illuminate/pipeline": "~5.5|~5.6|~5.7|~5.8|^6.0",
+                "php": "^7.1",
+                "symfony/http-foundation": "~3.3|~4.1",
+                "symfony/var-dumper": "^3.4|^4.0"
             },
             "require-dev": {
-                "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5",
-                "phpunit/phpunit-mock-objects": "2.3.0 || ^3.0"
+                "larapack/dd": "^1.1",
+                "phpunit/phpunit": "^7.0",
+                "spatie/phpunit-snapshot-assertions": "^2.0"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.x-dev"
+                    "dev-master": "1.0-dev"
                 }
             },
             "autoload": {
                 "psr-4": {
-                    "Composer\\Spdx\\": "src"
-                }
+                    "Facade\\FlareClient\\": "src"
+                },
+                "files": [
+                    "src/helpers.php"
+                ]
             },
             "notification-url": "https://packagist.org/downloads/",
             "license": [
                 "MIT"
             ],
-            "authors": [
-                {
-                    "name": "Nils Adermann",
-                    "email": "[email protected]",
-                    "homepage": "http://www.naderman.de"
-                },
-                {
-                    "name": "Jordi Boggiano",
-                    "email": "[email protected]",
-                    "homepage": "http://seld.be"
-                },
-                {
-                    "name": "Rob Bast",
-                    "email": "[email protected]",
-                    "homepage": "http://robbast.nl"
-                }
-            ],
-            "description": "SPDX licenses list and validation library.",
+            "description": "Send PHP errors to Flare",
+            "homepage": "https://github.com/facade/flare-client-php",
             "keywords": [
-                "license",
-                "spdx",
-                "validator"
+                "exception",
+                "facade",
+                "flare",
+                "reporting"
             ],
-            "time": "2018-11-01T09:45:54+00:00"
+            "time": "2019-09-11T14:19:56+00:00"
         },
         {
-            "name": "composer/xdebug-handler",
-            "version": "1.3.1",
+            "name": "facade/ignition",
+            "version": "1.6.5",
             "source": {
                 "type": "git",
-                "url": "https://github.com/composer/xdebug-handler.git",
-                "reference": "dc523135366eb68f22268d069ea7749486458562"
+                "url": "https://github.com/facade/ignition.git",
+                "reference": "97244f6d511332f3574acab8242c09ddcfda892b"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/dc523135366eb68f22268d069ea7749486458562",
-                "reference": "dc523135366eb68f22268d069ea7749486458562",
+                "url": "https://api.github.com/repos/facade/ignition/zipball/97244f6d511332f3574acab8242c09ddcfda892b",
+                "reference": "97244f6d511332f3574acab8242c09ddcfda892b",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.3.2 || ^7.0",
-                "psr/log": "^1.0"
+                "ext-json": "*",
+                "ext-mbstring": "*",
+                "facade/flare-client-php": "^1.0.4",
+                "facade/ignition-contracts": "^1.0",
+                "filp/whoops": "^2.4",
+                "illuminate/support": "~5.5.0 || ~5.6.0 || ~5.7.0 || ~5.8.0 || ^6.0",
+                "monolog/monolog": "^1.12 || ^2.0",
+                "php": "^7.1",
+                "scrivo/highlight.php": "^9.15",
+                "symfony/console": "^3.4 || ^4.0",
+                "symfony/var-dumper": "^3.4 || ^4.0"
             },
             "require-dev": {
-                "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5"
+                "friendsofphp/php-cs-fixer": "^2.14",
+                "mockery/mockery": "^1.2",
+                "orchestra/testbench": "^3.5 || ^3.6 || ^3.7 || ^3.8 || ^4.0"
+            },
+            "suggest": {
+                "laravel/telescope": "^2.0"
             },
             "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0-dev"
+                },
+                "laravel": {
+                    "providers": [
+                        "Facade\\Ignition\\IgnitionServiceProvider"
+                    ],
+                    "aliases": {
+                        "Flare": "Facade\\Ignition\\Facades\\Flare"
+                    }
+                }
+            },
             "autoload": {
                 "psr-4": {
-                    "Composer\\XdebugHandler\\": "src"
+                    "Facade\\Ignition\\": "src"
                 }
             },
             "notification-url": "https://packagist.org/downloads/",
             "license": [
                 "MIT"
             ],
-            "authors": [
-                {
-                    "name": "John Stevenson",
-                    "email": "[email protected]"
-                }
-            ],
-            "description": "Restarts a process without xdebug.",
+            "description": "A beautiful error page for Laravel applications.",
+            "homepage": "https://github.com/facade/ignition",
             "keywords": [
-                "Xdebug",
-                "performance"
+                "error",
+                "flare",
+                "laravel",
+                "page"
             ],
-            "time": "2018-11-29T10:59:02+00:00"
+            "time": "2019-09-13T13:38:04+00:00"
         },
         {
-            "name": "doctrine/instantiator",
-            "version": "1.0.5",
+            "name": "facade/ignition-contracts",
+            "version": "1.0.0",
             "source": {
                 "type": "git",
-                "url": "https://github.com/doctrine/instantiator.git",
-                "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d"
+                "url": "https://github.com/facade/ignition-contracts.git",
+                "reference": "f445db0fb86f48e205787b2592840dd9c80ded28"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d",
-                "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d",
+                "url": "https://api.github.com/repos/facade/ignition-contracts/zipball/f445db0fb86f48e205787b2592840dd9c80ded28",
+                "reference": "f445db0fb86f48e205787b2592840dd9c80ded28",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.3,<8.0-DEV"
-            },
-            "require-dev": {
-                "athletic/athletic": "~0.1.8",
-                "ext-pdo": "*",
-                "ext-phar": "*",
-                "phpunit/phpunit": "~4.0",
-                "squizlabs/php_codesniffer": "~2.0"
+                "php": "^7.1"
             },
             "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "1.0.x-dev"
-                }
-            },
             "autoload": {
                 "psr-4": {
-                    "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/"
+                    "Facade\\IgnitionContracts\\": "src"
                 }
             },
             "notification-url": "https://packagist.org/downloads/",
             ],
             "authors": [
                 {
-                    "name": "Marco Pivetta",
-                    "email": "[email protected]",
-                    "homepage": "http://ocramius.github.com/"
+                    "name": "Freek Van der Herten",
+                    "email": "[email protected]",
+                    "homepage": "https://flareapp.io",
+                    "role": "Developer"
                 }
             ],
-            "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors",
-            "homepage": "https://github.com/doctrine/instantiator",
+            "description": "Solution contracts for Ignition",
+            "homepage": "https://github.com/facade/ignition-contracts",
             "keywords": [
-                "constructor",
-                "instantiate"
+                "contracts",
+                "flare",
+                "ignition"
             ],
-            "time": "2015-06-14T21:17:01+00:00"
+            "time": "2019-08-30T14:06:08+00:00"
         },
         {
             "name": "filp/whoops",
-            "version": "2.3.1",
+            "version": "2.5.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/filp/whoops.git",
-                "reference": "bc0fd11bc455cc20ee4b5edabc63ebbf859324c7"
+                "reference": "cde50e6720a39fdacb240159d3eea6865d51fd96"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/filp/whoops/zipball/bc0fd11bc455cc20ee4b5edabc63ebbf859324c7",
-                "reference": "bc0fd11bc455cc20ee4b5edabc63ebbf859324c7",
+                "url": "https://api.github.com/repos/filp/whoops/zipball/cde50e6720a39fdacb240159d3eea6865d51fd96",
+                "reference": "cde50e6720a39fdacb240159d3eea6865d51fd96",
                 "shasum": ""
             },
             "require": {
             "authors": [
                 {
                     "name": "Filipe Dobreira",
-                    "homepage": "https://github.com/filp",
-                    "role": "Developer"
+                    "role": "Developer",
+                    "homepage": "https://github.com/filp"
                 }
             ],
             "description": "php error handling for cool kids",
                 "throwable",
                 "whoops"
             ],
-            "time": "2018-10-23T09:00:00+00:00"
+            "time": "2019-08-07T09:00:00+00:00"
         },
         {
             "name": "fzaninotto/faker",
             ],
             "time": "2016-01-20T08:20:44+00:00"
         },
+        {
+            "name": "jakub-onderka/php-console-color",
+            "version": "v0.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/JakubOnderka/PHP-Console-Color.git",
+                "reference": "d5deaecff52a0d61ccb613bb3804088da0307191"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/JakubOnderka/PHP-Console-Color/zipball/d5deaecff52a0d61ccb613bb3804088da0307191",
+                "reference": "d5deaecff52a0d61ccb613bb3804088da0307191",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.4.0"
+            },
+            "require-dev": {
+                "jakub-onderka/php-code-style": "1.0",
+                "jakub-onderka/php-parallel-lint": "1.0",
+                "jakub-onderka/php-var-dump-check": "0.*",
+                "phpunit/phpunit": "~4.3",
+                "squizlabs/php_codesniffer": "1.*"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "JakubOnderka\\PhpConsoleColor\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-2-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Jakub Onderka",
+                    "email": "[email protected]"
+                }
+            ],
+            "time": "2018-09-29T17:23:10+00:00"
+        },
+        {
+            "name": "jakub-onderka/php-console-highlighter",
+            "version": "v0.4",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/JakubOnderka/PHP-Console-Highlighter.git",
+                "reference": "9f7a229a69d52506914b4bc61bfdb199d90c5547"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/JakubOnderka/PHP-Console-Highlighter/zipball/9f7a229a69d52506914b4bc61bfdb199d90c5547",
+                "reference": "9f7a229a69d52506914b4bc61bfdb199d90c5547",
+                "shasum": ""
+            },
+            "require": {
+                "ext-tokenizer": "*",
+                "jakub-onderka/php-console-color": "~0.2",
+                "php": ">=5.4.0"
+            },
+            "require-dev": {
+                "jakub-onderka/php-code-style": "~1.0",
+                "jakub-onderka/php-parallel-lint": "~1.0",
+                "jakub-onderka/php-var-dump-check": "~0.1",
+                "phpunit/phpunit": "~4.0",
+                "squizlabs/php_codesniffer": "~1.5"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "JakubOnderka\\PhpConsoleHighlighter\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Jakub Onderka",
+                    "email": "[email protected]",
+                    "homepage": "http://www.acci.cz/"
+                }
+            ],
+            "description": "Highlight PHP code in terminal",
+            "time": "2018-09-29T18:48:56+00:00"
+        },
         {
             "name": "justinrainbow/json-schema",
             "version": "5.2.8",
         },
         {
             "name": "laravel/browser-kit-testing",
-            "version": "v2.0.1",
+            "version": "v5.1.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/laravel/browser-kit-testing.git",
-                "reference": "f0bb9f200ec35f9d876ded6eacfbc60868d311b9"
+                "reference": "cb0cf22cf38fe8796842adc8b9ad550ded2a1377"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/laravel/browser-kit-testing/zipball/f0bb9f200ec35f9d876ded6eacfbc60868d311b9",
-                "reference": "f0bb9f200ec35f9d876ded6eacfbc60868d311b9",
+                "url": "https://api.github.com/repos/laravel/browser-kit-testing/zipball/cb0cf22cf38fe8796842adc8b9ad550ded2a1377",
+                "reference": "cb0cf22cf38fe8796842adc8b9ad550ded2a1377",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.5.9",
-                "phpunit/phpunit": "~6.0",
-                "symfony/css-selector": "~3.1",
-                "symfony/dom-crawler": "~3.1"
+                "ext-dom": "*",
+                "ext-json": "*",
+                "illuminate/contracts": "~5.7.0|~5.8.0|^6.0",
+                "illuminate/database": "~5.7.0|~5.8.0|^6.0",
+                "illuminate/http": "~5.7.0|~5.8.0|^6.0",
+                "illuminate/support": "~5.7.0|~5.8.0|^6.0",
+                "mockery/mockery": "^1.0",
+                "php": ">=7.1.3",
+                "phpunit/phpunit": "^7.0|^8.0",
+                "symfony/console": "^4.2",
+                "symfony/css-selector": "^4.2",
+                "symfony/dom-crawler": "^4.2",
+                "symfony/http-foundation": "^4.2",
+                "symfony/http-kernel": "^4.2"
+            },
+            "require-dev": {
+                "laravel/framework": "~5.7.0|~5.8.0|^6.0"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "2.0-dev"
+                    "dev-master": "5.0-dev"
                 }
             },
             "autoload": {
                     "email": "[email protected]"
                 }
             ],
-            "description": "Provides backwards compatibility for BrowserKit testing in Laravel 5.4.",
+            "description": "Provides backwards compatibility for BrowserKit testing in the latest Laravel release.",
             "keywords": [
                 "laravel",
                 "testing"
             ],
-            "time": "2017-06-21T11:44:53+00:00"
+            "time": "2019-07-30T14:57:44+00:00"
         },
         {
             "name": "maximebf/debugbar",
         },
         {
             "name": "mockery/mockery",
-            "version": "1.2.0",
+            "version": "1.2.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/mockery/mockery.git",
-                "reference": "100633629bf76d57430b86b7098cd6beb996a35a"
+                "reference": "4eff936d83eb809bde2c57a3cea0ee9643769031"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/mockery/mockery/zipball/100633629bf76d57430b86b7098cd6beb996a35a",
-                "reference": "100633629bf76d57430b86b7098cd6beb996a35a",
+                "url": "https://api.github.com/repos/mockery/mockery/zipball/4eff936d83eb809bde2c57a3cea0ee9643769031",
+                "reference": "4eff936d83eb809bde2c57a3cea0ee9643769031",
                 "shasum": ""
             },
             "require": {
                 "php": ">=5.6.0"
             },
             "require-dev": {
-                "phpunit/phpunit": "~5.7.10|~6.5|~7.0"
+                "phpunit/phpunit": "~5.7.10|~6.5|~7.0|~8.0"
             },
             "type": "library",
             "extra": {
                 "test double",
                 "testing"
             ],
-            "time": "2018-10-02T21:52:37+00:00"
+            "time": "2019-08-07T15:01:07+00:00"
         },
         {
             "name": "myclabs/deep-copy",
-            "version": "1.7.0",
+            "version": "1.9.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/myclabs/DeepCopy.git",
-                "reference": "3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e"
+                "reference": "007c053ae6f31bba39dfa19a7726f56e9763bbea"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e",
-                "reference": "3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e",
+                "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/007c053ae6f31bba39dfa19a7726f56e9763bbea",
+                "reference": "007c053ae6f31bba39dfa19a7726f56e9763bbea",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.6 || ^7.0"
+                "php": "^7.1"
+            },
+            "replace": {
+                "myclabs/deep-copy": "self.version"
             },
             "require-dev": {
                 "doctrine/collections": "^1.0",
                 "doctrine/common": "^2.6",
-                "phpunit/phpunit": "^4.1"
+                "phpunit/phpunit": "^7.1"
             },
             "type": "library",
             "autoload": {
                 "object",
                 "object graph"
             ],
-            "time": "2017-10-19T19:58:43+00:00"
+            "time": "2019-08-09T12:45:53+00:00"
+        },
+        {
+            "name": "nunomaduro/collision",
+            "version": "v3.0.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/nunomaduro/collision.git",
+                "reference": "af42d339fe2742295a54f6fdd42aaa6f8c4aca68"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/nunomaduro/collision/zipball/af42d339fe2742295a54f6fdd42aaa6f8c4aca68",
+                "reference": "af42d339fe2742295a54f6fdd42aaa6f8c4aca68",
+                "shasum": ""
+            },
+            "require": {
+                "filp/whoops": "^2.1.4",
+                "jakub-onderka/php-console-highlighter": "0.3.*|0.4.*",
+                "php": "^7.1",
+                "symfony/console": "~2.8|~3.3|~4.0"
+            },
+            "require-dev": {
+                "laravel/framework": "5.8.*",
+                "nunomaduro/larastan": "^0.3.0",
+                "phpstan/phpstan": "^0.11",
+                "phpunit/phpunit": "~8.0"
+            },
+            "type": "library",
+            "extra": {
+                "laravel": {
+                    "providers": [
+                        "NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider"
+                    ]
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "NunoMaduro\\Collision\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nuno Maduro",
+                    "email": "[email protected]"
+                }
+            ],
+            "description": "Cli error handling for console/command-line PHP applications.",
+            "keywords": [
+                "artisan",
+                "cli",
+                "command-line",
+                "console",
+                "error",
+                "handling",
+                "laravel",
+                "laravel-zero",
+                "php",
+                "symfony"
+            ],
+            "time": "2019-03-07T21:35:13+00:00"
         },
         {
             "name": "phar-io/manifest",
-            "version": "1.0.1",
+            "version": "1.0.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phar-io/manifest.git",
-                "reference": "2df402786ab5368a0169091f61a7c1e0eb6852d0"
+                "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phar-io/manifest/zipball/2df402786ab5368a0169091f61a7c1e0eb6852d0",
-                "reference": "2df402786ab5368a0169091f61a7c1e0eb6852d0",
+                "url": "https://api.github.com/repos/phar-io/manifest/zipball/7761fcacf03b4d4f16e7ccb606d4879ca431fcf4",
+                "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4",
                 "shasum": ""
             },
             "require": {
                 "ext-dom": "*",
                 "ext-phar": "*",
-                "phar-io/version": "^1.0.1",
+                "phar-io/version": "^2.0",
                 "php": "^5.6 || ^7.0"
             },
             "type": "library",
             "authors": [
                 {
                     "name": "Arne Blankerts",
-                    "email": "[email protected]",
-                    "role": "Developer"
+                    "role": "Developer",
+                    "email": "[email protected]"
                 },
                 {
                     "name": "Sebastian Heuer",
-                    "email": "[email protected]",
-                    "role": "Developer"
+                    "role": "Developer",
+                    "email": "[email protected]"
                 },
                 {
                     "name": "Sebastian Bergmann",
-                    "email": "[email protected]",
-                    "role": "Developer"
+                    "role": "Developer",
+                    "email": "[email protected]"
                 }
             ],
             "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)",
-            "time": "2017-03-05T18:14:27+00:00"
+            "time": "2018-07-08T19:23:20+00:00"
         },
         {
             "name": "phar-io/version",
-            "version": "1.0.1",
+            "version": "2.0.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phar-io/version.git",
-                "reference": "a70c0ced4be299a63d32fa96d9281d03e94041df"
+                "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phar-io/version/zipball/a70c0ced4be299a63d32fa96d9281d03e94041df",
-                "reference": "a70c0ced4be299a63d32fa96d9281d03e94041df",
+                "url": "https://api.github.com/repos/phar-io/version/zipball/45a2ec53a73c70ce41d55cedef9063630abaf1b6",
+                "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6",
                 "shasum": ""
             },
             "require": {
                 }
             ],
             "description": "Library for handling version information and constraints",
-            "time": "2017-03-05T17:38:23+00:00"
+            "time": "2018-07-08T19:19:57+00:00"
         },
         {
             "name": "phpdocumentor/reflection-common",
-            "version": "1.0.1",
+            "version": "2.0.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpDocumentor/ReflectionCommon.git",
-                "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6"
+                "reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6",
-                "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6",
+                "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/63a995caa1ca9e5590304cd845c15ad6d482a62a",
+                "reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.5"
+                "php": ">=7.1"
             },
             "require-dev": {
-                "phpunit/phpunit": "^4.6"
+                "phpunit/phpunit": "~6"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.0.x-dev"
+                    "dev-master": "2.x-dev"
                 }
             },
             "autoload": {
                 "psr-4": {
-                    "phpDocumentor\\Reflection\\": [
-                        "src"
-                    ]
+                    "phpDocumentor\\Reflection\\": "src/"
                 }
             },
             "notification-url": "https://packagist.org/downloads/",
                 "reflection",
                 "static analysis"
             ],
-            "time": "2017-09-11T18:02:19+00:00"
+            "time": "2018-08-07T13:53:10+00:00"
         },
         {
             "name": "phpdocumentor/reflection-docblock",
-            "version": "4.3.0",
+            "version": "4.3.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
-                "reference": "94fd0001232e47129dd3504189fa1c7225010d08"
+                "reference": "b83ff7cfcfee7827e1e78b637a5904fe6a96698e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/94fd0001232e47129dd3504189fa1c7225010d08",
-                "reference": "94fd0001232e47129dd3504189fa1c7225010d08",
+                "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/b83ff7cfcfee7827e1e78b637a5904fe6a96698e",
+                "reference": "b83ff7cfcfee7827e1e78b637a5904fe6a96698e",
                 "shasum": ""
             },
             "require": {
                 "php": "^7.0",
-                "phpdocumentor/reflection-common": "^1.0.0",
-                "phpdocumentor/type-resolver": "^0.4.0",
+                "phpdocumentor/reflection-common": "^1.0.0 || ^2.0.0",
+                "phpdocumentor/type-resolver": "~0.4 || ^1.0.0",
                 "webmozart/assert": "^1.0"
             },
             "require-dev": {
-                "doctrine/instantiator": "~1.0.5",
+                "doctrine/instantiator": "^1.0.5",
                 "mockery/mockery": "^1.0",
                 "phpunit/phpunit": "^6.4"
             },
                 }
             ],
             "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
-            "time": "2017-11-30T07:14:17+00:00"
+            "time": "2019-09-12T14:27:41+00:00"
         },
         {
             "name": "phpdocumentor/type-resolver",
-            "version": "0.4.0",
+            "version": "1.0.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpDocumentor/TypeResolver.git",
-                "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7"
+                "reference": "2e32a6d48972b2c1976ed5d8967145b6cec4a4a9"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/9c977708995954784726e25d0cd1dddf4e65b0f7",
-                "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7",
+                "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/2e32a6d48972b2c1976ed5d8967145b6cec4a4a9",
+                "reference": "2e32a6d48972b2c1976ed5d8967145b6cec4a4a9",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.5 || ^7.0",
-                "phpdocumentor/reflection-common": "^1.0"
+                "php": "^7.1",
+                "phpdocumentor/reflection-common": "^2.0"
             },
             "require-dev": {
-                "mockery/mockery": "^0.9.4",
-                "phpunit/phpunit": "^5.2||^4.8.24"
+                "ext-tokenizer": "^7.1",
+                "mockery/mockery": "~1",
+                "phpunit/phpunit": "^7.0"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.0.x-dev"
+                    "dev-master": "1.x-dev"
                 }
             },
             "autoload": {
                 "psr-4": {
-                    "phpDocumentor\\Reflection\\": [
-                        "src/"
-                    ]
+                    "phpDocumentor\\Reflection\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Mike van Riel",
+                    "email": "[email protected]"
+                }
+            ],
+            "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names",
+            "time": "2019-08-22T18:11:29+00:00"
+        },
+        {
+            "name": "phploc/phploc",
+            "version": "5.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/phploc.git",
+                "reference": "5b714ccb7cb8ca29ccf9caf6eb1aed0131d3a884"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/phploc/zipball/5b714ccb7cb8ca29ccf9caf6eb1aed0131d3a884",
+                "reference": "5b714ccb7cb8ca29ccf9caf6eb1aed0131d3a884",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.2",
+                "sebastian/finder-facade": "^1.1",
+                "sebastian/version": "^2.0",
+                "symfony/console": "^4.0"
+            },
+            "bin": [
+                "phploc"
+            ],
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "5.0-dev"
                 }
             },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
             "notification-url": "https://packagist.org/downloads/",
             "license": [
-                "MIT"
+                "BSD-3-Clause"
             ],
             "authors": [
                 {
-                    "name": "Mike van Riel",
-                    "email": "[email protected]"
+                    "name": "Sebastian Bergmann",
+                    "email": "[email protected]",
+                    "role": "lead"
                 }
             ],
-            "time": "2017-07-14T14:27:02+00:00"
+            "description": "A tool for quickly measuring the size of a PHP project.",
+            "homepage": "https://github.com/sebastianbergmann/phploc",
+            "time": "2019-03-16T10:41:19+00:00"
         },
         {
             "name": "phpspec/prophecy",
-            "version": "1.8.0",
+            "version": "1.8.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpspec/prophecy.git",
-                "reference": "4ba436b55987b4bf311cb7c6ba82aa528aac0a06"
+                "reference": "1927e75f4ed19131ec9bcc3b002e07fb1173ee76"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpspec/prophecy/zipball/4ba436b55987b4bf311cb7c6ba82aa528aac0a06",
-                "reference": "4ba436b55987b4bf311cb7c6ba82aa528aac0a06",
+                "url": "https://api.github.com/repos/phpspec/prophecy/zipball/1927e75f4ed19131ec9bcc3b002e07fb1173ee76",
+                "reference": "1927e75f4ed19131ec9bcc3b002e07fb1173ee76",
                 "shasum": ""
             },
             "require": {
                 }
             },
             "autoload": {
-                "psr-0": {
-                    "Prophecy\\": "src/"
+                "psr-4": {
+                    "Prophecy\\": "src/Prophecy"
                 }
             },
             "notification-url": "https://packagist.org/downloads/",
                 "spy",
                 "stub"
             ],
-            "time": "2018-08-05T17:53:17+00:00"
+            "time": "2019-06-13T12:50:23+00:00"
         },
         {
             "name": "phpunit/php-code-coverage",
-            "version": "5.3.2",
+            "version": "7.0.7",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
-                "reference": "c89677919c5dd6d3b3852f230a663118762218ac"
+                "reference": "7743bbcfff2a907e9ee4a25be13d0f8ec5e73800"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/c89677919c5dd6d3b3852f230a663118762218ac",
-                "reference": "c89677919c5dd6d3b3852f230a663118762218ac",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/7743bbcfff2a907e9ee4a25be13d0f8ec5e73800",
+                "reference": "7743bbcfff2a907e9ee4a25be13d0f8ec5e73800",
                 "shasum": ""
             },
             "require": {
                 "ext-dom": "*",
                 "ext-xmlwriter": "*",
-                "php": "^7.0",
-                "phpunit/php-file-iterator": "^1.4.2",
+                "php": "^7.2",
+                "phpunit/php-file-iterator": "^2.0.2",
                 "phpunit/php-text-template": "^1.2.1",
-                "phpunit/php-token-stream": "^2.0.1",
+                "phpunit/php-token-stream": "^3.1.0",
                 "sebastian/code-unit-reverse-lookup": "^1.0.1",
-                "sebastian/environment": "^3.0",
+                "sebastian/environment": "^4.2.2",
                 "sebastian/version": "^2.0.1",
-                "theseer/tokenizer": "^1.1"
+                "theseer/tokenizer": "^1.1.3"
             },
             "require-dev": {
-                "phpunit/phpunit": "^6.0"
+                "phpunit/phpunit": "^8.2.2"
             },
             "suggest": {
-                "ext-xdebug": "^2.5.5"
+                "ext-xdebug": "^2.7.2"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "5.3.x-dev"
+                    "dev-master": "7.0-dev"
                 }
             },
             "autoload": {
                 "testing",
                 "xunit"
             ],
-            "time": "2018-04-06T15:36:58+00:00"
+            "time": "2019-07-25T05:31:54+00:00"
         },
         {
             "name": "phpunit/php-file-iterator",
-            "version": "1.4.5",
+            "version": "2.0.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/php-file-iterator.git",
-                "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4"
+                "reference": "050bedf145a257b1ff02746c31894800e5122946"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/730b01bc3e867237eaac355e06a36b85dd93a8b4",
-                "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/050bedf145a257b1ff02746c31894800e5122946",
+                "reference": "050bedf145a257b1ff02746c31894800e5122946",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.3.3"
+                "php": "^7.1"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^7.1"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.4.x-dev"
+                    "dev-master": "2.0.x-dev"
                 }
             },
             "autoload": {
             "authors": [
                 {
                     "name": "Sebastian Bergmann",
-                    "email": "sb@sebastian-bergmann.de",
+                    "email": "sebastian@phpunit.de",
                     "role": "lead"
                 }
             ],
                 "filesystem",
                 "iterator"
             ],
-            "time": "2017-11-27T13:52:08+00:00"
+            "time": "2018-09-13T20:33:42+00:00"
         },
         {
             "name": "phpunit/php-text-template",
         },
         {
             "name": "phpunit/php-timer",
-            "version": "1.0.9",
+            "version": "2.1.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/php-timer.git",
-                "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f"
+                "reference": "1038454804406b0b5f5f520358e78c1c2f71501e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3dcf38ca72b158baf0bc245e9184d3fdffa9c46f",
-                "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/1038454804406b0b5f5f520358e78c1c2f71501e",
+                "reference": "1038454804406b0b5f5f520358e78c1c2f71501e",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.3.3 || ^7.0"
+                "php": "^7.1"
             },
             "require-dev": {
-                "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0"
+                "phpunit/phpunit": "^7.0"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.0-dev"
+                    "dev-master": "2.1-dev"
                 }
             },
             "autoload": {
             "authors": [
                 {
                     "name": "Sebastian Bergmann",
-                    "email": "sb@sebastian-bergmann.de",
+                    "email": "sebastian@phpunit.de",
                     "role": "lead"
                 }
             ],
             "keywords": [
                 "timer"
             ],
-            "time": "2017-02-26T11:10:40+00:00"
+            "time": "2019-06-07T04:22:29+00:00"
         },
         {
             "name": "phpunit/php-token-stream",
-            "version": "2.0.2",
+            "version": "3.1.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/php-token-stream.git",
-                "reference": "791198a2c6254db10131eecfe8c06670700904db"
+                "reference": "e899757bb3df5ff6e95089132f32cd59aac2220a"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/791198a2c6254db10131eecfe8c06670700904db",
-                "reference": "791198a2c6254db10131eecfe8c06670700904db",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/e899757bb3df5ff6e95089132f32cd59aac2220a",
+                "reference": "e899757bb3df5ff6e95089132f32cd59aac2220a",
                 "shasum": ""
             },
             "require": {
                 "ext-tokenizer": "*",
-                "php": "^7.0"
+                "php": "^7.1"
             },
             "require-dev": {
-                "phpunit/phpunit": "^6.2.4"
+                "phpunit/phpunit": "^7.0"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "2.0-dev"
+                    "dev-master": "3.1-dev"
                 }
             },
             "autoload": {
             "keywords": [
                 "tokenizer"
             ],
-            "time": "2017-11-27T05:48:46+00:00"
+            "time": "2019-07-25T05:29:42+00:00"
         },
         {
             "name": "phpunit/phpunit",
-            "version": "6.5.13",
+            "version": "8.3.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/phpunit.git",
-                "reference": "0973426fb012359b2f18d3bd1e90ef1172839693"
+                "reference": "302faed7059fde575cf3403a78c730c5e3a62750"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0973426fb012359b2f18d3bd1e90ef1172839693",
-                "reference": "0973426fb012359b2f18d3bd1e90ef1172839693",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/302faed7059fde575cf3403a78c730c5e3a62750",
+                "reference": "302faed7059fde575cf3403a78c730c5e3a62750",
                 "shasum": ""
             },
             "require": {
+                "doctrine/instantiator": "^1.2.0",
                 "ext-dom": "*",
                 "ext-json": "*",
                 "ext-libxml": "*",
                 "ext-mbstring": "*",
                 "ext-xml": "*",
-                "myclabs/deep-copy": "^1.6.1",
-                "phar-io/manifest": "^1.0.1",
-                "phar-io/version": "^1.0",
-                "php": "^7.0",
-                "phpspec/prophecy": "^1.7",
-                "phpunit/php-code-coverage": "^5.3",
-                "phpunit/php-file-iterator": "^1.4.3",
+                "ext-xmlwriter": "*",
+                "myclabs/deep-copy": "^1.9.1",
+                "phar-io/manifest": "^1.0.3",
+                "phar-io/version": "^2.0.1",
+                "php": "^7.2",
+                "phpspec/prophecy": "^1.8.1",
+                "phpunit/php-code-coverage": "^7.0.7",
+                "phpunit/php-file-iterator": "^2.0.2",
                 "phpunit/php-text-template": "^1.2.1",
-                "phpunit/php-timer": "^1.0.9",
-                "phpunit/phpunit-mock-objects": "^5.0.9",
-                "sebastian/comparator": "^2.1",
-                "sebastian/diff": "^2.0",
-                "sebastian/environment": "^3.1",
-                "sebastian/exporter": "^3.1",
-                "sebastian/global-state": "^2.0",
+                "phpunit/php-timer": "^2.1.2",
+                "sebastian/comparator": "^3.0.2",
+                "sebastian/diff": "^3.0.2",
+                "sebastian/environment": "^4.2.2",
+                "sebastian/exporter": "^3.1.1",
+                "sebastian/global-state": "^3.0.0",
                 "sebastian/object-enumerator": "^3.0.3",
-                "sebastian/resource-operations": "^1.0",
+                "sebastian/resource-operations": "^2.0.1",
+                "sebastian/type": "^1.1.3",
                 "sebastian/version": "^2.0.1"
             },
-            "conflict": {
-                "phpdocumentor/reflection-docblock": "3.0.2",
-                "phpunit/dbunit": "<3.0"
-            },
             "require-dev": {
                 "ext-pdo": "*"
             },
             "suggest": {
+                "ext-soap": "*",
                 "ext-xdebug": "*",
-                "phpunit/php-invoker": "^1.1"
+                "phpunit/php-invoker": "^2.0.0"
             },
             "bin": [
                 "phpunit"
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "6.5.x-dev"
+                    "dev-master": "8.3-dev"
                 }
             },
             "autoload": {
                 "testing",
                 "xunit"
             ],
-            "time": "2018-09-08T15:10:43+00:00"
+            "time": "2019-09-14T09:12:03+00:00"
         },
         {
-            "name": "phpunit/phpunit-mock-objects",
-            "version": "5.0.10",
+            "name": "scrivo/highlight.php",
+            "version": "v9.15.10.0",
             "source": {
                 "type": "git",
-                "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git",
-                "reference": "cd1cf05c553ecfec36b170070573e540b67d3f1f"
+                "url": "https://github.com/scrivo/highlight.php.git",
+                "reference": "9ad3adb4456dc91196327498dbbce6aa1ba1239e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/cd1cf05c553ecfec36b170070573e540b67d3f1f",
-                "reference": "cd1cf05c553ecfec36b170070573e540b67d3f1f",
+                "url": "https://api.github.com/repos/scrivo/highlight.php/zipball/9ad3adb4456dc91196327498dbbce6aa1ba1239e",
+                "reference": "9ad3adb4456dc91196327498dbbce6aa1ba1239e",
                 "shasum": ""
             },
             "require": {
-                "doctrine/instantiator": "^1.0.5",
-                "php": "^7.0",
-                "phpunit/php-text-template": "^1.2.1",
-                "sebastian/exporter": "^3.1"
-            },
-            "conflict": {
-                "phpunit/phpunit": "<6.0"
+                "ext-json": "*",
+                "ext-mbstring": "*",
+                "php": ">=5.4"
             },
             "require-dev": {
-                "phpunit/phpunit": "^6.5.11"
+                "phpunit/phpunit": "^4.8|^5.7",
+                "symfony/finder": "^2.8"
             },
             "suggest": {
-                "ext-soap": "*"
+                "ext-dom": "Needed to make use of the features in the utilities namespace"
             },
             "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "5.0.x-dev"
-                }
-            },
             "autoload": {
-                "classmap": [
-                    "src/"
+                "psr-0": {
+                    "Highlight\\": "",
+                    "HighlightUtilities\\": ""
+                },
+                "files": [
+                    "HighlightUtilities/functions.php"
                 ]
             },
             "notification-url": "https://packagist.org/downloads/",
             ],
             "authors": [
                 {
-                    "name": "Sebastian Bergmann",
-                    "email": "[email protected]",
-                    "role": "lead"
+                    "name": "Geert Bergman",
+                    "role": "Project Author",
+                    "homepage": "http://www.scrivo.org/"
+                },
+                {
+                    "name": "Vladimir Jimenez",
+                    "role": "Contributor",
+                    "homepage": "https://allejo.io"
+                },
+                {
+                    "name": "Martin Folkers",
+                    "role": "Contributor",
+                    "homepage": "https://twobrain.io"
                 }
             ],
-            "description": "Mock Object library for PHPUnit",
-            "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/",
+            "description": "Server side syntax highlighter that supports 185 languages. It's a PHP port of highlight.js",
             "keywords": [
-                "mock",
-                "xunit"
+                "code",
+                "highlight",
+                "highlight.js",
+                "highlight.php",
+                "syntax"
             ],
-            "time": "2018-08-09T05:50:03+00:00"
+            "time": "2019-08-27T04:27:48+00:00"
         },
         {
             "name": "sebastian/code-unit-reverse-lookup",
         },
         {
             "name": "sebastian/comparator",
-            "version": "2.1.3",
+            "version": "3.0.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/comparator.git",
-                "reference": "34369daee48eafb2651bea869b4b15d75ccc35f9"
+                "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/34369daee48eafb2651bea869b4b15d75ccc35f9",
-                "reference": "34369daee48eafb2651bea869b4b15d75ccc35f9",
+                "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/5de4fc177adf9bce8df98d8d141a7559d7ccf6da",
+                "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da",
                 "shasum": ""
             },
             "require": {
-                "php": "^7.0",
-                "sebastian/diff": "^2.0 || ^3.0",
+                "php": "^7.1",
+                "sebastian/diff": "^3.0",
                 "sebastian/exporter": "^3.1"
             },
             "require-dev": {
-                "phpunit/phpunit": "^6.4"
+                "phpunit/phpunit": "^7.1"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "2.1.x-dev"
+                    "dev-master": "3.0-dev"
                 }
             },
             "autoload": {
                 "compare",
                 "equality"
             ],
-            "time": "2018-02-01T13:46:46+00:00"
+            "time": "2018-07-12T15:12:46+00:00"
         },
         {
             "name": "sebastian/diff",
-            "version": "2.0.1",
+            "version": "3.0.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/diff.git",
-                "reference": "347c1d8b49c5c3ee30c7040ea6fc446790e6bddd"
+                "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/347c1d8b49c5c3ee30c7040ea6fc446790e6bddd",
-                "reference": "347c1d8b49c5c3ee30c7040ea6fc446790e6bddd",
+                "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/720fcc7e9b5cf384ea68d9d930d480907a0c1a29",
+                "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29",
                 "shasum": ""
             },
             "require": {
-                "php": "^7.0"
+                "php": "^7.1"
             },
             "require-dev": {
-                "phpunit/phpunit": "^6.2"
+                "phpunit/phpunit": "^7.5 || ^8.0",
+                "symfony/process": "^2 || ^3.3 || ^4"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "2.0-dev"
+                    "dev-master": "3.0-dev"
                 }
             },
             "autoload": {
             "description": "Diff implementation",
             "homepage": "https://github.com/sebastianbergmann/diff",
             "keywords": [
-                "diff"
+                "diff",
+                "udiff",
+                "unidiff",
+                "unified diff"
             ],
-            "time": "2017-08-03T08:09:46+00:00"
+            "time": "2019-02-04T06:01:07+00:00"
         },
         {
             "name": "sebastian/environment",
-            "version": "3.1.0",
+            "version": "4.2.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/environment.git",
-                "reference": "cd0871b3975fb7fc44d11314fd1ee20925fce4f5"
+                "reference": "f2a2c8e1c97c11ace607a7a667d73d47c19fe404"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/cd0871b3975fb7fc44d11314fd1ee20925fce4f5",
-                "reference": "cd0871b3975fb7fc44d11314fd1ee20925fce4f5",
+                "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/f2a2c8e1c97c11ace607a7a667d73d47c19fe404",
+                "reference": "f2a2c8e1c97c11ace607a7a667d73d47c19fe404",
                 "shasum": ""
             },
             "require": {
-                "php": "^7.0"
+                "php": "^7.1"
             },
             "require-dev": {
-                "phpunit/phpunit": "^6.1"
+                "phpunit/phpunit": "^7.5"
+            },
+            "suggest": {
+                "ext-posix": "*"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.1.x-dev"
+                    "dev-master": "4.2-dev"
                 }
             },
             "autoload": {
                 "environment",
                 "hhvm"
             ],
-            "time": "2017-07-01T08:51:00+00:00"
+            "time": "2019-05-05T09:05:15+00:00"
         },
         {
             "name": "sebastian/exporter",
-            "version": "3.1.0",
+            "version": "3.1.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/exporter.git",
-                "reference": "234199f4528de6d12aaa58b612e98f7d36adb937"
+                "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/234199f4528de6d12aaa58b612e98f7d36adb937",
-                "reference": "234199f4528de6d12aaa58b612e98f7d36adb937",
+                "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/68609e1261d215ea5b21b7987539cbfbe156ec3e",
+                "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e",
                 "shasum": ""
             },
             "require": {
                 "BSD-3-Clause"
             ],
             "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "[email protected]"
+                },
                 {
                     "name": "Jeff Welch",
                     "email": "[email protected]"
                     "name": "Volker Dusch",
                     "email": "[email protected]"
                 },
-                {
-                    "name": "Bernhard Schussek",
-                    "email": "[email protected]"
-                },
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "[email protected]"
-                },
                 {
                     "name": "Adam Harvey",
                     "email": "[email protected]"
+                },
+                {
+                    "name": "Bernhard Schussek",
+                    "email": "[email protected]"
                 }
             ],
             "description": "Provides the functionality to export PHP variables for visualization",
                 "export",
                 "exporter"
             ],
-            "time": "2017-04-03T13:19:02+00:00"
+            "time": "2019-09-14T09:02:43+00:00"
+        },
+        {
+            "name": "sebastian/finder-facade",
+            "version": "1.2.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/finder-facade.git",
+                "reference": "4a3174709c2dc565fe5fb26fcf827f6a1fc7b09f"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/finder-facade/zipball/4a3174709c2dc565fe5fb26fcf827f6a1fc7b09f",
+                "reference": "4a3174709c2dc565fe5fb26fcf827f6a1fc7b09f",
+                "shasum": ""
+            },
+            "require": {
+                "symfony/finder": "~2.3|~3.0|~4.0",
+                "theseer/fdomdocument": "~1.3"
+            },
+            "type": "library",
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "[email protected]",
+                    "role": "lead"
+                }
+            ],
+            "description": "FinderFacade is a convenience wrapper for Symfony's Finder component.",
+            "homepage": "https://github.com/sebastianbergmann/finder-facade",
+            "time": "2017-11-18T17:31:49+00:00"
         },
         {
             "name": "sebastian/global-state",
-            "version": "2.0.0",
+            "version": "3.0.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/global-state.git",
-                "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4"
+                "reference": "edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4",
-                "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4",
+                "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4",
+                "reference": "edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4",
                 "shasum": ""
             },
             "require": {
-                "php": "^7.0"
+                "php": "^7.2",
+                "sebastian/object-reflector": "^1.1.1",
+                "sebastian/recursion-context": "^3.0"
             },
             "require-dev": {
-                "phpunit/phpunit": "^6.0"
+                "ext-dom": "*",
+                "phpunit/phpunit": "^8.0"
             },
             "suggest": {
                 "ext-uopz": "*"
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "2.0-dev"
+                    "dev-master": "3.0-dev"
                 }
             },
             "autoload": {
             "keywords": [
                 "global state"
             ],
-            "time": "2017-04-27T15:39:26+00:00"
+            "time": "2019-02-01T05:30:01+00:00"
         },
         {
             "name": "sebastian/object-enumerator",
         },
         {
             "name": "sebastian/resource-operations",
-            "version": "1.0.0",
+            "version": "2.0.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/resource-operations.git",
-                "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52"
+                "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/ce990bb21759f94aeafd30209e8cfcdfa8bc3f52",
-                "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52",
+                "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/4d7a795d35b889bf80a0cc04e08d77cedfa917a9",
+                "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.6.0"
+                "php": "^7.1"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.0.x-dev"
+                    "dev-master": "2.0-dev"
                 }
             },
             "autoload": {
             ],
             "description": "Provides a list of PHP built-in functions that operate on resources",
             "homepage": "https://www.github.com/sebastianbergmann/resource-operations",
-            "time": "2015-07-28T20:34:47+00:00"
+            "time": "2018-10-04T04:07:39+00:00"
+        },
+        {
+            "name": "sebastian/type",
+            "version": "1.1.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/type.git",
+                "reference": "3aaaa15fa71d27650d62a948be022fe3b48541a3"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/3aaaa15fa71d27650d62a948be022fe3b48541a3",
+                "reference": "3aaaa15fa71d27650d62a948be022fe3b48541a3",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.2"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^8.2"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.1-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "[email protected]",
+                    "role": "lead"
+                }
+            ],
+            "description": "Collection of value objects that represent the types of the PHP type system",
+            "homepage": "https://github.com/sebastianbergmann/type",
+            "time": "2019-07-02T08:10:15+00:00"
         },
         {
             "name": "sebastian/version",
         },
         {
             "name": "squizlabs/php_codesniffer",
-            "version": "3.4.0",
+            "version": "3.4.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
-                "reference": "379deb987e26c7cd103a7b387aea178baec96e48"
+                "reference": "b8a7362af1cc1aadb5bd36c3defc4dda2cf5f0a8"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/379deb987e26c7cd103a7b387aea178baec96e48",
-                "reference": "379deb987e26c7cd103a7b387aea178baec96e48",
+                "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/b8a7362af1cc1aadb5bd36c3defc4dda2cf5f0a8",
+                "reference": "b8a7362af1cc1aadb5bd36c3defc4dda2cf5f0a8",
                 "shasum": ""
             },
             "require": {
                 }
             ],
             "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.",
-            "homepage": "http://www.squizlabs.com/php-codesniffer",
+            "homepage": "https://github.com/squizlabs/PHP_CodeSniffer",
             "keywords": [
                 "phpcs",
                 "standards"
             ],
-            "time": "2018-12-19T23:57:18+00:00"
+            "time": "2019-04-10T23:49:02+00:00"
         },
         {
             "name": "symfony/dom-crawler",
-            "version": "v3.1.10",
+            "version": "v4.3.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/dom-crawler.git",
-                "reference": "7eede2a901a19928494194f7d1815a77b9a473a0"
+                "reference": "cc686552948d627528c0e2e759186dff67c2610e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/7eede2a901a19928494194f7d1815a77b9a473a0",
-                "reference": "7eede2a901a19928494194f7d1815a77b9a473a0",
+                "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/cc686552948d627528c0e2e759186dff67c2610e",
+                "reference": "cc686552948d627528c0e2e759186dff67c2610e",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.5.9",
+                "php": "^7.1.3",
+                "symfony/polyfill-ctype": "~1.8",
                 "symfony/polyfill-mbstring": "~1.0"
             },
+            "conflict": {
+                "masterminds/html5": "<2.6"
+            },
             "require-dev": {
-                "symfony/css-selector": "~2.8|~3.0"
+                "masterminds/html5": "^2.6",
+                "symfony/css-selector": "~3.4|~4.0"
             },
             "suggest": {
                 "symfony/css-selector": ""
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.1-dev"
+                    "dev-master": "4.3-dev"
                 }
             },
             "autoload": {
             ],
             "description": "Symfony DomCrawler Component",
             "homepage": "https://symfony.com",
-            "time": "2017-01-21T17:13:55+00:00"
+            "time": "2019-08-26T08:26:39+00:00"
         },
         {
             "name": "symfony/filesystem",
-            "version": "v3.3.6",
+            "version": "v4.3.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/filesystem.git",
-                "reference": "427987eb4eed764c3b6e38d52a0f87989e010676"
+                "reference": "9abbb7ef96a51f4d7e69627bc6f63307994e4263"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/filesystem/zipball/427987eb4eed764c3b6e38d52a0f87989e010676",
-                "reference": "427987eb4eed764c3b6e38d52a0f87989e010676",
+                "url": "https://api.github.com/repos/symfony/filesystem/zipball/9abbb7ef96a51f4d7e69627bc6f63307994e4263",
+                "reference": "9abbb7ef96a51f4d7e69627bc6f63307994e4263",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.5.9"
+                "php": "^7.1.3",
+                "symfony/polyfill-ctype": "~1.8"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.3-dev"
+                    "dev-master": "4.3-dev"
                 }
             },
             "autoload": {
             ],
             "description": "Symfony Filesystem Component",
             "homepage": "https://symfony.com",
-            "time": "2017-07-11T07:17:58+00:00"
+            "time": "2019-08-20T14:07:54+00:00"
+        },
+        {
+            "name": "theseer/fdomdocument",
+            "version": "1.6.6",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/theseer/fDOMDocument.git",
+                "reference": "6e8203e40a32a9c770bcb62fe37e68b948da6dca"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/theseer/fDOMDocument/zipball/6e8203e40a32a9c770bcb62fe37e68b948da6dca",
+                "reference": "6e8203e40a32a9c770bcb62fe37e68b948da6dca",
+                "shasum": ""
+            },
+            "require": {
+                "ext-dom": "*",
+                "lib-libxml": "*",
+                "php": ">=5.3.3"
+            },
+            "type": "library",
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Arne Blankerts",
+                    "role": "lead",
+                    "email": "[email protected]"
+                }
+            ],
+            "description": "The classes contained within this repository extend the standard DOM to use exceptions at all occasions of errors instead of PHP warnings or notices. They also add various custom methods and shortcuts for convenience and to simplify the usage of DOM.",
+            "homepage": "https://github.com/theseer/fDOMDocument",
+            "time": "2017-06-30T11:53:12+00:00"
         },
         {
             "name": "theseer/tokenizer",
-            "version": "1.1.0",
+            "version": "1.1.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/theseer/tokenizer.git",
-                "reference": "cb2f008f3f05af2893a87208fe6a6c4985483f8b"
+                "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/theseer/tokenizer/zipball/cb2f008f3f05af2893a87208fe6a6c4985483f8b",
-                "reference": "cb2f008f3f05af2893a87208fe6a6c4985483f8b",
+                "url": "https://api.github.com/repos/theseer/tokenizer/zipball/11336f6f84e16a720dae9d8e6ed5019efa85a0f9",
+                "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9",
                 "shasum": ""
             },
             "require": {
             "authors": [
                 {
                     "name": "Arne Blankerts",
-                    "email": "[email protected]",
-                    "role": "Developer"
+                    "role": "Developer",
+                    "email": "[email protected]"
                 }
             ],
             "description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
-            "time": "2017-04-07T12:08:54+00:00"
+            "time": "2019-06-13T22:48:21+00:00"
         },
         {
             "name": "webmozart/assert",
-            "version": "1.4.0",
+            "version": "1.5.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/webmozart/assert.git",
-                "reference": "83e253c8e0be5b0257b881e1827274667c5c17a9"
+                "reference": "88e6d84706d09a236046d686bbea96f07b3a34f4"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/webmozart/assert/zipball/83e253c8e0be5b0257b881e1827274667c5c17a9",
-                "reference": "83e253c8e0be5b0257b881e1827274667c5c17a9",
+                "url": "https://api.github.com/repos/webmozart/assert/zipball/88e6d84706d09a236046d686bbea96f07b3a34f4",
+                "reference": "88e6d84706d09a236046d686bbea96f07b3a34f4",
                 "shasum": ""
             },
             "require": {
                 "symfony/polyfill-ctype": "^1.8"
             },
             "require-dev": {
-                "phpunit/phpunit": "^4.6",
-                "sebastian/version": "^1.0.1"
+                "phpunit/phpunit": "^4.8.36 || ^7.5.13"
             },
             "type": "library",
             "extra": {
                 "check",
                 "validate"
             ],
-            "time": "2018-12-25T11:19:39+00:00"
+            "time": "2019-08-24T08:43:50+00:00"
+        },
+        {
+            "name": "wnx/laravel-stats",
+            "version": "v2.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/stefanzweifel/laravel-stats.git",
+                "reference": "1b3c60bfbf81233973cbc2a63be4e6f83b2d6205"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/stefanzweifel/laravel-stats/zipball/1b3c60bfbf81233973cbc2a63be4e6f83b2d6205",
+                "reference": "1b3c60bfbf81233973cbc2a63be4e6f83b2d6205",
+                "shasum": ""
+            },
+            "require": {
+                "illuminate/console": "~5.8.0|^6.0",
+                "illuminate/support": "~5.8.0|^6.0",
+                "php": ">=7.2.0",
+                "phploc/phploc": "~4.0|~5.0",
+                "symfony/finder": "~3.3|~4.0"
+            },
+            "require-dev": {
+                "laravel/browser-kit-testing": "~2.0|~3.0|~4.0|~5.0",
+                "laravel/dusk": "~3.0|~4.0|~5.0",
+                "mockery/mockery": "^1.1",
+                "orchestra/testbench": "^3.8",
+                "phpunit/phpunit": "6.*|7.*|8.*"
+            },
+            "type": "library",
+            "extra": {
+                "laravel": {
+                    "providers": [
+                        "Wnx\\LaravelStats\\StatsServiceProvider"
+                    ]
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Wnx\\LaravelStats\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Stefan Zweifel",
+                    "email": "[email protected]",
+                    "homepage": "https://stefanzweifel.io",
+                    "role": "Developer"
+                }
+            ],
+            "description": "Get insights about your Laravel Project",
+            "homepage": "https://github.com/stefanzweifel/laravel-stats",
+            "keywords": [
+                "laravel",
+                "statistics",
+                "stats",
+                "wnx"
+            ],
+            "time": "2019-09-01T14:18:49+00:00"
         }
     ],
     "aliases": [],
-    "minimum-stability": "stable",
-    "stability-flags": {
-        "laravel/socialite": 20
-    },
-    "prefer-stable": false,
+    "minimum-stability": "dev",
+    "stability-flags": [],
+    "prefer-stable": true,
     "prefer-lowest": false,
     "platform": {
-        "php": ">=7.0.5",
-        "ext-json": "*",
-        "ext-tidy": "*",
+        "php": "^7.2",
+        "ext-curl": "*",
         "ext-dom": "*",
-        "ext-xml": "*",
-        "ext-mbstring": "*",
         "ext-gd": "*",
-        "ext-curl": "*"
+        "ext-json": "*",
+        "ext-mbstring": "*",
+        "ext-tidy": "*",
+        "ext-xml": "*"
     },
     "platform-dev": [],
     "platform-overrides": {
-        "php": "7.0.5"
+        "php": "7.2.0"
     }
 }
index de6b0b276899b976afd5f78235a6b5d7ec803f75..ddf3c295d22e09023fad7e62eaefc5c198f454a1 100644 (file)
@@ -15,8 +15,8 @@ $factory->define(\BookStack\Auth\User::class, function ($faker) {
     return [
         'name' => $faker->name,
         'email' => $faker->email,
-        'password' => str_random(10),
-        'remember_token' => str_random(10),
+        'password' => Str::random(10),
+        'remember_token' => Str::random(10),
         'email_confirmed' => 1
     ];
 });
@@ -24,7 +24,7 @@ $factory->define(\BookStack\Auth\User::class, function ($faker) {
 $factory->define(\BookStack\Entities\Bookshelf::class, function ($faker) {
     return [
         'name' => $faker->sentence,
-        'slug' => str_random(10),
+        'slug' => Str::random(10),
         'description' => $faker->paragraph
     ];
 });
@@ -32,7 +32,7 @@ $factory->define(\BookStack\Entities\Bookshelf::class, function ($faker) {
 $factory->define(\BookStack\Entities\Book::class, function ($faker) {
     return [
         'name' => $faker->sentence,
-        'slug' => str_random(10),
+        'slug' => Str::random(10),
         'description' => $faker->paragraph
     ];
 });
@@ -40,7 +40,7 @@ $factory->define(\BookStack\Entities\Book::class, function ($faker) {
 $factory->define(\BookStack\Entities\Chapter::class, function ($faker) {
     return [
         'name' => $faker->sentence,
-        'slug' => str_random(10),
+        'slug' => Str::random(10),
         'description' => $faker->paragraph
     ];
 });
@@ -49,7 +49,7 @@ $factory->define(\BookStack\Entities\Page::class, function ($faker) {
     $html = '<p>' . implode('</p>', $faker->paragraphs(5)) . '</p>';
     return [
         'name' => $faker->sentence,
-        'slug' => str_random(10),
+        'slug' => Str::random(10),
         'html' => $html,
         'text' => strip_tags($html),
         'revision_count' => 1
diff --git a/database/migrations/2019_07_07_112515_add_template_support.php b/database/migrations/2019_07_07_112515_add_template_support.php
new file mode 100644 (file)
index 0000000..a545081
--- /dev/null
@@ -0,0 +1,54 @@
+<?php
+
+use Carbon\Carbon;
+use Illuminate\Support\Facades\Schema;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Database\Migrations\Migration;
+
+class AddTemplateSupport extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::table('pages', function (Blueprint $table) {
+            $table->boolean('template')->default(false);
+            $table->index('template');
+        });
+
+        // Create new templates-manage permission and assign to admin role
+        $adminRoleId = DB::table('roles')->where('system_name', '=', 'admin')->first()->id;
+        $permissionId = DB::table('role_permissions')->insertGetId([
+            'name' => 'templates-manage',
+            'display_name' => 'Manage Page Templates',
+            'created_at' => Carbon::now()->toDateTimeString(),
+            'updated_at' => Carbon::now()->toDateTimeString()
+        ]);
+        DB::table('permission_role')->insert([
+            'role_id' => $adminRoleId,
+            'permission_id' => $permissionId
+        ]);
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::table('pages', function (Blueprint $table) {
+            $table->dropColumn('template');
+        });
+
+        // Remove templates-manage permission
+        $templatesManagePermission = DB::table('role_permissions')
+            ->where('name', '=', 'templates_manage')->first();
+
+        DB::table('permission_role')->where('permission_id', '=', $templatesManagePermission->id)->delete();
+        DB::table('role_permissions')->where('name', '=', 'templates_manage')->delete();
+    }
+}
diff --git a/database/migrations/2019_08_17_140214_add_user_invites_table.php b/database/migrations/2019_08_17_140214_add_user_invites_table.php
new file mode 100644 (file)
index 0000000..23bd698
--- /dev/null
@@ -0,0 +1,33 @@
+<?php
+
+use Illuminate\Support\Facades\Schema;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Database\Migrations\Migration;
+
+class AddUserInvitesTable extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::create('user_invites', function (Blueprint $table) {
+            $table->increments('id');
+            $table->integer('user_id')->index();
+            $table->string('token')->index();
+            $table->nullableTimestamps();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::dropIfExists('user_invites');
+    }
+}
index 988ea2100519ef6598cc0b13f69105e8b8682355..d86cb0dddd06947d478dde126f7ae8c685ec17a9 100644 (file)
@@ -6,7 +6,7 @@ use Illuminate\Database\Eloquent\Model;
 class DatabaseSeeder extends Seeder
 {
     /**
-     * Run the database seeds.
+     * Seed the application's database.
      *
      * @return void
      */
index ce3cd1307d6f5279f0f1a37ee830bd5d7c661a80..deb1aa11c3b3b196add089e024404eb7c73555a8 100644 (file)
@@ -1,6 +1,14 @@
 <?php
 
+use BookStack\Auth\Permissions\PermissionService;
+use BookStack\Auth\Role;
+use BookStack\Auth\User;
+use BookStack\Entities\Bookshelf;
+use BookStack\Entities\Chapter;
+use BookStack\Entities\Page;
+use BookStack\Entities\SearchService;
 use Illuminate\Database\Seeder;
+use Illuminate\Support\Str;
 
 class DummyContentSeeder extends Seeder
 {
@@ -12,39 +20,39 @@ class DummyContentSeeder extends Seeder
     public function run()
     {
         // Create an editor user
-        $editorUser = factory(\BookStack\Auth\User::class)->create();
-        $editorRole = \BookStack\Auth\Role::getRole('editor');
+        $editorUser = factory(User::class)->create();
+        $editorRole = Role::getRole('editor');
         $editorUser->attachRole($editorRole);
 
         // Create a viewer user
-        $viewerUser = factory(\BookStack\Auth\User::class)->create();
-        $role = \BookStack\Auth\Role::getRole('viewer');
+        $viewerUser = factory(User::class)->create();
+        $role = Role::getRole('viewer');
         $viewerUser->attachRole($role);
 
         $byData = ['created_by' => $editorUser->id, 'updated_by' => $editorUser->id];
 
         factory(\BookStack\Entities\Book::class, 5)->create($byData)
             ->each(function($book) use ($editorUser, $byData) {
-                $chapters = factory(\BookStack\Entities\Chapter::class, 3)->create($byData)
+                $chapters = factory(Chapter::class, 3)->create($byData)
                     ->each(function($chapter) use ($editorUser, $book, $byData){
-                        $pages = factory(\BookStack\Entities\Page::class, 3)->make(array_merge($byData, ['book_id' => $book->id]));
+                        $pages = factory(Page::class, 3)->make(array_merge($byData, ['book_id' => $book->id]));
                         $chapter->pages()->saveMany($pages);
                     });
-                $pages = factory(\BookStack\Entities\Page::class, 3)->make($byData);
+                $pages = factory(Page::class, 3)->make($byData);
                 $book->chapters()->saveMany($chapters);
                 $book->pages()->saveMany($pages);
             });
 
-        $largeBook = factory(\BookStack\Entities\Book::class)->create(array_merge($byData, ['name' => 'Large book' . str_random(10)]));
-        $pages = factory(\BookStack\Entities\Page::class, 200)->make($byData);
-        $chapters = factory(\BookStack\Entities\Chapter::class, 50)->make($byData);
+        $largeBook = factory(\BookStack\Entities\Book::class)->create(array_merge($byData, ['name' => 'Large book' . Str::random(10)]));
+        $pages = factory(Page::class, 200)->make($byData);
+        $chapters = factory(Chapter::class, 50)->make($byData);
         $largeBook->pages()->saveMany($pages);
         $largeBook->chapters()->saveMany($chapters);
 
-        $shelves = factory(\BookStack\Entities\Bookshelf::class, 10)->create($byData);
+        $shelves = factory(Bookshelf::class, 10)->create($byData);
         $largeBook->shelves()->attach($shelves->pluck('id'));
 
-        app(\BookStack\Auth\Permissions\PermissionService::class)->buildJointPermissions();
-        app(\BookStack\Entities\SearchService::class)->indexAllEntities();
+        app(PermissionService::class)->buildJointPermissions();
+        app(SearchService::class)->indexAllEntities();
     }
 }
index 136b6cb6a9f6465627bdab0ccab31f7fd8c24112..4db10395adf037a48aac650a19b2cc02748f3f84 100644 (file)
@@ -1,6 +1,13 @@
 <?php
 
+use BookStack\Auth\Permissions\PermissionService;
+use BookStack\Auth\Role;
+use BookStack\Auth\User;
+use BookStack\Entities\Chapter;
+use BookStack\Entities\Page;
+use BookStack\Entities\SearchService;
 use Illuminate\Database\Seeder;
+use Illuminate\Support\Str;
 
 class LargeContentSeeder extends Seeder
 {
@@ -12,16 +19,16 @@ class LargeContentSeeder extends Seeder
     public function run()
     {
         // Create an editor user
-        $editorUser = factory(\BookStack\Auth\User::class)->create();
-        $editorRole = \BookStack\Auth\Role::getRole('editor');
+        $editorUser = factory(User::class)->create();
+        $editorRole = Role::getRole('editor');
         $editorUser->attachRole($editorRole);
 
-        $largeBook = factory(\BookStack\Entities\Book::class)->create(['name' => 'Large book' . str_random(10), 'created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);
-        $pages = factory(\BookStack\Entities\Page::class, 200)->make(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);
-        $chapters = factory(\BookStack\Entities\Chapter::class, 50)->make(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);
+        $largeBook = factory(\BookStack\Entities\Book::class)->create(['name' => 'Large book' . Str::random(10), 'created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);
+        $pages = factory(Page::class, 200)->make(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);
+        $chapters = factory(Chapter::class, 50)->make(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);
         $largeBook->pages()->saveMany($pages);
         $largeBook->chapters()->saveMany($chapters);
-        app(\BookStack\Auth\Permissions\PermissionService::class)->buildJointPermissions();
-        app(\BookStack\Entities\SearchService::class)->indexAllEntities();
+        app(PermissionService::class)->buildJointPermissions();
+        app(SearchService::class)->indexAllEntities();
     }
 }
diff --git a/dev/docker/Dockerfile b/dev/docker/Dockerfile
new file mode 100644 (file)
index 0000000..8816615
--- /dev/null
@@ -0,0 +1,16 @@
+FROM php:7.3-apache
+
+ENV APACHE_DOCUMENT_ROOT /app/public
+WORKDIR /app
+
+RUN apt-get update -y \
+    && apt-get install -y git zip unzip libtidy-dev libpng-dev libldap2-dev libxml++2.6-dev wait-for-it \
+    && docker-php-ext-configure ldap --with-libdir=lib/x86_64-linux-gnu \
+    && docker-php-ext-install pdo pdo_mysql tidy dom xml mbstring gd ldap \
+    && a2enmod rewrite \
+    && sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf \
+    && sed -ri -e 's!/var/www/!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf \
+    && php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" \
+    && php composer-setup.php \
+    && mv composer.phar /usr/bin/composer \
+    && php -r "unlink('composer-setup.php');"
diff --git a/dev/docker/entrypoint.app.sh b/dev/docker/entrypoint.app.sh
new file mode 100755 (executable)
index 0000000..ff44f0c
--- /dev/null
@@ -0,0 +1,14 @@
+#!/bin/bash
+
+set -e
+
+env
+
+if [[ -n "$1" ]]; then
+    exec "$@"
+else
+    wait-for-it db:3306 -t 45
+    php artisan migrate --database=mysql
+    chown -R www-data:www-data storage
+    exec apache2-foreground
+fi
\ No newline at end of file
diff --git a/dev/docker/entrypoint.node.sh b/dev/docker/entrypoint.node.sh
new file mode 100755 (executable)
index 0000000..e59e1e8
--- /dev/null
@@ -0,0 +1,8 @@
+#!/bin/sh
+
+set -e
+
+npm install
+npm rebuild node-sass
+
+exec npm run watch
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644 (file)
index 0000000..ea7a61a
--- /dev/null
@@ -0,0 +1,48 @@
+# This is a Docker Compose configuration
+# intended for development purposes only
+
+version: '3'
+
+volumes:
+  db: {}
+
+services:
+  db:
+    image: mysql:8
+    environment:
+      MYSQL_DATABASE: bookstack-test
+      MYSQL_USER: bookstack-test
+      MYSQL_PASSWORD: bookstack-test
+      MYSQL_RANDOM_ROOT_PASSWORD: 'true'
+    command: --default-authentication-plugin=mysql_native_password
+    volumes:
+      - db:/var/lib/mysql
+  app:
+    build:
+      context: .
+      dockerfile: ./dev/docker/Dockerfile
+    environment:
+      DB_CONNECTION: mysql
+      DB_HOST: db
+      DB_PORT: 3306
+      DB_DATABASE: bookstack-test
+      DB_USERNAME: bookstack-test
+      DB_PASSWORD: bookstack-test
+      MAIL_DRIVER: smtp
+      MAIL_HOST: mailhog
+      MAIL_PORT: 1025
+    ports:
+      - ${DEV_PORT:-8080}:80
+    volumes:
+      - ./:/app
+    entrypoint: /app/dev/docker/entrypoint.app.sh
+  node:
+    image: node:alpine
+    working_dir: /app
+    volumes:
+      - ./:/app
+    entrypoint: /app/dev/docker/entrypoint.node.sh
+  mailhog:
+    image: mailhog/mailhog
+    ports:
+      - ${DEV_MAIL_PORT:-8025}:8025
index 1a5fef175c91884031624809093d9ac089a7722a..47afc27a15d7109a2bdec012a5735641f71ef44a 100644 (file)
   "requires": true,
   "lockfileVersion": 1,
   "dependencies": {
-    "@babel/code-frame": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz",
-      "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==",
-      "dev": true,
-      "requires": {
-        "@babel/highlight": "^7.0.0"
-      }
-    },
-    "@babel/core": {
-      "version": "7.1.6",
-      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.1.6.tgz",
-      "integrity": "sha512-Hz6PJT6e44iUNpAn8AoyAs6B3bl60g7MJQaI0rZEar6ECzh6+srYO1xlIdssio34mPaUtAb1y+XlkkSJzok3yw==",
-      "dev": true,
-      "requires": {
-        "@babel/code-frame": "^7.0.0",
-        "@babel/generator": "^7.1.6",
-        "@babel/helpers": "^7.1.5",
-        "@babel/parser": "^7.1.6",
-        "@babel/template": "^7.1.2",
-        "@babel/traverse": "^7.1.6",
-        "@babel/types": "^7.1.6",
-        "convert-source-map": "^1.1.0",
-        "debug": "^4.1.0",
-        "json5": "^2.1.0",
-        "lodash": "^4.17.10",
-        "resolve": "^1.3.2",
-        "semver": "^5.4.1",
-        "source-map": "^0.5.0"
-      },
-      "dependencies": {
-        "json5": {
-          "version": "2.1.0",
-          "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.0.tgz",
-          "integrity": "sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ==",
-          "dev": true,
-          "requires": {
-            "minimist": "^1.2.0"
-          }
-        },
-        "minimist": {
-          "version": "1.2.0",
-          "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
-          "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
-          "dev": true
-        },
-        "source-map": {
-          "version": "0.5.7",
-          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
-          "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
-          "dev": true
-        }
-      }
-    },
-    "@babel/generator": {
-      "version": "7.1.6",
-      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.1.6.tgz",
-      "integrity": "sha512-brwPBtVvdYdGxtenbQgfCdDPmtkmUBZPjUoK5SXJEBuHaA5BCubh9ly65fzXz7R6o5rA76Rs22ES8Z+HCc0YIQ==",
-      "dev": true,
-      "requires": {
-        "@babel/types": "^7.1.6",
-        "jsesc": "^2.5.1",
-        "lodash": "^4.17.10",
-        "source-map": "^0.5.0",
-        "trim-right": "^1.0.1"
-      },
-      "dependencies": {
-        "source-map": {
-          "version": "0.5.7",
-          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
-          "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
-          "dev": true
-        }
-      }
-    },
-    "@babel/helper-annotate-as-pure": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.0.0.tgz",
-      "integrity": "sha512-3UYcJUj9kvSLbLbUIfQTqzcy5VX7GRZ/CCDrnOaZorFFM01aXp1+GJwuFGV4NDDoAS+mOUyHcO6UD/RfqOks3Q==",
-      "dev": true,
-      "requires": {
-        "@babel/types": "^7.0.0"
-      }
-    },
-    "@babel/helper-builder-binary-assignment-operator-visitor": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.1.0.tgz",
-      "integrity": "sha512-qNSR4jrmJ8M1VMM9tibvyRAHXQs2PmaksQF7c1CGJNipfe3D8p+wgNwgso/P2A2r2mdgBWAXljNWR0QRZAMW8w==",
-      "dev": true,
-      "requires": {
-        "@babel/helper-explode-assignable-expression": "^7.1.0",
-        "@babel/types": "^7.0.0"
-      }
-    },
-    "@babel/helper-call-delegate": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/@babel/helper-call-delegate/-/helper-call-delegate-7.1.0.tgz",
-      "integrity": "sha512-YEtYZrw3GUK6emQHKthltKNZwszBcHK58Ygcis+gVUrF4/FmTVr5CCqQNSfmvg2y+YDEANyYoaLz/SHsnusCwQ==",
-      "dev": true,
-      "requires": {
-        "@babel/helper-hoist-variables": "^7.0.0",
-        "@babel/traverse": "^7.1.0",
-        "@babel/types": "^7.0.0"
-      }
-    },
-    "@babel/helper-define-map": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.1.0.tgz",
-      "integrity": "sha512-yPPcW8dc3gZLN+U1mhYV91QU3n5uTbx7DUdf8NnPbjS0RMwBuHi9Xt2MUgppmNz7CJxTBWsGczTiEp1CSOTPRg==",
-      "dev": true,
-      "requires": {
-        "@babel/helper-function-name": "^7.1.0",
-        "@babel/types": "^7.0.0",
-        "lodash": "^4.17.10"
-      }
-    },
-    "@babel/helper-explode-assignable-expression": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.1.0.tgz",
-      "integrity": "sha512-NRQpfHrJ1msCHtKjbzs9YcMmJZOg6mQMmGRB+hbamEdG5PNpaSm95275VD92DvJKuyl0s2sFiDmMZ+EnnvufqA==",
-      "dev": true,
-      "requires": {
-        "@babel/traverse": "^7.1.0",
-        "@babel/types": "^7.0.0"
-      }
-    },
-    "@babel/helper-function-name": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz",
-      "integrity": "sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw==",
-      "dev": true,
-      "requires": {
-        "@babel/helper-get-function-arity": "^7.0.0",
-        "@babel/template": "^7.1.0",
-        "@babel/types": "^7.0.0"
-      }
-    },
-    "@babel/helper-get-function-arity": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz",
-      "integrity": "sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ==",
-      "dev": true,
-      "requires": {
-        "@babel/types": "^7.0.0"
-      }
-    },
-    "@babel/helper-hoist-variables": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.0.0.tgz",
-      "integrity": "sha512-Ggv5sldXUeSKsuzLkddtyhyHe2YantsxWKNi7A+7LeD12ExRDWTRk29JCXpaHPAbMaIPZSil7n+lq78WY2VY7w==",
-      "dev": true,
-      "requires": {
-        "@babel/types": "^7.0.0"
-      }
-    },
-    "@babel/helper-member-expression-to-functions": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.0.0.tgz",
-      "integrity": "sha512-avo+lm/QmZlv27Zsi0xEor2fKcqWG56D5ae9dzklpIaY7cQMK5N8VSpaNVPPagiqmy7LrEjK1IWdGMOqPu5csg==",
-      "dev": true,
-      "requires": {
-        "@babel/types": "^7.0.0"
-      }
-    },
-    "@babel/helper-module-imports": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.0.0.tgz",
-      "integrity": "sha512-aP/hlLq01DWNEiDg4Jn23i+CXxW/owM4WpDLFUbpjxe4NS3BhLVZQ5i7E0ZrxuQ/vwekIeciyamgB1UIYxxM6A==",
-      "dev": true,
-      "requires": {
-        "@babel/types": "^7.0.0"
-      }
-    },
-    "@babel/helper-module-transforms": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.1.0.tgz",
-      "integrity": "sha512-0JZRd2yhawo79Rcm4w0LwSMILFmFXjugG3yqf+P/UsKsRS1mJCmMwwlHDlMg7Avr9LrvSpp4ZSULO9r8jpCzcw==",
-      "dev": true,
-      "requires": {
-        "@babel/helper-module-imports": "^7.0.0",
-        "@babel/helper-simple-access": "^7.1.0",
-        "@babel/helper-split-export-declaration": "^7.0.0",
-        "@babel/template": "^7.1.0",
-        "@babel/types": "^7.0.0",
-        "lodash": "^4.17.10"
-      }
-    },
-    "@babel/helper-optimise-call-expression": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.0.0.tgz",
-      "integrity": "sha512-u8nd9NQePYNQV8iPWu/pLLYBqZBa4ZaY1YWRFMuxrid94wKI1QNt67NEZ7GAe5Kc/0LLScbim05xZFWkAdrj9g==",
-      "dev": true,
-      "requires": {
-        "@babel/types": "^7.0.0"
-      }
-    },
-    "@babel/helper-plugin-utils": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0.tgz",
-      "integrity": "sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA==",
-      "dev": true
-    },
-    "@babel/helper-regex": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/@babel/helper-regex/-/helper-regex-7.0.0.tgz",
-      "integrity": "sha512-TR0/N0NDCcUIUEbqV6dCO+LptmmSQFQ7q70lfcEB4URsjD0E1HzicrwUH+ap6BAQ2jhCX9Q4UqZy4wilujWlkg==",
-      "dev": true,
-      "requires": {
-        "lodash": "^4.17.10"
-      }
-    },
-    "@babel/helper-remap-async-to-generator": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.1.0.tgz",
-      "integrity": "sha512-3fOK0L+Fdlg8S5al8u/hWE6vhufGSn0bN09xm2LXMy//REAF8kDCrYoOBKYmA8m5Nom+sV9LyLCwrFynA8/slg==",
-      "dev": true,
-      "requires": {
-        "@babel/helper-annotate-as-pure": "^7.0.0",
-        "@babel/helper-wrap-function": "^7.1.0",
-        "@babel/template": "^7.1.0",
-        "@babel/traverse": "^7.1.0",
-        "@babel/types": "^7.0.0"
-      }
-    },
-    "@babel/helper-replace-supers": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.1.0.tgz",
-      "integrity": "sha512-BvcDWYZRWVuDeXTYZWxekQNO5D4kO55aArwZOTFXw6rlLQA8ZaDicJR1sO47h+HrnCiDFiww0fSPV0d713KBGQ==",
-      "dev": true,
-      "requires": {
-        "@babel/helper-member-expression-to-functions": "^7.0.0",
-        "@babel/helper-optimise-call-expression": "^7.0.0",
-        "@babel/traverse": "^7.1.0",
-        "@babel/types": "^7.0.0"
-      }
-    },
-    "@babel/helper-simple-access": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.1.0.tgz",
-      "integrity": "sha512-Vk+78hNjRbsiu49zAPALxTb+JUQCz1aolpd8osOF16BGnLtseD21nbHgLPGUwrXEurZgiCOUmvs3ExTu4F5x6w==",
-      "dev": true,
-      "requires": {
-        "@babel/template": "^7.1.0",
-        "@babel/types": "^7.0.0"
-      }
-    },
-    "@babel/helper-split-export-declaration": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0.tgz",
-      "integrity": "sha512-MXkOJqva62dfC0w85mEf/LucPPS/1+04nmmRMPEBUB++hiiThQ2zPtX/mEWQ3mtzCEjIJvPY8nuwxXtQeQwUag==",
-      "dev": true,
-      "requires": {
-        "@babel/types": "^7.0.0"
-      }
-    },
-    "@babel/helper-wrap-function": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.1.0.tgz",
-      "integrity": "sha512-R6HU3dete+rwsdAfrOzTlE9Mcpk4RjU3aX3gi9grtmugQY0u79X7eogUvfXA5sI81Mfq1cn6AgxihfN33STjJA==",
-      "dev": true,
-      "requires": {
-        "@babel/helper-function-name": "^7.1.0",
-        "@babel/template": "^7.1.0",
-        "@babel/traverse": "^7.1.0",
-        "@babel/types": "^7.0.0"
-      }
-    },
-    "@babel/helpers": {
-      "version": "7.1.5",
-      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.1.5.tgz",
-      "integrity": "sha512-2jkcdL02ywNBry1YNFAH/fViq4fXG0vdckHqeJk+75fpQ2OH+Az6076tX/M0835zA45E0Cqa6pV5Kiv9YOqjEg==",
-      "dev": true,
-      "requires": {
-        "@babel/template": "^7.1.2",
-        "@babel/traverse": "^7.1.5",
-        "@babel/types": "^7.1.5"
-      }
-    },
-    "@babel/highlight": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz",
-      "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==",
-      "dev": true,
-      "requires": {
-        "chalk": "^2.0.0",
-        "esutils": "^2.0.2",
-        "js-tokens": "^4.0.0"
-      }
-    },
-    "@babel/parser": {
-      "version": "7.1.6",
-      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.1.6.tgz",
-      "integrity": "sha512-dWP6LJm9nKT6ALaa+bnL247GHHMWir3vSlZ2+IHgHgktZQx0L3Uvq2uAWcuzIe+fujRsYWBW2q622C5UvGK9iQ==",
-      "dev": true
-    },
-    "@babel/plugin-proposal-async-generator-functions": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.1.0.tgz",
-      "integrity": "sha512-Fq803F3Jcxo20MXUSDdmZZXrPe6BWyGcWBPPNB/M7WaUYESKDeKMOGIxEzQOjGSmW/NWb6UaPZrtTB2ekhB/ew==",
-      "dev": true,
-      "requires": {
-        "@babel/helper-plugin-utils": "^7.0.0",
-        "@babel/helper-remap-async-to-generator": "^7.1.0",
-        "@babel/plugin-syntax-async-generators": "^7.0.0"
-      }
-    },
-    "@babel/plugin-proposal-json-strings": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.0.0.tgz",
-      "integrity": "sha512-kfVdUkIAGJIVmHmtS/40i/fg/AGnw/rsZBCaapY5yjeO5RA9m165Xbw9KMOu2nqXP5dTFjEjHdfNdoVcHv133Q==",
-      "dev": true,
-      "requires": {
-        "@babel/helper-plugin-utils": "^7.0.0",
-        "@babel/plugin-syntax-json-strings": "^7.0.0"
-      }
-    },
-    "@babel/plugin-proposal-object-rest-spread": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.0.0.tgz",
-      "integrity": "sha512-14fhfoPcNu7itSen7Py1iGN0gEm87hX/B+8nZPqkdmANyyYWYMY2pjA3r8WXbWVKMzfnSNS0xY8GVS0IjXi/iw==",
-      "dev": true,
-      "requires": {
-        "@babel/helper-plugin-utils": "^7.0.0",
-        "@babel/plugin-syntax-object-rest-spread": "^7.0.0"
-      }
-    },
-    "@babel/plugin-proposal-optional-catch-binding": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.0.0.tgz",
-      "integrity": "sha512-JPqAvLG1s13B/AuoBjdBYvn38RqW6n1TzrQO839/sIpqLpbnXKacsAgpZHzLD83Sm8SDXMkkrAvEnJ25+0yIpw==",
-      "dev": true,
-      "requires": {
-        "@babel/helper-plugin-utils": "^7.0.0",
-        "@babel/plugin-syntax-optional-catch-binding": "^7.0.0"
-      }
-    },
-    "@babel/plugin-proposal-unicode-property-regex": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.0.0.tgz",
-      "integrity": "sha512-tM3icA6GhC3ch2SkmSxv7J/hCWKISzwycub6eGsDrFDgukD4dZ/I+x81XgW0YslS6mzNuQ1Cbzh5osjIMgepPQ==",
-      "dev": true,
-      "requires": {
-        "@babel/helper-plugin-utils": "^7.0.0",
-        "@babel/helper-regex": "^7.0.0",
-        "regexpu-core": "^4.2.0"
-      }
-    },
-    "@babel/plugin-syntax-async-generators": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.0.0.tgz",
-      "integrity": "sha512-im7ged00ddGKAjcZgewXmp1vxSZQQywuQXe2B1A7kajjZmDeY/ekMPmWr9zJgveSaQH0k7BcGrojQhcK06l0zA==",
-      "dev": true,
-      "requires": {
-        "@babel/helper-plugin-utils": "^7.0.0"
-      }
-    },
-    "@babel/plugin-syntax-json-strings": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.0.0.tgz",
-      "integrity": "sha512-UlSfNydC+XLj4bw7ijpldc1uZ/HB84vw+U6BTuqMdIEmz/LDe63w/GHtpQMdXWdqQZFeAI9PjnHe/vDhwirhKA==",
-      "dev": true,
-      "requires": {
-        "@babel/helper-plugin-utils": "^7.0.0"
-      }
-    },
-    "@babel/plugin-syntax-object-rest-spread": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.0.0.tgz",
-      "integrity": "sha512-5A0n4p6bIiVe5OvQPxBnesezsgFJdHhSs3uFSvaPdMqtsovajLZ+G2vZyvNe10EzJBWWo3AcHGKhAFUxqwp2dw==",
-      "dev": true,
-      "requires": {
-        "@babel/helper-plugin-utils": "^7.0.0"
-      }
-    },
-    "@babel/plugin-syntax-optional-catch-binding": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.0.0.tgz",
-      "integrity": "sha512-Wc+HVvwjcq5qBg1w5RG9o9RVzmCaAg/Vp0erHCKpAYV8La6I94o4GQAmFYNmkzoMO6gzoOSulpKeSSz6mPEoZw==",
-      "dev": true,
-      "requires": {
-        "@babel/helper-plugin-utils": "^7.0.0"
-      }
-    },
-    "@babel/plugin-transform-arrow-functions": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.0.0.tgz",
-      "integrity": "sha512-2EZDBl1WIO/q4DIkIp4s86sdp4ZifL51MoIviLY/gG/mLSuOIEg7J8o6mhbxOTvUJkaN50n+8u41FVsr5KLy/w==",
-      "dev": true,
-      "requires": {
-        "@babel/helper-plugin-utils": "^7.0.0"
-      }
-    },
-    "@babel/plugin-transform-async-to-generator": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.1.0.tgz",
-      "integrity": "sha512-rNmcmoQ78IrvNCIt/R9U+cixUHeYAzgusTFgIAv+wQb9HJU4szhpDD6e5GCACmj/JP5KxuCwM96bX3L9v4ZN/g==",
-      "dev": true,
-      "requires": {
-        "@babel/helper-module-imports": "^7.0.0",
-        "@babel/helper-plugin-utils": "^7.0.0",
-        "@babel/helper-remap-async-to-generator": "^7.1.0"
-      }
-    },
-    "@babel/plugin-transform-block-scoped-functions": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.0.0.tgz",
-      "integrity": "sha512-AOBiyUp7vYTqz2Jibe1UaAWL0Hl9JUXEgjFvvvcSc9MVDItv46ViXFw2F7SVt1B5k+KWjl44eeXOAk3UDEaJjQ==",
-      "dev": true,
-      "requires": {
-        "@babel/helper-plugin-utils": "^7.0.0"
-      }
-    },
-    "@babel/plugin-transform-block-scoping": {
-      "version": "7.1.5",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.1.5.tgz",
-      "integrity": "sha512-jlYcDrz+5ayWC7mxgpn1Wj8zj0mmjCT2w0mPIMSwO926eXBRxpEgoN/uQVRBfjtr8ayjcmS+xk2G1jaP8JjMJQ==",
-      "dev": true,
-      "requires": {
-        "@babel/helper-plugin-utils": "^7.0.0",
-        "lodash": "^4.17.10"
-      }
-    },
-    "@babel/plugin-transform-classes": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.1.0.tgz",
-      "integrity": "sha512-rNaqoD+4OCBZjM7VaskladgqnZ1LO6o2UxuWSDzljzW21pN1KXkB7BstAVweZdxQkHAujps5QMNOTWesBciKFg==",
-      "dev": true,
-      "requires": {
-        "@babel/helper-annotate-as-pure": "^7.0.0",
-        "@babel/helper-define-map": "^7.1.0",
-        "@babel/helper-function-name": "^7.1.0",
-        "@babel/helper-optimise-call-expression": "^7.0.0",
-        "@babel/helper-plugin-utils": "^7.0.0",
-        "@babel/helper-replace-supers": "^7.1.0",
-        "@babel/helper-split-export-declaration": "^7.0.0",
-        "globals": "^11.1.0"
-      }
-    },
-    "@babel/plugin-transform-computed-properties": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.0.0.tgz",
-      "integrity": "sha512-ubouZdChNAv4AAWAgU7QKbB93NU5sHwInEWfp+/OzJKA02E6Woh9RVoX4sZrbRwtybky/d7baTUqwFx+HgbvMA==",
-      "dev": true,
-      "requires": {
-        "@babel/helper-plugin-utils": "^7.0.0"
-      }
-    },
-    "@babel/plugin-transform-destructuring": {
-      "version": "7.1.3",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.1.3.tgz",
-      "integrity": "sha512-Mb9M4DGIOspH1ExHOUnn2UUXFOyVTiX84fXCd+6B5iWrQg/QMeeRmSwpZ9lnjYLSXtZwiw80ytVMr3zue0ucYw==",
-      "dev": true,
-      "requires": {
-        "@babel/helper-plugin-utils": "^7.0.0"
-      }
-    },
-    "@babel/plugin-transform-dotall-regex": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.0.0.tgz",
-      "integrity": "sha512-00THs8eJxOJUFVx1w8i1MBF4XH4PsAjKjQ1eqN/uCH3YKwP21GCKfrn6YZFZswbOk9+0cw1zGQPHVc1KBlSxig==",
-      "dev": true,
-      "requires": {
-        "@babel/helper-plugin-utils": "^7.0.0",
-        "@babel/helper-regex": "^7.0.0",
-        "regexpu-core": "^4.1.3"
-      }
-    },
-    "@babel/plugin-transform-duplicate-keys": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.0.0.tgz",
-      "integrity": "sha512-w2vfPkMqRkdxx+C71ATLJG30PpwtTpW7DDdLqYt2acXU7YjztzeWW2Jk1T6hKqCLYCcEA5UQM/+xTAm+QCSnuQ==",
-      "dev": true,
-      "requires": {
-        "@babel/helper-plugin-utils": "^7.0.0"
-      }
-    },
-    "@babel/plugin-transform-exponentiation-operator": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.1.0.tgz",
-      "integrity": "sha512-uZt9kD1Pp/JubkukOGQml9tqAeI8NkE98oZnHZ2qHRElmeKCodbTZgOEUtujSCSLhHSBWbzNiFSDIMC4/RBTLQ==",
-      "dev": true,
-      "requires": {
-        "@babel/helper-builder-binary-assignment-operator-visitor": "^7.1.0",
-        "@babel/helper-plugin-utils": "^7.0.0"
-      }
-    },
-    "@babel/plugin-transform-for-of": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.0.0.tgz",
-      "integrity": "sha512-TlxKecN20X2tt2UEr2LNE6aqA0oPeMT1Y3cgz8k4Dn1j5ObT8M3nl9aA37LLklx0PBZKETC9ZAf9n/6SujTuXA==",
-      "dev": true,
-      "requires": {
-        "@babel/helper-plugin-utils": "^7.0.0"
-      }
-    },
-    "@babel/plugin-transform-function-name": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.1.0.tgz",
-      "integrity": "sha512-VxOa1TMlFMtqPW2IDYZQaHsFrq/dDoIjgN098NowhexhZcz3UGlvPgZXuE1jEvNygyWyxRacqDpCZt+par1FNg==",
-      "dev": true,
-      "requires": {
-        "@babel/helper-function-name": "^7.1.0",
-        "@babel/helper-plugin-utils": "^7.0.0"
-      }
-    },
-    "@babel/plugin-transform-literals": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.0.0.tgz",
-      "integrity": "sha512-1NTDBWkeNXgpUcyoVFxbr9hS57EpZYXpje92zv0SUzjdu3enaRwF/l3cmyRnXLtIdyJASyiS6PtybK+CgKf7jA==",
-      "dev": true,
-      "requires": {
-        "@babel/helper-plugin-utils": "^7.0.0"
-      }
-    },
-    "@babel/plugin-transform-modules-amd": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.1.0.tgz",
-      "integrity": "sha512-wt8P+xQ85rrnGNr2x1iV3DW32W8zrB6ctuBkYBbf5/ZzJY99Ob4MFgsZDFgczNU76iy9PWsy4EuxOliDjdKw6A==",
-      "dev": true,
-      "requires": {
-        "@babel/helper-module-transforms": "^7.1.0",
-        "@babel/helper-plugin-utils": "^7.0.0"
-      }
-    },
-    "@babel/plugin-transform-modules-commonjs": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.1.0.tgz",
-      "integrity": "sha512-wtNwtMjn1XGwM0AXPspQgvmE6msSJP15CX2RVfpTSTNPLhKhaOjaIfBaVfj4iUZ/VrFSodcFedwtPg/NxwQlPA==",
-      "dev": true,
-      "requires": {
-        "@babel/helper-module-transforms": "^7.1.0",
-        "@babel/helper-plugin-utils": "^7.0.0",
-        "@babel/helper-simple-access": "^7.1.0"
-      }
-    },
-    "@babel/plugin-transform-modules-systemjs": {
-      "version": "7.1.3",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.1.3.tgz",
-      "integrity": "sha512-PvTxgjxQAq4pvVUZF3mD5gEtVDuId8NtWkJsZLEJZMZAW3TvgQl1pmydLLN1bM8huHFVVU43lf0uvjQj9FRkKw==",
-      "dev": true,
-      "requires": {
-        "@babel/helper-hoist-variables": "^7.0.0",
-        "@babel/helper-plugin-utils": "^7.0.0"
-      }
-    },
-    "@babel/plugin-transform-modules-umd": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.1.0.tgz",
-      "integrity": "sha512-enrRtn5TfRhMmbRwm7F8qOj0qEYByqUvTttPEGimcBH4CJHphjyK1Vg7sdU7JjeEmgSpM890IT/efS2nMHwYig==",
-      "dev": true,
-      "requires": {
-        "@babel/helper-module-transforms": "^7.1.0",
-        "@babel/helper-plugin-utils": "^7.0.0"
-      }
-    },
-    "@babel/plugin-transform-new-target": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.0.0.tgz",
-      "integrity": "sha512-yin069FYjah+LbqfGeTfzIBODex/e++Yfa0rH0fpfam9uTbuEeEOx5GLGr210ggOV77mVRNoeqSYqeuaqSzVSw==",
-      "dev": true,
-      "requires": {
-        "@babel/helper-plugin-utils": "^7.0.0"
-      }
-    },
-    "@babel/plugin-transform-object-super": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.1.0.tgz",
-      "integrity": "sha512-/O02Je1CRTSk2SSJaq0xjwQ8hG4zhZGNjE8psTsSNPXyLRCODv7/PBozqT5AmQMzp7MI3ndvMhGdqp9c96tTEw==",
-      "dev": true,
-      "requires": {
-        "@babel/helper-plugin-utils": "^7.0.0",
-        "@babel/helper-replace-supers": "^7.1.0"
-      }
-    },
-    "@babel/plugin-transform-parameters": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.1.0.tgz",
-      "integrity": "sha512-vHV7oxkEJ8IHxTfRr3hNGzV446GAb+0hgbA7o/0Jd76s+YzccdWuTU296FOCOl/xweU4t/Ya4g41yWz80RFCRw==",
-      "dev": true,
-      "requires": {
-        "@babel/helper-call-delegate": "^7.1.0",
-        "@babel/helper-get-function-arity": "^7.0.0",
-        "@babel/helper-plugin-utils": "^7.0.0"
-      }
-    },
-    "@babel/plugin-transform-regenerator": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.0.0.tgz",
-      "integrity": "sha512-sj2qzsEx8KDVv1QuJc/dEfilkg3RRPvPYx/VnKLtItVQRWt1Wqf5eVCOLZm29CiGFfYYsA3VPjfizTCV0S0Dlw==",
-      "dev": true,
-      "requires": {
-        "regenerator-transform": "^0.13.3"
-      }
-    },
-    "@babel/plugin-transform-shorthand-properties": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.0.0.tgz",
-      "integrity": "sha512-g/99LI4vm5iOf5r1Gdxq5Xmu91zvjhEG5+yZDJW268AZELAu4J1EiFLnkSG3yuUsZyOipVOVUKoGPYwfsTymhw==",
-      "dev": true,
-      "requires": {
-        "@babel/helper-plugin-utils": "^7.0.0"
-      }
-    },
-    "@babel/plugin-transform-spread": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.0.0.tgz",
-      "integrity": "sha512-L702YFy2EvirrR4shTj0g2xQp7aNwZoWNCkNu2mcoU0uyzMl0XRwDSwzB/xp6DSUFiBmEXuyAyEN16LsgVqGGQ==",
-      "dev": true,
-      "requires": {
-        "@babel/helper-plugin-utils": "^7.0.0"
-      }
-    },
-    "@babel/plugin-transform-sticky-regex": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.0.0.tgz",
-      "integrity": "sha512-LFUToxiyS/WD+XEWpkx/XJBrUXKewSZpzX68s+yEOtIbdnsRjpryDw9U06gYc6klYEij/+KQVRnD3nz3AoKmjw==",
-      "dev": true,
-      "requires": {
-        "@babel/helper-plugin-utils": "^7.0.0",
-        "@babel/helper-regex": "^7.0.0"
-      }
-    },
-    "@babel/plugin-transform-template-literals": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.0.0.tgz",
-      "integrity": "sha512-vA6rkTCabRZu7Nbl9DfLZE1imj4tzdWcg5vtdQGvj+OH9itNNB6hxuRMHuIY8SGnEt1T9g5foqs9LnrHzsqEFg==",
-      "dev": true,
-      "requires": {
-        "@babel/helper-annotate-as-pure": "^7.0.0",
-        "@babel/helper-plugin-utils": "^7.0.0"
-      }
-    },
-    "@babel/plugin-transform-typeof-symbol": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.0.0.tgz",
-      "integrity": "sha512-1r1X5DO78WnaAIvs5uC48t41LLckxsYklJrZjNKcevyz83sF2l4RHbw29qrCPr/6ksFsdfRpT/ZgxNWHXRnffg==",
-      "dev": true,
-      "requires": {
-        "@babel/helper-plugin-utils": "^7.0.0"
-      }
-    },
-    "@babel/plugin-transform-unicode-regex": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.0.0.tgz",
-      "integrity": "sha512-uJBrJhBOEa3D033P95nPHu3nbFwFE9ZgXsfEitzoIXIwqAZWk7uXcg06yFKXz9FSxBH5ucgU/cYdX0IV8ldHKw==",
-      "dev": true,
-      "requires": {
-        "@babel/helper-plugin-utils": "^7.0.0",
-        "@babel/helper-regex": "^7.0.0",
-        "regexpu-core": "^4.1.3"
-      }
-    },
-    "@babel/polyfill": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/@babel/polyfill/-/polyfill-7.0.0.tgz",
-      "integrity": "sha512-dnrMRkyyr74CRelJwvgnnSUDh2ge2NCTyHVwpOdvRMHtJUyxLtMAfhBN3s64pY41zdw0kgiLPh6S20eb1NcX6Q==",
-      "dev": true,
-      "requires": {
-        "core-js": "^2.5.7",
-        "regenerator-runtime": "^0.11.1"
-      }
-    },
-    "@babel/preset-env": {
-      "version": "7.1.6",
-      "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.1.6.tgz",
-      "integrity": "sha512-YIBfpJNQMBkb6MCkjz/A9J76SNCSuGVamOVBgoUkLzpJD/z8ghHi9I42LQ4pulVX68N/MmImz6ZTixt7Azgexw==",
-      "dev": true,
-      "requires": {
-        "@babel/helper-module-imports": "^7.0.0",
-        "@babel/helper-plugin-utils": "^7.0.0",
-        "@babel/plugin-proposal-async-generator-functions": "^7.1.0",
-        "@babel/plugin-proposal-json-strings": "^7.0.0",
-        "@babel/plugin-proposal-object-rest-spread": "^7.0.0",
-        "@babel/plugin-proposal-optional-catch-binding": "^7.0.0",
-        "@babel/plugin-proposal-unicode-property-regex": "^7.0.0",
-        "@babel/plugin-syntax-async-generators": "^7.0.0",
-        "@babel/plugin-syntax-object-rest-spread": "^7.0.0",
-        "@babel/plugin-syntax-optional-catch-binding": "^7.0.0",
-        "@babel/plugin-transform-arrow-functions": "^7.0.0",
-        "@babel/plugin-transform-async-to-generator": "^7.1.0",
-        "@babel/plugin-transform-block-scoped-functions": "^7.0.0",
-        "@babel/plugin-transform-block-scoping": "^7.1.5",
-        "@babel/plugin-transform-classes": "^7.1.0",
-        "@babel/plugin-transform-computed-properties": "^7.0.0",
-        "@babel/plugin-transform-destructuring": "^7.0.0",
-        "@babel/plugin-transform-dotall-regex": "^7.0.0",
-        "@babel/plugin-transform-duplicate-keys": "^7.0.0",
-        "@babel/plugin-transform-exponentiation-operator": "^7.1.0",
-        "@babel/plugin-transform-for-of": "^7.0.0",
-        "@babel/plugin-transform-function-name": "^7.1.0",
-        "@babel/plugin-transform-literals": "^7.0.0",
-        "@babel/plugin-transform-modules-amd": "^7.1.0",
-        "@babel/plugin-transform-modules-commonjs": "^7.1.0",
-        "@babel/plugin-transform-modules-systemjs": "^7.0.0",
-        "@babel/plugin-transform-modules-umd": "^7.1.0",
-        "@babel/plugin-transform-new-target": "^7.0.0",
-        "@babel/plugin-transform-object-super": "^7.1.0",
-        "@babel/plugin-transform-parameters": "^7.1.0",
-        "@babel/plugin-transform-regenerator": "^7.0.0",
-        "@babel/plugin-transform-shorthand-properties": "^7.0.0",
-        "@babel/plugin-transform-spread": "^7.0.0",
-        "@babel/plugin-transform-sticky-regex": "^7.0.0",
-        "@babel/plugin-transform-template-literals": "^7.0.0",
-        "@babel/plugin-transform-typeof-symbol": "^7.0.0",
-        "@babel/plugin-transform-unicode-regex": "^7.0.0",
-        "browserslist": "^4.1.0",
-        "invariant": "^2.2.2",
-        "js-levenshtein": "^1.1.3",
-        "semver": "^5.3.0"
-      }
-    },
-    "@babel/template": {
-      "version": "7.1.2",
-      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.1.2.tgz",
-      "integrity": "sha512-SY1MmplssORfFiLDcOETrW7fCLl+PavlwMh92rrGcikQaRq4iWPVH0MpwPpY3etVMx6RnDjXtr6VZYr/IbP/Ag==",
-      "dev": true,
-      "requires": {
-        "@babel/code-frame": "^7.0.0",
-        "@babel/parser": "^7.1.2",
-        "@babel/types": "^7.1.2"
-      }
-    },
-    "@babel/traverse": {
-      "version": "7.1.6",
-      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.1.6.tgz",
-      "integrity": "sha512-CXedit6GpISz3sC2k2FsGCUpOhUqKdyL0lqNrImQojagnUMXf8hex4AxYFRuMkNGcvJX5QAFGzB5WJQmSv8SiQ==",
-      "dev": true,
-      "requires": {
-        "@babel/code-frame": "^7.0.0",
-        "@babel/generator": "^7.1.6",
-        "@babel/helper-function-name": "^7.1.0",
-        "@babel/helper-split-export-declaration": "^7.0.0",
-        "@babel/parser": "^7.1.6",
-        "@babel/types": "^7.1.6",
-        "debug": "^4.1.0",
-        "globals": "^11.1.0",
-        "lodash": "^4.17.10"
-      }
-    },
-    "@babel/types": {
-      "version": "7.1.6",
-      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.1.6.tgz",
-      "integrity": "sha512-DMiUzlY9DSjVsOylJssxLHSgj6tWM9PRFJOGW/RaOglVOK9nzTxoOMfTfRQXGUCUQ/HmlG2efwC+XqUEJ5ay4w==",
-      "dev": true,
-      "requires": {
-        "esutils": "^2.0.2",
-        "lodash": "^4.17.10",
-        "to-fast-properties": "^2.0.0"
-      }
-    },
     "@webassemblyjs/ast": {
-      "version": "1.7.11",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.7.11.tgz",
-      "integrity": "sha512-ZEzy4vjvTzScC+SH8RBssQUawpaInUdMTYwYYLh54/s8TuT0gBLuyUnppKsVyZEi876VmmStKsUs28UxPgdvrA==",
+      "version": "1.8.5",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz",
+      "integrity": "sha512-aJMfngIZ65+t71C3y2nBBg5FFG0Okt9m0XEgWZ7Ywgn1oMAT8cNwx00Uv1cQyHtidq0Xn94R4TAywO+LCQ+ZAQ==",
       "dev": true,
       "requires": {
-        "@webassemblyjs/helper-module-context": "1.7.11",
-        "@webassemblyjs/helper-wasm-bytecode": "1.7.11",
-        "@webassemblyjs/wast-parser": "1.7.11"
+        "@webassemblyjs/helper-module-context": "1.8.5",
+        "@webassemblyjs/helper-wasm-bytecode": "1.8.5",
+        "@webassemblyjs/wast-parser": "1.8.5"
       }
     },
     "@webassemblyjs/floating-point-hex-parser": {
-      "version": "1.7.11",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.7.11.tgz",
-      "integrity": "sha512-zY8dSNyYcgzNRNT666/zOoAyImshm3ycKdoLsyDw/Bwo6+/uktb7p4xyApuef1dwEBo/U/SYQzbGBvV+nru2Xg==",
+      "version": "1.8.5",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.8.5.tgz",
+      "integrity": "sha512-9p+79WHru1oqBh9ewP9zW95E3XAo+90oth7S5Re3eQnECGq59ly1Ri5tsIipKGpiStHsUYmY3zMLqtk3gTcOtQ==",
       "dev": true
     },
     "@webassemblyjs/helper-api-error": {
-      "version": "1.7.11",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.7.11.tgz",
-      "integrity": "sha512-7r1qXLmiglC+wPNkGuXCvkmalyEstKVwcueZRP2GNC2PAvxbLYwLLPr14rcdJaE4UtHxQKfFkuDFuv91ipqvXg==",
+      "version": "1.8.5",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.8.5.tgz",
+      "integrity": "sha512-Za/tnzsvnqdaSPOUXHyKJ2XI7PDX64kWtURyGiJJZKVEdFOsdKUCPTNEVFZq3zJ2R0G5wc2PZ5gvdTRFgm81zA==",
       "dev": true
     },
     "@webassemblyjs/helper-buffer": {
-      "version": "1.7.11",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.7.11.tgz",
-      "integrity": "sha512-MynuervdylPPh3ix+mKZloTcL06P8tenNH3sx6s0qE8SLR6DdwnfgA7Hc9NSYeob2jrW5Vql6GVlsQzKQCa13w==",
+      "version": "1.8.5",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.8.5.tgz",
+      "integrity": "sha512-Ri2R8nOS0U6G49Q86goFIPNgjyl6+oE1abW1pS84BuhP1Qcr5JqMwRFT3Ah3ADDDYGEgGs1iyb1DGX+kAi/c/Q==",
       "dev": true
     },
     "@webassemblyjs/helper-code-frame": {
-      "version": "1.7.11",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.7.11.tgz",
-      "integrity": "sha512-T8ESC9KMXFTXA5urJcyor5cn6qWeZ4/zLPyWeEXZ03hj/x9weSokGNkVCdnhSabKGYWxElSdgJ+sFa9G/RdHNw==",
+      "version": "1.8.5",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.8.5.tgz",
+      "integrity": "sha512-VQAadSubZIhNpH46IR3yWO4kZZjMxN1opDrzePLdVKAZ+DFjkGD/rf4v1jap744uPVU6yjL/smZbRIIJTOUnKQ==",
       "dev": true,
       "requires": {
-        "@webassemblyjs/wast-printer": "1.7.11"
+        "@webassemblyjs/wast-printer": "1.8.5"
       }
     },
     "@webassemblyjs/helper-fsm": {
-      "version": "1.7.11",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.7.11.tgz",
-      "integrity": "sha512-nsAQWNP1+8Z6tkzdYlXT0kxfa2Z1tRTARd8wYnc/e3Zv3VydVVnaeePgqUzFrpkGUyhUUxOl5ML7f1NuT+gC0A==",
+      "version": "1.8.5",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.8.5.tgz",
+      "integrity": "sha512-kRuX/saORcg8se/ft6Q2UbRpZwP4y7YrWsLXPbbmtepKr22i8Z4O3V5QE9DbZK908dh5Xya4Un57SDIKwB9eow==",
       "dev": true
     },
     "@webassemblyjs/helper-module-context": {
-      "version": "1.7.11",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.7.11.tgz",
-      "integrity": "sha512-JxfD5DX8Ygq4PvXDucq0M+sbUFA7BJAv/GGl9ITovqE+idGX+J3QSzJYz+LwQmL7fC3Rs+utvWoJxDb6pmC0qg==",
-      "dev": true
+      "version": "1.8.5",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.8.5.tgz",
+      "integrity": "sha512-/O1B236mN7UNEU4t9X7Pj38i4VoU8CcMHyy3l2cV/kIF4U5KoHXDVqcDuOs1ltkac90IM4vZdHc52t1x8Yfs3g==",
+      "dev": true,
+      "requires": {
+        "@webassemblyjs/ast": "1.8.5",
+        "mamacro": "^0.0.3"
+      }
     },
     "@webassemblyjs/helper-wasm-bytecode": {
-      "version": "1.7.11",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.7.11.tgz",
-      "integrity": "sha512-cMXeVS9rhoXsI9LLL4tJxBgVD/KMOKXuFqYb5oCJ/opScWpkCMEz9EJtkonaNcnLv2R3K5jIeS4TRj/drde1JQ==",
+      "version": "1.8.5",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.8.5.tgz",
+      "integrity": "sha512-Cu4YMYG3Ddl72CbmpjU/wbP6SACcOPVbHN1dI4VJNJVgFwaKf1ppeFJrwydOG3NDHxVGuCfPlLZNyEdIYlQ6QQ==",
       "dev": true
     },
     "@webassemblyjs/helper-wasm-section": {
-      "version": "1.7.11",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.7.11.tgz",
-      "integrity": "sha512-8ZRY5iZbZdtNFE5UFunB8mmBEAbSI3guwbrsCl4fWdfRiAcvqQpeqd5KHhSWLL5wuxo53zcaGZDBU64qgn4I4Q==",
+      "version": "1.8.5",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.8.5.tgz",
+      "integrity": "sha512-VV083zwR+VTrIWWtgIUpqfvVdK4ff38loRmrdDBgBT8ADXYsEZ5mPQ4Nde90N3UYatHdYoDIFb7oHzMncI02tA==",
       "dev": true,
       "requires": {
-        "@webassemblyjs/ast": "1.7.11",
-        "@webassemblyjs/helper-buffer": "1.7.11",
-        "@webassemblyjs/helper-wasm-bytecode": "1.7.11",
-        "@webassemblyjs/wasm-gen": "1.7.11"
+        "@webassemblyjs/ast": "1.8.5",
+        "@webassemblyjs/helper-buffer": "1.8.5",
+        "@webassemblyjs/helper-wasm-bytecode": "1.8.5",
+        "@webassemblyjs/wasm-gen": "1.8.5"
       }
     },
     "@webassemblyjs/ieee754": {
-      "version": "1.7.11",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.7.11.tgz",
-      "integrity": "sha512-Mmqx/cS68K1tSrvRLtaV/Lp3NZWzXtOHUW2IvDvl2sihAwJh4ACE0eL6A8FvMyDG9abes3saB6dMimLOs+HMoQ==",
+      "version": "1.8.5",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.8.5.tgz",
+      "integrity": "sha512-aaCvQYrvKbY/n6wKHb/ylAJr27GglahUO89CcGXMItrOBqRarUMxWLJgxm9PJNuKULwN5n1csT9bYoMeZOGF3g==",
       "dev": true,
       "requires": {
         "@xtuc/ieee754": "^1.2.0"
       }
     },
     "@webassemblyjs/leb128": {
-      "version": "1.7.11",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.7.11.tgz",
-      "integrity": "sha512-vuGmgZjjp3zjcerQg+JA+tGOncOnJLWVkt8Aze5eWQLwTQGNgVLcyOTqgSCxWTR4J42ijHbBxnuRaL1Rv7XMdw==",
+      "version": "1.8.5",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.8.5.tgz",
+      "integrity": "sha512-plYUuUwleLIziknvlP8VpTgO4kqNaH57Y3JnNa6DLpu/sGcP6hbVdfdX5aHAV716pQBKrfuU26BJK29qY37J7A==",
       "dev": true,
       "requires": {
-        "@xtuc/long": "4.2.1"
+        "@xtuc/long": "4.2.2"
       }
     },
     "@webassemblyjs/utf8": {
-      "version": "1.7.11",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.7.11.tgz",
-      "integrity": "sha512-C6GFkc7aErQIAH+BMrIdVSmW+6HSe20wg57HEC1uqJP8E/xpMjXqQUxkQw07MhNDSDcGpxI9G5JSNOQCqJk4sA==",
+      "version": "1.8.5",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.8.5.tgz",
+      "integrity": "sha512-U7zgftmQriw37tfD934UNInokz6yTmn29inT2cAetAsaU9YeVCveWEwhKL1Mg4yS7q//NGdzy79nlXh3bT8Kjw==",
       "dev": true
     },
     "@webassemblyjs/wasm-edit": {
-      "version": "1.7.11",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.7.11.tgz",
-      "integrity": "sha512-FUd97guNGsCZQgeTPKdgxJhBXkUbMTY6hFPf2Y4OedXd48H97J+sOY2Ltaq6WGVpIH8o/TGOVNiVz/SbpEMJGg==",
+      "version": "1.8.5",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.8.5.tgz",
+      "integrity": "sha512-A41EMy8MWw5yvqj7MQzkDjU29K7UJq1VrX2vWLzfpRHt3ISftOXqrtojn7nlPsZ9Ijhp5NwuODuycSvfAO/26Q==",
       "dev": true,
       "requires": {
-        "@webassemblyjs/ast": "1.7.11",
-        "@webassemblyjs/helper-buffer": "1.7.11",
-        "@webassemblyjs/helper-wasm-bytecode": "1.7.11",
-        "@webassemblyjs/helper-wasm-section": "1.7.11",
-        "@webassemblyjs/wasm-gen": "1.7.11",
-        "@webassemblyjs/wasm-opt": "1.7.11",
-        "@webassemblyjs/wasm-parser": "1.7.11",
-        "@webassemblyjs/wast-printer": "1.7.11"
+        "@webassemblyjs/ast": "1.8.5",
+        "@webassemblyjs/helper-buffer": "1.8.5",
+        "@webassemblyjs/helper-wasm-bytecode": "1.8.5",
+        "@webassemblyjs/helper-wasm-section": "1.8.5",
+        "@webassemblyjs/wasm-gen": "1.8.5",
+        "@webassemblyjs/wasm-opt": "1.8.5",
+        "@webassemblyjs/wasm-parser": "1.8.5",
+        "@webassemblyjs/wast-printer": "1.8.5"
       }
     },
     "@webassemblyjs/wasm-gen": {
-      "version": "1.7.11",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.7.11.tgz",
-      "integrity": "sha512-U/KDYp7fgAZX5KPfq4NOupK/BmhDc5Kjy2GIqstMhvvdJRcER/kUsMThpWeRP8BMn4LXaKhSTggIJPOeYHwISA==",
+      "version": "1.8.5",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.8.5.tgz",
+      "integrity": "sha512-BCZBT0LURC0CXDzj5FXSc2FPTsxwp3nWcqXQdOZE4U7h7i8FqtFK5Egia6f9raQLpEKT1VL7zr4r3+QX6zArWg==",
       "dev": true,
       "requires": {
-        "@webassemblyjs/ast": "1.7.11",
-        "@webassemblyjs/helper-wasm-bytecode": "1.7.11",
-        "@webassemblyjs/ieee754": "1.7.11",
-        "@webassemblyjs/leb128": "1.7.11",
-        "@webassemblyjs/utf8": "1.7.11"
+        "@webassemblyjs/ast": "1.8.5",
+        "@webassemblyjs/helper-wasm-bytecode": "1.8.5",
+        "@webassemblyjs/ieee754": "1.8.5",
+        "@webassemblyjs/leb128": "1.8.5",
+        "@webassemblyjs/utf8": "1.8.5"
       }
     },
     "@webassemblyjs/wasm-opt": {
-      "version": "1.7.11",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.7.11.tgz",
-      "integrity": "sha512-XynkOwQyiRidh0GLua7SkeHvAPXQV/RxsUeERILmAInZegApOUAIJfRuPYe2F7RcjOC9tW3Cb9juPvAC/sCqvg==",
+      "version": "1.8.5",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.8.5.tgz",
+      "integrity": "sha512-HKo2mO/Uh9A6ojzu7cjslGaHaUU14LdLbGEKqTR7PBKwT6LdPtLLh9fPY33rmr5wcOMrsWDbbdCHq4hQUdd37Q==",
       "dev": true,
       "requires": {
-        "@webassemblyjs/ast": "1.7.11",
-        "@webassemblyjs/helper-buffer": "1.7.11",
-        "@webassemblyjs/wasm-gen": "1.7.11",
-        "@webassemblyjs/wasm-parser": "1.7.11"
+        "@webassemblyjs/ast": "1.8.5",
+        "@webassemblyjs/helper-buffer": "1.8.5",
+        "@webassemblyjs/wasm-gen": "1.8.5",
+        "@webassemblyjs/wasm-parser": "1.8.5"
       }
     },
     "@webassemblyjs/wasm-parser": {
-      "version": "1.7.11",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.7.11.tgz",
-      "integrity": "sha512-6lmXRTrrZjYD8Ng8xRyvyXQJYUQKYSXhJqXOBLw24rdiXsHAOlvw5PhesjdcaMadU/pyPQOJ5dHreMjBxwnQKg==",
+      "version": "1.8.5",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.8.5.tgz",
+      "integrity": "sha512-pi0SYE9T6tfcMkthwcgCpL0cM9nRYr6/6fjgDtL6q/ZqKHdMWvxitRi5JcZ7RI4SNJJYnYNaWy5UUrHQy998lw==",
       "dev": true,
       "requires": {
-        "@webassemblyjs/ast": "1.7.11",
-        "@webassemblyjs/helper-api-error": "1.7.11",
-        "@webassemblyjs/helper-wasm-bytecode": "1.7.11",
-        "@webassemblyjs/ieee754": "1.7.11",
-        "@webassemblyjs/leb128": "1.7.11",
-        "@webassemblyjs/utf8": "1.7.11"
+        "@webassemblyjs/ast": "1.8.5",
+        "@webassemblyjs/helper-api-error": "1.8.5",
+        "@webassemblyjs/helper-wasm-bytecode": "1.8.5",
+        "@webassemblyjs/ieee754": "1.8.5",
+        "@webassemblyjs/leb128": "1.8.5",
+        "@webassemblyjs/utf8": "1.8.5"
       }
     },
     "@webassemblyjs/wast-parser": {
-      "version": "1.7.11",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.7.11.tgz",
-      "integrity": "sha512-lEyVCg2np15tS+dm7+JJTNhNWq9yTZvi3qEhAIIOaofcYlUp0UR5/tVqOwa/gXYr3gjwSZqw+/lS9dscyLelbQ==",
+      "version": "1.8.5",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.8.5.tgz",
+      "integrity": "sha512-daXC1FyKWHF1i11obK086QRlsMsY4+tIOKgBqI1lxAnkp9xe9YMcgOxm9kLe+ttjs5aWV2KKE1TWJCN57/Btsg==",
       "dev": true,
       "requires": {
-        "@webassemblyjs/ast": "1.7.11",
-        "@webassemblyjs/floating-point-hex-parser": "1.7.11",
-        "@webassemblyjs/helper-api-error": "1.7.11",
-        "@webassemblyjs/helper-code-frame": "1.7.11",
-        "@webassemblyjs/helper-fsm": "1.7.11",
-        "@xtuc/long": "4.2.1"
+        "@webassemblyjs/ast": "1.8.5",
+        "@webassemblyjs/floating-point-hex-parser": "1.8.5",
+        "@webassemblyjs/helper-api-error": "1.8.5",
+        "@webassemblyjs/helper-code-frame": "1.8.5",
+        "@webassemblyjs/helper-fsm": "1.8.5",
+        "@xtuc/long": "4.2.2"
       }
     },
     "@webassemblyjs/wast-printer": {
-      "version": "1.7.11",
-      "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.7.11.tgz",
-      "integrity": "sha512-m5vkAsuJ32QpkdkDOUPGSltrg8Cuk3KBx4YrmAGQwCZPRdUHXxG4phIOuuycLemHFr74sWL9Wthqss4fzdzSwg==",
+      "version": "1.8.5",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.8.5.tgz",
+      "integrity": "sha512-w0U0pD4EhlnvRyeJzBqaVSJAo9w/ce7/WPogeXLzGkO6hzhr4GnQIZ4W4uUt5b9ooAaXPtnXlj0gzsXEOUNYMg==",
       "dev": true,
       "requires": {
-        "@webassemblyjs/ast": "1.7.11",
-        "@webassemblyjs/wast-parser": "1.7.11",
-        "@xtuc/long": "4.2.1"
+        "@webassemblyjs/ast": "1.8.5",
+        "@webassemblyjs/wast-parser": "1.8.5",
+        "@xtuc/long": "4.2.2"
       }
     },
     "@xtuc/ieee754": {
       "dev": true
     },
     "@xtuc/long": {
-      "version": "4.2.1",
-      "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.1.tgz",
-      "integrity": "sha512-FZdkNBDqBRHKQ2MEbSC17xnPFOhZxeJ2YGSfr2BKf3sujG49Qe3bB+rGCwQfIaA7WHnGeGkSijX4FuBCdrzW/g==",
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz",
+      "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
       "dev": true
     },
     "abbrev": {
       "dev": true
     },
     "acorn": {
-      "version": "5.7.3",
-      "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz",
-      "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==",
+      "version": "6.1.1",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.1.1.tgz",
+      "integrity": "sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA==",
       "dev": true
     },
     "acorn-dynamic-import": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-3.0.0.tgz",
-      "integrity": "sha512-zVWV8Z8lislJoOKKqdNMOB+s6+XV5WERty8MnKBeFgwA+19XJjJHs2RP5dzM57FftIs+jQnRToLiWazKr6sSWg==",
-      "dev": true,
-      "requires": {
-        "acorn": "^5.0.0"
-      }
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz",
+      "integrity": "sha512-d3OEjQV4ROpoflsnUA8HozoIR504TFxNivYEUi6uwz0IYhBkTDXGuWlNdMtybRt3nqVx/L6XqMt0FxkXuWKZhw==",
+      "dev": true
     },
     "ajv": {
-      "version": "5.5.2",
-      "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz",
-      "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=",
+      "version": "6.10.0",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz",
+      "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==",
       "dev": true,
       "requires": {
-        "co": "^4.6.0",
-        "fast-deep-equal": "^1.0.0",
+        "fast-deep-equal": "^2.0.1",
         "fast-json-stable-stringify": "^2.0.0",
-        "json-schema-traverse": "^0.3.0"
+        "json-schema-traverse": "^0.4.1",
+        "uri-js": "^4.2.2"
       }
     },
     "ajv-errors": {
       }
     },
     "anymatch": {
-      "version": "1.3.2",
-      "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz",
-      "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==",
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz",
+      "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==",
       "dev": true,
       "requires": {
-        "micromatch": "^2.1.5",
-        "normalize-path": "^2.0.0"
+        "micromatch": "^3.1.4",
+        "normalize-path": "^2.1.1"
+      },
+      "dependencies": {
+        "normalize-path": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
+          "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=",
+          "dev": true,
+          "requires": {
+            "remove-trailing-separator": "^1.0.1"
+          }
+        }
       }
     },
     "aproba": {
       }
     },
     "arr-diff": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz",
-      "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=",
-      "dev": true,
-      "requires": {
-        "arr-flatten": "^1.0.1"
-      }
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
+      "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=",
+      "dev": true
     },
     "arr-flatten": {
       "version": "1.1.0",
       "dev": true
     },
     "array-unique": {
-      "version": "0.2.1",
-      "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz",
-      "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=",
+      "version": "0.3.2",
+      "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz",
+      "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=",
       "dev": true
     },
     "asn1": {
       }
     },
     "assert": {
-      "version": "1.4.1",
-      "resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz",
-      "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=",
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz",
+      "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==",
       "dev": true,
       "requires": {
+        "object-assign": "^4.1.1",
         "util": "0.10.3"
       },
       "dependencies": {
       "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
       "dev": true
     },
-    "autoprefixer": {
-      "version": "9.4.7",
-      "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.4.7.tgz",
-      "integrity": "sha512-qS5wW6aXHkm53Y4z73tFGsUhmZu4aMPV9iHXYlF0c/wxjknXNHuj/1cIQb+6YH692DbJGGWcckAXX+VxKvahMA==",
-      "dev": true,
-      "requires": {
-        "browserslist": "^4.4.1",
-        "caniuse-lite": "^1.0.30000932",
-        "normalize-range": "^0.1.2",
-        "num2fraction": "^1.2.2",
-        "postcss": "^7.0.14",
-        "postcss-value-parser": "^3.3.1"
-      },
-      "dependencies": {
-        "browserslist": {
-          "version": "4.4.1",
-          "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.4.1.tgz",
-          "integrity": "sha512-pEBxEXg7JwaakBXjATYw/D1YZh4QUSCX/Mnd/wnqSRPPSi1U39iDhDoKGoBUcraKdxDlrYqJxSI5nNvD+dWP2A==",
-          "dev": true,
-          "requires": {
-            "caniuse-lite": "^1.0.30000929",
-            "electron-to-chromium": "^1.3.103",
-            "node-releases": "^1.1.3"
-          }
-        },
-        "caniuse-lite": {
-          "version": "1.0.30000934",
-          "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000934.tgz",
-          "integrity": "sha512-o7yfZn0R9N+mWAuksDsdLsb1gu9o//XK0QSU0zSSReKNRsXsFc/n/psxi0YSPNiqlKxImp5h4DHnAPdwYJ8nNA==",
-          "dev": true
-        },
-        "electron-to-chromium": {
-          "version": "1.3.112",
-          "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.112.tgz",
-          "integrity": "sha512-FyVLdiRZnLw2WE5ECtveN0JJ7klyiz/HMfKE1/Rjff3l7pe4vfkYtBlcCqTckvR8E7asjJGh0m9gRPR3Anp/UA==",
-          "dev": true
-        },
-        "node-releases": {
-          "version": "1.1.7",
-          "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.7.tgz",
-          "integrity": "sha512-bKdrwaqJUPHqlCzDD7so/R+Nk0jGv9a11ZhLrD9f6i947qGLrGAhU3OxRENa19QQmwzGy/g6zCDEuLGDO8HPvA==",
-          "dev": true,
-          "requires": {
-            "semver": "^5.3.0"
-          }
-        }
-      }
-    },
     "aws-sign2": {
       "version": "0.7.0",
       "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
       "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==",
       "dev": true
     },
-    "axios": {
-      "version": "0.18.0",
-      "resolved": "https://registry.npmjs.org/axios/-/axios-0.18.0.tgz",
-      "integrity": "sha1-MtU+SFHv3AoRmTts0AB4nXDAUQI=",
-      "requires": {
-        "follow-redirects": "^1.3.0",
-        "is-buffer": "^1.1.5"
-      },
-      "dependencies": {
-        "debug": {
-          "version": "3.1.0",
-          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
-          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
-          "requires": {
-            "ms": "2.0.0"
-          }
-        },
-        "follow-redirects": {
-          "version": "1.4.1",
-          "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.4.1.tgz",
-          "integrity": "sha512-uxYePVPogtya1ktGnAAXOacnbIuRMB4dkvqeNz2qTtTQsuzSfbDolV+wMMKxAmCx0bLgAKLbBOkjItMbbkR1vg==",
-          "requires": {
-            "debug": "^3.1.0"
-          }
-        }
-      }
-    },
-    "babel-loader": {
-      "version": "8.0.4",
-      "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.0.4.tgz",
-      "integrity": "sha512-fhBhNkUToJcW9nV46v8w87AJOwAJDz84c1CL57n3Stj73FANM/b9TbCUK4YhdOwEyZ+OxhYpdeZDNzSI29Firw==",
-      "dev": true,
-      "requires": {
-        "find-cache-dir": "^1.0.0",
-        "loader-utils": "^1.0.2",
-        "mkdirp": "^0.5.1",
-        "util.promisify": "^1.0.0"
-      }
-    },
     "balanced-match": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
             "kind-of": "^6.0.2"
           }
         },
-        "isobject": {
-          "version": "3.0.1",
-          "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
-          "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
-          "dev": true
-        },
         "kind-of": {
           "version": "6.0.2",
           "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
       }
     },
     "bluebird": {
-      "version": "3.5.2",
-      "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.2.tgz",
-      "integrity": "sha512-dhHTWMI7kMx5whMQntl7Vr9C6BvV10lFXDAasnqnrMYhXVCzzk6IO9Fo2L75jXHT07WrOngL1WDXOp+yYS91Yg==",
+      "version": "3.5.5",
+      "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.5.tgz",
+      "integrity": "sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w==",
       "dev": true
     },
     "bn.js": {
       }
     },
     "braces": {
-      "version": "1.8.5",
-      "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz",
-      "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=",
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz",
+      "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==",
       "dev": true,
       "requires": {
-        "expand-range": "^1.8.1",
-        "preserve": "^0.2.0",
-        "repeat-element": "^1.1.2"
+        "arr-flatten": "^1.1.0",
+        "array-unique": "^0.3.2",
+        "extend-shallow": "^2.0.1",
+        "fill-range": "^4.0.0",
+        "isobject": "^3.0.1",
+        "repeat-element": "^1.1.2",
+        "snapdragon": "^0.8.1",
+        "snapdragon-node": "^2.0.1",
+        "split-string": "^3.0.2",
+        "to-regex": "^3.0.1"
+      },
+      "dependencies": {
+        "extend-shallow": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+          "dev": true,
+          "requires": {
+            "is-extendable": "^0.1.0"
+          }
+        }
       }
     },
     "brorand": {
         "pako": "~1.0.5"
       }
     },
-    "browserslist": {
-      "version": "4.3.4",
-      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.3.4.tgz",
-      "integrity": "sha512-u5iz+ijIMUlmV8blX82VGFrB9ecnUg5qEt55CMZ/YJEhha+d8qpBfOFuutJ6F/VKRXjZoD33b6uvarpPxcl3RA==",
-      "dev": true,
-      "requires": {
-        "caniuse-lite": "^1.0.30000899",
-        "electron-to-chromium": "^1.3.82",
-        "node-releases": "^1.0.1"
-      }
-    },
     "buffer": {
       "version": "4.9.1",
       "resolved": "http://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz",
         "y18n": "^4.0.0"
       },
       "dependencies": {
-        "bluebird": {
-          "version": "3.5.3",
-          "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.3.tgz",
-          "integrity": "sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw==",
-          "dev": true
-        },
         "graceful-fs": {
           "version": "4.1.15",
           "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz",
         "to-object-path": "^0.3.0",
         "union-value": "^1.0.0",
         "unset-value": "^1.0.0"
-      },
-      "dependencies": {
-        "isobject": {
-          "version": "3.0.1",
-          "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
-          "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
-          "dev": true
-        }
       }
     },
     "camelcase": {
-      "version": "2.1.1",
-      "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz",
-      "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=",
+      "version": "5.3.1",
+      "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
+      "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
       "dev": true
     },
     "camelcase-keys": {
       "requires": {
         "camelcase": "^2.0.0",
         "map-obj": "^1.0.0"
+      },
+      "dependencies": {
+        "camelcase": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz",
+          "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=",
+          "dev": true
+        }
       }
     },
-    "caniuse-lite": {
-      "version": "1.0.30000904",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000904.tgz",
-      "integrity": "sha512-M4sXvogCoY5Fp6fuXIaQG/MIexlEFQ3Lgwban+KlqiQUbUIkSmjAB8ZJIP79aj2cdqz2F1Lb+Z+5GwHvCrbLtg==",
-      "dev": true
-    },
     "caseless": {
       "version": "0.12.0",
       "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
       }
     },
     "chokidar": {
-      "version": "1.7.0",
-      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz",
-      "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=",
+      "version": "2.1.6",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.6.tgz",
+      "integrity": "sha512-V2jUo67OKkc6ySiRpJrjlpJKl9kDuG+Xb8VgsGzb+aEouhgS1D0weyPU4lEzdAcsCAvrih2J2BqyXqHWvVLw5g==",
       "dev": true,
       "requires": {
-        "anymatch": "^1.3.0",
-        "async-each": "^1.0.0",
-        "fsevents": "^1.0.0",
-        "glob-parent": "^2.0.0",
-        "inherits": "^2.0.1",
+        "anymatch": "^2.0.0",
+        "async-each": "^1.0.1",
+        "braces": "^2.3.2",
+        "fsevents": "^1.2.7",
+        "glob-parent": "^3.1.0",
+        "inherits": "^2.0.3",
         "is-binary-path": "^1.0.0",
-        "is-glob": "^2.0.0",
+        "is-glob": "^4.0.0",
+        "normalize-path": "^3.0.0",
         "path-is-absolute": "^1.0.0",
-        "readdirp": "^2.0.0"
+        "readdirp": "^2.2.1",
+        "upath": "^1.1.1"
       }
     },
     "chownr": {
       "dev": true
     },
     "chrome-trace-event": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.0.tgz",
-      "integrity": "sha512-xDbVgyfDTT2piup/h8dK/y4QZfJRSa73bw1WZ8b4XM1o7fsFubUVGYcE+1ANtOzJJELGpYoG2961z0Z6OAld9A==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz",
+      "integrity": "sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ==",
       "dev": true,
       "requires": {
         "tslib": "^1.9.0"
           "requires": {
             "is-descriptor": "^0.1.0"
           }
-        },
-        "isobject": {
-          "version": "3.0.1",
-          "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
-          "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
-          "dev": true
         }
       }
     },
         "shallow-clone": "^1.0.0"
       },
       "dependencies": {
-        "for-own": {
-          "version": "1.0.0",
-          "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz",
-          "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=",
-          "dev": true,
-          "requires": {
-            "for-in": "^1.0.1"
-          }
-        },
         "kind-of": {
           "version": "6.0.2",
           "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
         }
       }
     },
-    "co": {
-      "version": "4.6.0",
-      "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
-      "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=",
-      "dev": true
-    },
     "code-point-at": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
       "dev": true
     },
     "codemirror": {
-      "version": "5.42.0",
-      "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.42.0.tgz",
-      "integrity": "sha512-pbApC8zDzItP3HRphD6kQVwS976qB5Qi0hU3MZMixLk+AyugOW1RF+8XJEjeyl5yWsHNe88tDUxzeRh5AOxPRw=="
+      "version": "5.47.0",
+      "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.47.0.tgz",
+      "integrity": "sha512-kV49Fr+NGFHFc/Imsx6g180hSlkGhuHxTSDDmDHOuyln0MQYFLixDY4+bFkBVeCEiepYfDimAF/e++9jPJk4QA=="
     },
     "collection-visit": {
       "version": "1.0.0",
       "dev": true
     },
     "combined-stream": {
-      "version": "1.0.7",
-      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz",
-      "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==",
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
       "dev": true,
       "requires": {
         "delayed-stream": "~1.0.0"
       }
     },
     "commander": {
-      "version": "2.17.1",
-      "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz",
-      "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==",
+      "version": "2.20.0",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz",
+      "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==",
       "dev": true
     },
     "commondir": {
       "dev": true
     },
     "component-emitter": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
-      "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=",
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
+      "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==",
       "dev": true
     },
     "concat-map": {
       "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=",
       "dev": true
     },
-    "convert-source-map": {
-      "version": "1.6.0",
-      "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz",
-      "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==",
-      "dev": true,
-      "requires": {
-        "safe-buffer": "~5.1.1"
-      }
-    },
     "copy-concurrently": {
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz",
       "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=",
       "dev": true
     },
-    "core-js": {
-      "version": "2.5.7",
-      "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz",
-      "integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw==",
-      "dev": true
-    },
     "core-util-is": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
       "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
       "dev": true
     },
-    "cosmiconfig": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-4.0.0.tgz",
-      "integrity": "sha512-6e5vDdrXZD+t5v0L8CrurPeybg4Fmf+FCSYxXKYVAqLUtyCSbuyqE059d0kDthTNRzKVjL7QMgNpEUlsoYH3iQ==",
-      "dev": true,
-      "requires": {
-        "is-directory": "^0.3.1",
-        "js-yaml": "^3.9.0",
-        "parse-json": "^4.0.0",
-        "require-from-string": "^2.0.1"
-      },
-      "dependencies": {
-        "parse-json": {
-          "version": "4.0.0",
-          "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz",
-          "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=",
-          "dev": true,
-          "requires": {
-            "error-ex": "^1.3.1",
-            "json-parse-better-errors": "^1.0.1"
-          }
-        }
-      }
-    },
     "create-ecdh": {
       "version": "4.0.3",
       "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz",
       }
     },
     "css-loader": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-2.1.0.tgz",
-      "integrity": "sha512-MoOu+CStsGrSt5K2OeZ89q3Snf+IkxRfAIt9aAKg4piioTrhtP1iEFPu+OVn3Ohz24FO6L+rw9UJxBILiSBw5Q==",
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-2.1.1.tgz",
+      "integrity": "sha512-OcKJU/lt232vl1P9EEDamhoO9iKY3tIjY5GU+XDLblAykTdgs6Ux9P1hTHve8nFKy5KPpOXOsVI/hIwi3841+w==",
       "dev": true,
       "requires": {
-        "icss-utils": "^4.0.0",
-        "loader-utils": "^1.2.1",
-        "lodash": "^4.17.11",
-        "postcss": "^7.0.6",
+        "camelcase": "^5.2.0",
+        "icss-utils": "^4.1.0",
+        "loader-utils": "^1.2.3",
+        "normalize-path": "^3.0.0",
+        "postcss": "^7.0.14",
         "postcss-modules-extract-imports": "^2.0.0",
-        "postcss-modules-local-by-default": "^2.0.3",
-        "postcss-modules-scope": "^2.0.0",
+        "postcss-modules-local-by-default": "^2.0.6",
+        "postcss-modules-scope": "^2.1.0",
         "postcss-modules-values": "^2.0.0",
         "postcss-value-parser": "^3.3.0",
         "schema-utils": "^1.0.0"
       },
       "dependencies": {
-        "ajv": {
-          "version": "6.8.1",
-          "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.8.1.tgz",
-          "integrity": "sha512-eqxCp82P+JfqL683wwsL73XmFs1eG6qjw+RD3YHx+Jll1r0jNd4dh8QG9NYAeNGA/hnZjeEDgtTskgJULbxpWQ==",
-          "dev": true,
-          "requires": {
-            "fast-deep-equal": "^2.0.1",
-            "fast-json-stable-stringify": "^2.0.0",
-            "json-schema-traverse": "^0.4.1",
-            "uri-js": "^4.2.2"
-          }
-        },
         "big.js": {
           "version": "5.2.2",
           "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
           "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==",
           "dev": true
         },
-        "fast-deep-equal": {
-          "version": "2.0.1",
-          "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
-          "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=",
-          "dev": true
-        },
-        "json-schema-traverse": {
-          "version": "0.4.1",
-          "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
-          "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
-          "dev": true
-        },
         "json5": {
           "version": "1.0.1",
           "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
             "json5": "^1.0.1"
           }
         },
-        "lodash": {
-          "version": "4.17.11",
-          "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
-          "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==",
-          "dev": true
-        },
         "minimist": {
           "version": "1.2.0",
           "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
           "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
           "dev": true
-        },
-        "schema-utils": {
-          "version": "1.0.0",
-          "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz",
-          "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==",
-          "dev": true,
-          "requires": {
-            "ajv": "^6.1.0",
-            "ajv-errors": "^1.0.0",
-            "ajv-keywords": "^3.1.0"
-          }
-        }
-      }
-    },
-    "css-selector-tokenizer": {
-      "version": "0.7.1",
-      "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.7.1.tgz",
-      "integrity": "sha512-xYL0AMZJ4gFzJQsHUKa5jiWWi2vH77WVNg7JYRyewwj6oPh4yb/y6Y9ZCw9dsj/9UauMhtuxR+ogQd//EdEVNA==",
-      "dev": true,
-      "requires": {
-        "cssesc": "^0.1.0",
-        "fastparse": "^1.1.1",
-        "regexpu-core": "^1.0.0"
-      },
-      "dependencies": {
-        "jsesc": {
-          "version": "0.5.0",
-          "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz",
-          "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=",
-          "dev": true
-        },
-        "regexpu-core": {
-          "version": "1.0.0",
-          "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-1.0.0.tgz",
-          "integrity": "sha1-hqdj9Y7k18L2sQLkdkBQ3n7ZDGs=",
-          "dev": true,
-          "requires": {
-            "regenerate": "^1.2.1",
-            "regjsgen": "^0.2.0",
-            "regjsparser": "^0.1.4"
-          }
-        },
-        "regjsgen": {
-          "version": "0.2.0",
-          "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz",
-          "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=",
-          "dev": true
-        },
-        "regjsparser": {
-          "version": "0.1.5",
-          "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz",
-          "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=",
-          "dev": true,
-          "requires": {
-            "jsesc": "~0.5.0"
-          }
         }
       }
     },
     "cssesc": {
-      "version": "0.1.0",
-      "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-0.1.0.tgz",
-      "integrity": "sha1-yBSQPkViM3GgR3tAEJqq++6t27Q=",
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+      "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
       "dev": true
     },
     "currently-unhandled": {
       "dev": true
     },
     "debug": {
-      "version": "4.1.0",
-      "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.0.tgz",
-      "integrity": "sha512-heNPJUJIqC+xB6ayLAMHaIrmN9HKa7aQO8MGqKpvCA+uJYVcvR6l5kgdrhRuwPFHU7P5/A1w0BjByPHwpfTDKg==",
+      "version": "2.6.9",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
       "dev": true,
-      "requires": {
-        "ms": "^2.1.1"
-      },
-      "dependencies": {
-        "ms": {
-          "version": "2.1.1",
-          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
-          "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
-          "dev": true
-        }
+      "requires": {
+        "ms": "2.0.0"
       }
     },
     "decamelize": {
             "kind-of": "^6.0.2"
           }
         },
-        "isobject": {
-          "version": "3.0.1",
-          "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
-          "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
-          "dev": true
-        },
         "kind-of": {
           "version": "6.0.2",
           "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
         "minimalistic-assert": "^1.0.0"
       }
     },
+    "detect-file": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz",
+      "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=",
+      "dev": true
+    },
     "diffie-hellman": {
       "version": "5.0.3",
       "resolved": "http://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
       "integrity": "sha512-3VduRWLxx9hbVr42QieQN25mx/I61/mRdUSuxAmDGdDqZIN8qtP7tcKMa3KfpJjuGjOJGYYUzzeq6eGDnkzesA=="
     },
     "duplexify": {
-      "version": "3.6.1",
-      "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.6.1.tgz",
-      "integrity": "sha512-vM58DwdnKmty+FSPzT14K9JXb90H+j5emaR4KYbr2KTIz00WHGbWOe5ghQTx233ZCLZtrGDALzKwcjEtSt35mA==",
+      "version": "3.7.1",
+      "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz",
+      "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==",
       "dev": true,
       "requires": {
         "end-of-stream": "^1.0.0",
         "safer-buffer": "^2.1.0"
       }
     },
-    "electron-to-chromium": {
-      "version": "1.3.83",
-      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.83.tgz",
-      "integrity": "sha512-DqJoDarxq50dcHsOOlMLNoy+qQitlMNbYb6wwbE0oUw2veHdRkpNrhmngiUYKMErdJ8SJ48rpJsZTQgy5SoEAA==",
-      "dev": true
-    },
     "elliptic": {
       "version": "6.4.1",
       "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.1.tgz",
       "dev": true
     },
     "eslint-scope": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.0.tgz",
-      "integrity": "sha512-1G6UTDi7Jc1ELFwnR58HV4fK9OQK4S6N985f166xqXxpjU6plxFISJa2Ba9KCQuFa8RCnj/lSFJbHo7UFDBnUA==",
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz",
+      "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==",
       "dev": true,
       "requires": {
         "esrecurse": "^4.1.0",
         "estraverse": "^4.1.1"
       }
     },
-    "esprima": {
-      "version": "4.0.1",
-      "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
-      "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
-      "dev": true
-    },
     "esrecurse": {
       "version": "4.2.1",
       "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz",
       "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=",
       "dev": true
     },
-    "esutils": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz",
-      "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=",
-      "dev": true
-    },
     "events": {
-      "version": "1.1.1",
-      "resolved": "http://registry.npmjs.org/events/-/events-1.1.1.tgz",
-      "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=",
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/events/-/events-3.0.0.tgz",
+      "integrity": "sha512-Dc381HFWJzEOhQ+d8pkNon++bk9h6cdAoAj4iE6Q4y6xgTzySWXlKn05/TVNpjnfRqi/X0EpJEJohPjNI3zpVA==",
       "dev": true
     },
     "evp_bytestokey": {
       }
     },
     "execa": {
-      "version": "0.10.0",
-      "resolved": "https://registry.npmjs.org/execa/-/execa-0.10.0.tgz",
-      "integrity": "sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw==",
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz",
+      "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==",
       "dev": true,
       "requires": {
         "cross-spawn": "^6.0.0",
-        "get-stream": "^3.0.0",
+        "get-stream": "^4.0.0",
         "is-stream": "^1.1.0",
         "npm-run-path": "^2.0.0",
         "p-finally": "^1.0.0",
       }
     },
     "expand-brackets": {
-      "version": "0.1.5",
-      "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz",
-      "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=",
+      "version": "2.1.4",
+      "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz",
+      "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=",
       "dev": true,
       "requires": {
-        "is-posix-bracket": "^0.1.0"
+        "debug": "^2.3.3",
+        "define-property": "^0.2.5",
+        "extend-shallow": "^2.0.1",
+        "posix-character-classes": "^0.1.0",
+        "regex-not": "^1.0.0",
+        "snapdragon": "^0.8.1",
+        "to-regex": "^3.0.1"
+      },
+      "dependencies": {
+        "define-property": {
+          "version": "0.2.5",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+          "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+          "dev": true,
+          "requires": {
+            "is-descriptor": "^0.1.0"
+          }
+        },
+        "extend-shallow": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+          "dev": true,
+          "requires": {
+            "is-extendable": "^0.1.0"
+          }
+        }
       }
     },
-    "expand-range": {
-      "version": "1.8.2",
-      "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz",
-      "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=",
+    "expand-tilde": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz",
+      "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=",
       "dev": true,
       "requires": {
-        "fill-range": "^2.1.0"
+        "homedir-polyfill": "^1.0.1"
       }
     },
     "extend": {
       }
     },
     "extglob": {
-      "version": "0.3.2",
-      "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz",
-      "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=",
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz",
+      "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==",
       "dev": true,
       "requires": {
-        "is-extglob": "^1.0.0"
+        "array-unique": "^0.3.2",
+        "define-property": "^1.0.0",
+        "expand-brackets": "^2.1.4",
+        "extend-shallow": "^2.0.1",
+        "fragment-cache": "^0.2.1",
+        "regex-not": "^1.0.0",
+        "snapdragon": "^0.8.1",
+        "to-regex": "^3.0.1"
+      },
+      "dependencies": {
+        "define-property": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
+          "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
+          "dev": true,
+          "requires": {
+            "is-descriptor": "^1.0.0"
+          }
+        },
+        "extend-shallow": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+          "dev": true,
+          "requires": {
+            "is-extendable": "^0.1.0"
+          }
+        },
+        "is-accessor-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+          "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+          "dev": true,
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-data-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+          "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+          "dev": true,
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-descriptor": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+          "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+          "dev": true,
+          "requires": {
+            "is-accessor-descriptor": "^1.0.0",
+            "is-data-descriptor": "^1.0.0",
+            "kind-of": "^6.0.2"
+          }
+        },
+        "kind-of": {
+          "version": "6.0.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
+          "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
+          "dev": true
+        }
       }
     },
     "extsprintf": {
       "dev": true
     },
     "fast-deep-equal": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz",
-      "integrity": "sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8=",
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
+      "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=",
       "dev": true
     },
     "fast-json-stable-stringify": {
       "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=",
       "dev": true
     },
-    "fastparse": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz",
-      "integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==",
-      "dev": true
-    },
     "figgy-pudding": {
       "version": "3.5.1",
       "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz",
       "integrity": "sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w==",
       "dev": true
     },
-    "filename-regex": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz",
-      "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=",
-      "dev": true
-    },
     "fill-range": {
-      "version": "2.2.4",
-      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz",
-      "integrity": "sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==",
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
+      "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=",
       "dev": true,
       "requires": {
-        "is-number": "^2.1.0",
-        "isobject": "^2.0.0",
-        "randomatic": "^3.0.0",
-        "repeat-element": "^1.1.2",
-        "repeat-string": "^1.5.2"
+        "extend-shallow": "^2.0.1",
+        "is-number": "^3.0.0",
+        "repeat-string": "^1.6.1",
+        "to-regex-range": "^2.1.0"
       },
       "dependencies": {
-        "kind-of": {
-          "version": "6.0.2",
-          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
-          "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
-          "dev": true
-        },
-        "randomatic": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.0.0.tgz",
-          "integrity": "sha512-VdxFOIEY3mNO5PtSRkkle/hPJDHvQhK21oa73K4yAc9qmp6N429gAyF1gZMOTMeS0/AYzaV/2Trcef+NaIonSA==",
+        "extend-shallow": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
           "dev": true,
           "requires": {
-            "is-number": "^4.0.0",
-            "kind-of": "^6.0.0",
-            "math-random": "^1.0.1"
-          },
-          "dependencies": {
-            "is-number": {
-              "version": "4.0.0",
-              "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz",
-              "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==",
-              "dev": true
-            }
+            "is-extendable": "^0.1.0"
           }
         }
       }
     },
     "find-cache-dir": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-1.0.0.tgz",
-      "integrity": "sha1-kojj6ePMN0hxfTnq3hfPcfww7m8=",
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz",
+      "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==",
       "dev": true,
       "requires": {
         "commondir": "^1.0.1",
-        "make-dir": "^1.0.0",
-        "pkg-dir": "^2.0.0"
+        "make-dir": "^2.0.0",
+        "pkg-dir": "^3.0.0"
       }
     },
     "find-up": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz",
-      "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=",
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
+      "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
+      "dev": true,
+      "requires": {
+        "locate-path": "^3.0.0"
+      }
+    },
+    "findup-sync": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz",
+      "integrity": "sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw=",
       "dev": true,
       "requires": {
-        "locate-path": "^2.0.0"
+        "detect-file": "^1.0.0",
+        "is-glob": "^3.1.0",
+        "micromatch": "^3.0.4",
+        "resolve-dir": "^1.0.1"
+      },
+      "dependencies": {
+        "is-accessor-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+          "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-data-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+          "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-descriptor": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+          "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+          "requires": {
+            "is-accessor-descriptor": "^1.0.0",
+            "is-data-descriptor": "^1.0.0",
+            "kind-of": "^6.0.2"
+          }
+        },
+        "is-glob": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz",
+          "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=",
+          "dev": true,
+          "requires": {
+            "is-extglob": "^2.1.0"
+          }
+        },
+        "kind-of": {
+          "version": "6.0.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
+          "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA=="
+        }
       }
     },
     "flush-write-stream": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.0.3.tgz",
-      "integrity": "sha512-calZMC10u0FMUqoiunI2AiGIIUtUIvifNwkHhNupZH4cbNnW1Itkoh/Nf5HFYmDrwWPjrUxpkZT0KhuCq0jmGw==",
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz",
+      "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==",
       "dev": true,
       "requires": {
-        "inherits": "^2.0.1",
-        "readable-stream": "^2.0.4"
+        "inherits": "^2.0.3",
+        "readable-stream": "^2.3.6"
+      },
+      "dependencies": {
+        "process-nextick-args": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
+          "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==",
+          "dev": true
+        },
+        "readable-stream": {
+          "version": "2.3.6",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
+          "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
+          "dev": true,
+          "requires": {
+            "core-util-is": "~1.0.0",
+            "inherits": "~2.0.3",
+            "isarray": "~1.0.0",
+            "process-nextick-args": "~2.0.0",
+            "safe-buffer": "~5.1.1",
+            "string_decoder": "~1.1.1",
+            "util-deprecate": "~1.0.1"
+          }
+        },
+        "string_decoder": {
+          "version": "1.1.1",
+          "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+          "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+          "dev": true,
+          "requires": {
+            "safe-buffer": "~5.1.0"
+          }
+        }
       }
     },
     "for-in": {
       "dev": true
     },
     "for-own": {
-      "version": "0.1.5",
-      "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz",
-      "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=",
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz",
+      "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=",
       "dev": true,
       "requires": {
         "for-in": "^1.0.1"
       "dev": true
     },
     "fsevents": {
-      "version": "1.2.4",
-      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz",
-      "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==",
+      "version": "1.2.9",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz",
+      "integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==",
       "dev": true,
       "optional": true,
       "requires": {
-        "nan": "^2.9.2",
-        "node-pre-gyp": "^0.10.0"
+        "nan": "^2.12.1",
+        "node-pre-gyp": "^0.12.0"
       },
       "dependencies": {
         "abbrev": {
         "ansi-regex": {
           "version": "2.1.1",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "aproba": {
           "version": "1.2.0",
           "optional": true
         },
         "are-we-there-yet": {
-          "version": "1.1.4",
+          "version": "1.1.5",
           "bundled": true,
           "dev": true,
           "optional": true,
           }
         },
         "chownr": {
-          "version": "1.0.1",
+          "version": "1.1.1",
           "bundled": true,
           "dev": true,
           "optional": true
           "optional": true
         },
         "debug": {
-          "version": "2.6.9",
+          "version": "4.1.1",
           "bundled": true,
           "dev": true,
           "optional": true,
           "requires": {
-            "ms": "2.0.0"
+            "ms": "^2.1.1"
           }
         },
         "deep-extend": {
-          "version": "0.5.1",
+          "version": "0.6.0",
           "bundled": true,
           "dev": true,
           "optional": true
           }
         },
         "glob": {
-          "version": "7.1.2",
+          "version": "7.1.3",
           "bundled": true,
           "dev": true,
           "optional": true,
           "optional": true
         },
         "iconv-lite": {
-          "version": "0.4.21",
+          "version": "0.4.24",
           "bundled": true,
           "dev": true,
           "optional": true,
           "requires": {
-            "safer-buffer": "^2.1.0"
+            "safer-buffer": ">= 2.1.2 < 3"
           }
         },
         "ignore-walk": {
         "minimist": {
           "version": "0.0.8",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "minipass": {
-          "version": "2.2.4",
+          "version": "2.3.5",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
-            "safe-buffer": "^5.1.1",
+            "safe-buffer": "^5.1.2",
             "yallist": "^3.0.0"
           }
         },
         "minizlib": {
-          "version": "1.1.0",
+          "version": "1.2.1",
           "bundled": true,
           "dev": true,
           "optional": true,
           "version": "0.5.1",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "minimist": "0.0.8"
           }
         },
         "ms": {
-          "version": "2.0.0",
+          "version": "2.1.1",
           "bundled": true,
           "dev": true,
           "optional": true
         },
-        "nan": {
-          "version": "2.10.0",
-          "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz",
-          "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==",
-          "dev": true,
-          "optional": true
-        },
         "needle": {
-          "version": "2.2.0",
+          "version": "2.3.0",
           "bundled": true,
           "dev": true,
           "optional": true,
           "requires": {
-            "debug": "^2.1.2",
+            "debug": "^4.1.0",
             "iconv-lite": "^0.4.4",
             "sax": "^1.2.4"
           }
         },
         "node-pre-gyp": {
-          "version": "0.10.0",
+          "version": "0.12.0",
           "bundled": true,
           "dev": true,
           "optional": true,
           "requires": {
             "detect-libc": "^1.0.2",
             "mkdirp": "^0.5.1",
-            "needle": "^2.2.0",
+            "needle": "^2.2.1",
             "nopt": "^4.0.1",
             "npm-packlist": "^1.1.6",
             "npmlog": "^4.0.2",
-            "rc": "^1.1.7",
+            "rc": "^1.2.7",
             "rimraf": "^2.6.1",
             "semver": "^5.3.0",
             "tar": "^4"
           }
         },
         "npm-bundled": {
-          "version": "1.0.3",
+          "version": "1.0.6",
           "bundled": true,
           "dev": true,
           "optional": true
         },
         "npm-packlist": {
-          "version": "1.1.10",
+          "version": "1.4.1",
           "bundled": true,
           "dev": true,
           "optional": true,
           "optional": true
         },
         "rc": {
-          "version": "1.2.7",
+          "version": "1.2.8",
           "bundled": true,
           "dev": true,
           "optional": true,
           "requires": {
-            "deep-extend": "^0.5.1",
+            "deep-extend": "^0.6.0",
             "ini": "~1.3.0",
             "minimist": "^1.2.0",
             "strip-json-comments": "~2.0.1"
           }
         },
         "rimraf": {
-          "version": "2.6.2",
+          "version": "2.6.3",
           "bundled": true,
           "dev": true,
           "optional": true,
           "requires": {
-            "glob": "^7.0.5"
+            "glob": "^7.1.3"
           }
         },
         "safe-buffer": {
-          "version": "5.1.1",
+          "version": "5.1.2",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "safer-buffer": {
           "version": "2.1.2",
           "optional": true
         },
         "semver": {
-          "version": "5.5.0",
+          "version": "5.7.0",
           "bundled": true,
           "dev": true,
           "optional": true
           "version": "3.0.1",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "ansi-regex": "^2.0.0"
           }
           "optional": true
         },
         "tar": {
-          "version": "4.4.1",
+          "version": "4.4.8",
           "bundled": true,
           "dev": true,
           "optional": true,
           "requires": {
-            "chownr": "^1.0.1",
+            "chownr": "^1.1.1",
             "fs-minipass": "^1.2.5",
-            "minipass": "^2.2.4",
-            "minizlib": "^1.1.0",
+            "minipass": "^2.3.4",
+            "minizlib": "^1.1.1",
             "mkdirp": "^0.5.0",
-            "safe-buffer": "^5.1.1",
+            "safe-buffer": "^5.1.2",
             "yallist": "^3.0.2"
           }
         },
           "optional": true
         },
         "wide-align": {
-          "version": "1.1.2",
+          "version": "1.1.3",
           "bundled": true,
           "dev": true,
           "optional": true,
           "requires": {
-            "string-width": "^1.0.2"
+            "string-width": "^1.0.2 || 2"
           }
         },
         "wrappy": {
           "version": "1.0.2",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "yallist": {
-          "version": "3.0.2",
+          "version": "3.0.3",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         }
       }
     },
     "fstream": {
-      "version": "1.0.11",
-      "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz",
-      "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=",
+      "version": "1.0.12",
+      "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz",
+      "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==",
       "dev": true,
       "requires": {
         "graceful-fs": "^4.1.2",
       "dev": true
     },
     "get-stream": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
-      "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=",
-      "dev": true
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
+      "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==",
+      "dev": true,
+      "requires": {
+        "pump": "^3.0.0"
+      }
     },
     "get-value": {
       "version": "2.0.6",
         "path-is-absolute": "^1.0.0"
       }
     },
-    "glob-base": {
-      "version": "0.3.0",
-      "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz",
-      "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=",
+    "glob-parent": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz",
+      "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=",
       "dev": true,
       "requires": {
-        "glob-parent": "^2.0.0",
-        "is-glob": "^2.0.0"
+        "is-glob": "^3.1.0",
+        "path-dirname": "^1.0.0"
+      },
+      "dependencies": {
+        "is-glob": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz",
+          "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=",
+          "dev": true,
+          "requires": {
+            "is-extglob": "^2.1.0"
+          }
+        }
       }
     },
-    "glob-parent": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz",
-      "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=",
+    "global-modules": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz",
+      "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==",
       "dev": true,
       "requires": {
-        "is-glob": "^2.0.0"
+        "global-prefix": "^1.0.1",
+        "is-windows": "^1.0.1",
+        "resolve-dir": "^1.0.0"
       }
     },
-    "global-modules-path": {
-      "version": "2.3.0",
-      "resolved": "https://registry.npmjs.org/global-modules-path/-/global-modules-path-2.3.0.tgz",
-      "integrity": "sha512-HchvMJNYh9dGSCy8pOQ2O8u/hoXaL+0XhnrwH0RyLiSXMMTl9W3N6KUU73+JFOg5PGjtzl6VZzUQsnrpm7Szag==",
-      "dev": true
-    },
-    "globals": {
-      "version": "11.9.0",
-      "resolved": "https://registry.npmjs.org/globals/-/globals-11.9.0.tgz",
-      "integrity": "sha512-5cJVtyXWH8PiJPVLZzzoIizXx944O4OmRro5MWKx5fT4MgcN7OfaMutPeaTdJCCURwbWdhhcCWcKIffPnmTzBg==",
-      "dev": true
+    "global-prefix": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz",
+      "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=",
+      "dev": true,
+      "requires": {
+        "expand-tilde": "^2.0.2",
+        "homedir-polyfill": "^1.0.1",
+        "ini": "^1.3.4",
+        "is-windows": "^1.0.1",
+        "which": "^1.2.14"
+      }
     },
     "globule": {
       "version": "1.2.1",
       "dev": true
     },
     "har-validator": {
-      "version": "5.1.0",
-      "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.0.tgz",
-      "integrity": "sha512-+qnmNjI4OfH2ipQ9VQOw23bBd/ibtfbVdK2fYbY4acTDqKTW/YDp9McimZdDbG8iV9fZizUqQMD5xvriB146TA==",
+      "version": "5.1.3",
+      "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz",
+      "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==",
       "dev": true,
       "requires": {
-        "ajv": "^5.3.0",
+        "ajv": "^6.5.5",
         "har-schema": "^2.0.0"
       }
     },
         "get-value": "^2.0.6",
         "has-values": "^1.0.0",
         "isobject": "^3.0.0"
-      },
-      "dependencies": {
-        "isobject": {
-          "version": "3.0.1",
-          "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
-          "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
-          "dev": true
-        }
       }
     },
     "has-values": {
         "kind-of": "^4.0.0"
       },
       "dependencies": {
-        "is-number": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
-          "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
-          "dev": true,
-          "requires": {
-            "kind-of": "^3.0.2"
-          },
-          "dependencies": {
-            "kind-of": {
-              "version": "3.2.2",
-              "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
-              "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
-              "dev": true,
-              "requires": {
-                "is-buffer": "^1.1.5"
-              }
-            }
-          }
-        },
         "kind-of": {
           "version": "4.0.0",
           "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz",
       }
     },
     "hash.js": {
-      "version": "1.1.5",
-      "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.5.tgz",
-      "integrity": "sha512-eWI5HG9Np+eHV1KQhisXWwM+4EPPYe5dFX1UZZH7k/E3JzDEazVH+VGlZi6R94ZqImq+A3D1mCEtrFIfg/E7sA==",
+      "version": "1.1.7",
+      "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
+      "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
       "dev": true,
       "requires": {
         "inherits": "^2.0.3",
         "minimalistic-crypto-utils": "^1.0.1"
       }
     },
+    "homedir-polyfill": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz",
+      "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==",
+      "dev": true,
+      "requires": {
+        "parse-passwd": "^1.0.0"
+      }
+    },
     "hosted-git-info": {
       "version": "2.7.1",
       "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz",
       "dev": true
     },
     "icss-utils": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-4.0.0.tgz",
-      "integrity": "sha512-bA/xGiwWM17qjllIs9X/y0EjsB7e0AV08F3OL8UPsoNkNRibIuu8f1eKTnQ8QO1DteKKTxTUAn+IEWUToIwGOA==",
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-4.1.1.tgz",
+      "integrity": "sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA==",
       "dev": true,
       "requires": {
-        "postcss": "^7.0.5"
+        "postcss": "^7.0.14"
       }
     },
     "ieee754": {
-      "version": "1.1.12",
-      "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.12.tgz",
-      "integrity": "sha512-GguP+DRY+pJ3soyIiGPTvdiVXjZ+DbXOxGpXn3eMvNW4x4irjqXm4wHKscC+TfxSJ0yw/S1F24tqdMNsMZTiLA==",
+      "version": "1.1.13",
+      "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz",
+      "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==",
       "dev": true
     },
     "iferr": {
       "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=",
       "dev": true
     },
-    "import-cwd": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz",
-      "integrity": "sha1-qmzzbnInYShcs3HsZRn1PiQ1sKk=",
-      "dev": true,
-      "requires": {
-        "import-from": "^2.1.0"
-      }
-    },
-    "import-from": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/import-from/-/import-from-2.1.0.tgz",
-      "integrity": "sha1-M1238qev/VOqpHHUuAId7ja387E=",
-      "dev": true,
-      "requires": {
-        "resolve-from": "^3.0.0"
-      }
-    },
     "import-local": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz",
       "requires": {
         "pkg-dir": "^3.0.0",
         "resolve-cwd": "^2.0.0"
-      },
-      "dependencies": {
-        "find-up": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
-          "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
-          "dev": true,
-          "requires": {
-            "locate-path": "^3.0.0"
-          }
-        },
-        "locate-path": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
-          "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
-          "dev": true,
-          "requires": {
-            "p-locate": "^3.0.0",
-            "path-exists": "^3.0.0"
-          }
-        },
-        "p-limit": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.0.0.tgz",
-          "integrity": "sha512-fl5s52lI5ahKCernzzIyAP0QAZbGIovtVHGwpcu1Jr/EpzLVDI2myISHwGqK7m8uQFugVWSrbxH7XnhGtvEc+A==",
-          "dev": true,
-          "requires": {
-            "p-try": "^2.0.0"
-          }
-        },
-        "p-locate": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
-          "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
-          "dev": true,
-          "requires": {
-            "p-limit": "^2.0.0"
-          }
-        },
-        "p-try": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.0.0.tgz",
-          "integrity": "sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ==",
-          "dev": true
-        },
-        "pkg-dir": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz",
-          "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==",
-          "dev": true,
-          "requires": {
-            "find-up": "^3.0.0"
-          }
-        }
       }
     },
     "imurmurhash": {
         "repeating": "^2.0.0"
       }
     },
+    "indexes-of": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz",
+      "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=",
+      "dev": true
+    },
     "indexof": {
       "version": "0.0.1",
       "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz",
       "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
       "dev": true
     },
-    "interpret": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz",
-      "integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=",
+    "ini": {
+      "version": "1.3.5",
+      "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
+      "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==",
       "dev": true
     },
-    "invariant": {
-      "version": "2.2.4",
-      "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
-      "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
-      "dev": true,
-      "requires": {
-        "loose-envify": "^1.0.0"
-      }
+    "interpret": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.2.0.tgz",
+      "integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==",
+      "dev": true
     },
     "invert-kv": {
       "version": "1.0.0",
     "is-buffer": {
       "version": "1.1.6",
       "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
-      "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="
+      "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==",
+      "dev": true
     },
     "is-builtin-module": {
       "version": "1.0.0",
         }
       }
     },
-    "is-directory": {
-      "version": "0.3.1",
-      "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz",
-      "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=",
-      "dev": true
-    },
-    "is-dotfile": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz",
-      "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=",
-      "dev": true
-    },
-    "is-equal-shallow": {
-      "version": "0.1.3",
-      "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz",
-      "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=",
-      "dev": true,
-      "requires": {
-        "is-primitive": "^2.0.0"
-      }
-    },
     "is-extendable": {
       "version": "0.1.1",
       "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
       "dev": true
     },
     "is-extglob": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
-      "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+      "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
       "dev": true
     },
     "is-finite": {
       }
     },
     "is-glob": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz",
-      "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=",
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
+      "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
       "dev": true,
       "requires": {
-        "is-extglob": "^1.0.0"
+        "is-extglob": "^2.1.1"
       }
     },
     "is-number": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz",
-      "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=",
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
+      "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
       "dev": true,
       "requires": {
         "kind-of": "^3.0.2"
       }
     },
+    "is-plain-obj": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz",
+      "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=",
+      "dev": true
+    },
     "is-plain-object": {
       "version": "2.0.4",
       "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
       "dev": true,
       "requires": {
         "isobject": "^3.0.1"
-      },
-      "dependencies": {
-        "isobject": {
-          "version": "3.0.1",
-          "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
-          "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
-          "dev": true
-        }
       }
     },
-    "is-posix-bracket": {
-      "version": "0.1.1",
-      "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz",
-      "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=",
-      "dev": true
-    },
-    "is-primitive": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz",
-      "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=",
-      "dev": true
-    },
     "is-regex": {
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz",
       "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==",
       "dev": true
     },
+    "is-wsl": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz",
+      "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=",
+      "dev": true
+    },
     "isarray": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
       "dev": true
     },
     "isobject": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz",
-      "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=",
-      "dev": true,
-      "requires": {
-        "isarray": "1.0.0"
-      }
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+      "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
+      "dev": true
     },
     "isstream": {
       "version": "0.1.2",
       "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=",
       "dev": true
     },
-    "jquery": {
-      "version": "3.3.1",
-      "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.3.1.tgz",
-      "integrity": "sha512-Ubldcmxp5np52/ENotGxlLe6aGMvmF4R8S6tZjsP6Knsaxd/xp3Zrh50cG93lR6nPXyUFwzN3ZSOQI0wRJNdGg=="
-    },
-    "jquery-sortable": {
-      "version": "0.9.13",
-      "resolved": "https://registry.npmjs.org/jquery-sortable/-/jquery-sortable-0.9.13.tgz",
-      "integrity": "sha1-HL+2VQE6B0c3BXHwbiL1JKAP+6I=",
-      "requires": {
-        "jquery": "^2.1.2"
-      },
-      "dependencies": {
-        "jquery": {
-          "version": "2.2.4",
-          "resolved": "https://registry.npmjs.org/jquery/-/jquery-2.2.4.tgz",
-          "integrity": "sha1-LInWiJterFIqfuoywUUhVZxsvwI="
-        }
-      }
-    },
     "js-base64": {
-      "version": "2.4.9",
-      "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.4.9.tgz",
-      "integrity": "sha512-xcinL3AuDJk7VSzsHgb9DvvIXayBbadtMZ4HFPx8rUszbW1MuNMlwYVC4zzCZ6e1sqZpnNS5ZFYOhXqA39T7LQ==",
-      "dev": true
-    },
-    "js-levenshtein": {
-      "version": "1.1.4",
-      "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.4.tgz",
-      "integrity": "sha512-PxfGzSs0ztShKrUYPIn5r0MtyAhYcCwmndozzpz8YObbPnD1jFxzlBGbRnX2mIu6Z13xN6+PTu05TQFnZFlzow==",
-      "dev": true
-    },
-    "js-tokens": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
-      "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+      "version": "2.5.1",
+      "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.1.tgz",
+      "integrity": "sha512-M7kLczedRMYX4L8Mdh4MzyAMM9O5osx+4FcOQuTvr3A9F2D9S5JXheN0ewNbrvK2UatkTRhL5ejGmGSjNMiZuw==",
       "dev": true
     },
-    "js-yaml": {
-      "version": "3.12.1",
-      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.1.tgz",
-      "integrity": "sha512-um46hB9wNOKlwkHgiuyEVAybXBjwFUV0Z/RaHJblRd9DXltue9FTYvzCr9ErQrK9Adz5MU4gHWVaNUfdmrC8qA==",
-      "dev": true,
-      "requires": {
-        "argparse": "^1.0.7",
-        "esprima": "^4.0.0"
-      }
-    },
     "jsbn": {
       "version": "0.1.1",
       "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
       "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=",
       "dev": true
     },
-    "jsesc": {
-      "version": "2.5.2",
-      "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
-      "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
-      "dev": true
-    },
     "json-parse-better-errors": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
       "dev": true
     },
     "json-schema-traverse": {
-      "version": "0.3.1",
-      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz",
-      "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=",
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+      "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
       "dev": true
     },
     "json-stringify-safe": {
       }
     },
     "livereload": {
-      "version": "0.7.0",
-      "resolved": "https://registry.npmjs.org/livereload/-/livereload-0.7.0.tgz",
-      "integrity": "sha512-PHnIGczQEvmCctDvRTWylA+1wSwE0/eFm+LkNhlmlAFus/aCRlVE97UOLOf6TUGLmZyfg7z7twG37ZiOgNJAyQ==",
+      "version": "0.8.0",
+      "resolved": "https://registry.npmjs.org/livereload/-/livereload-0.8.0.tgz",
+      "integrity": "sha512-Hi5Na6VIK3e8zlgOS50fu+iOTKWj5hM0BE7NKpZkwnfWTnktTjA38ZUXa2NlJww8/GrdVhpnxdqlLad5fkO27g==",
       "dev": true,
       "requires": {
-        "chokidar": "^1.7.0",
+        "chokidar": "^2.1.5",
         "opts": ">= 1.2.0",
         "ws": "^1.1.5"
       }
       }
     },
     "loader-runner": {
-      "version": "2.3.1",
-      "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.3.1.tgz",
-      "integrity": "sha512-By6ZFY7ETWOc9RFaAIb23IjJVcM4dvJC/N57nmdz9RSkMXvAXGI7SyVlAw3v8vjtDRlqThgVDVmTnr9fqMlxkw==",
+      "version": "2.4.0",
+      "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz",
+      "integrity": "sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==",
       "dev": true
     },
     "loader-utils": {
       }
     },
     "locate-path": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz",
-      "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=",
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
+      "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
       "dev": true,
       "requires": {
-        "p-locate": "^2.0.0",
+        "p-locate": "^3.0.0",
         "path-exists": "^3.0.0"
       }
     },
     "lodash": {
-      "version": "4.17.10",
-      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz",
-      "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==",
-      "dev": true
-    },
-    "lodash.assign": {
-      "version": "4.2.0",
-      "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz",
-      "integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=",
-      "dev": true
-    },
-    "lodash.clonedeep": {
-      "version": "4.5.0",
-      "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
-      "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=",
-      "dev": true
-    },
-    "lodash.debounce": {
-      "version": "4.0.8",
-      "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
-      "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=",
-      "dev": true
-    },
-    "lodash.mergewith": {
-      "version": "4.6.1",
-      "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz",
-      "integrity": "sha512-eWw5r+PYICtEBgrBE5hhlT6aAa75f411bgDz/ZL2KZqYV03USvucsxcHUIlGTDTECs1eunpI7HOV7U+WLDvNdQ==",
+      "version": "4.17.15",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
+      "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
       "dev": true
     },
     "lodash.tail": {
       "integrity": "sha1-0jM6NtnncXyK0vfKyv7HwytERmQ=",
       "dev": true
     },
-    "loose-envify": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
-      "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
-      "dev": true,
-      "requires": {
-        "js-tokens": "^3.0.0 || ^4.0.0"
-      }
-    },
     "loud-rejection": {
       "version": "1.6.0",
       "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz",
       }
     },
     "lru-cache": {
-      "version": "4.1.3",
-      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.tgz",
-      "integrity": "sha512-fFEhvcgzuIoJVUF8fYr5KR0YqxD238zgObTps31YdADwPPAp82a4M8TrckkWyx7ekNlf9aBcVn81cFwwXngrJA==",
+      "version": "4.1.5",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz",
+      "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==",
       "dev": true,
       "requires": {
         "pseudomap": "^1.0.2",
       }
     },
     "make-dir": {
-      "version": "1.3.0",
-      "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz",
-      "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==",
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
+      "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==",
       "dev": true,
       "requires": {
-        "pify": "^3.0.0"
+        "pify": "^4.0.1",
+        "semver": "^5.6.0"
+      },
+      "dependencies": {
+        "pify": {
+          "version": "4.0.1",
+          "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
+          "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
+          "dev": true
+        }
       }
     },
+    "mamacro": {
+      "version": "0.0.3",
+      "resolved": "https://registry.npmjs.org/mamacro/-/mamacro-0.0.3.tgz",
+      "integrity": "sha512-qMEwh+UujcQ+kbz3T6V+wAmO2U8veoq2w+3wY8MquqwVA3jChfwY+Tk52GZKDfACEPjuZ7r2oJLejwpt8jtwTA==",
+      "dev": true
+    },
     "map-age-cleaner": {
-      "version": "0.1.2",
-      "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.2.tgz",
-      "integrity": "sha512-UN1dNocxQq44IhJyMI4TU8phc2m9BddacHRPRjKGLYaF0jqd3xLz0jS0skpAU9WgYyoR4gHtUpzytNBS385FWQ==",
+      "version": "0.1.3",
+      "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz",
+      "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==",
       "dev": true,
       "requires": {
         "p-defer": "^1.0.0"
       "resolved": "https://registry.npmjs.org/markdown-it-task-lists/-/markdown-it-task-lists-2.1.1.tgz",
       "integrity": "sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA=="
     },
-    "math-random": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.1.tgz",
-      "integrity": "sha1-izqsWIuKZuSXXjzepn97sylgH6w=",
-      "dev": true
-    },
     "md5.js": {
       "version": "1.3.5",
       "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
       "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4="
     },
     "mem": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/mem/-/mem-4.0.0.tgz",
-      "integrity": "sha512-WQxG/5xYc3tMbYLXoXPm81ET2WDULiU5FxbuIoNbJqLOOI8zehXFdZuiUEgfdrU2mVB1pxBZUGlYORSrpuJreA==",
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz",
+      "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==",
       "dev": true,
       "requires": {
         "map-age-cleaner": "^0.1.1",
-        "mimic-fn": "^1.0.0",
-        "p-is-promise": "^1.1.0"
+        "mimic-fn": "^2.0.0",
+        "p-is-promise": "^2.0.0"
       }
     },
     "memory-fs": {
       }
     },
     "micromatch": {
-      "version": "2.3.11",
-      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz",
-      "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=",
-      "dev": true,
-      "requires": {
-        "arr-diff": "^2.0.0",
-        "array-unique": "^0.2.1",
-        "braces": "^1.8.2",
-        "expand-brackets": "^0.1.4",
-        "extglob": "^0.3.1",
-        "filename-regex": "^2.0.0",
-        "is-extglob": "^1.0.0",
-        "is-glob": "^2.0.1",
-        "kind-of": "^3.0.2",
-        "normalize-path": "^2.0.1",
-        "object.omit": "^2.0.0",
-        "parse-glob": "^3.0.4",
-        "regex-cache": "^0.4.2"
+      "version": "3.1.10",
+      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
+      "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==",
+      "dev": true,
+      "requires": {
+        "arr-diff": "^4.0.0",
+        "array-unique": "^0.3.2",
+        "braces": "^2.3.1",
+        "define-property": "^2.0.2",
+        "extend-shallow": "^3.0.2",
+        "extglob": "^2.0.4",
+        "fragment-cache": "^0.2.1",
+        "kind-of": "^6.0.2",
+        "nanomatch": "^1.2.9",
+        "object.pick": "^1.3.0",
+        "regex-not": "^1.0.0",
+        "snapdragon": "^0.8.1",
+        "to-regex": "^3.0.2"
+      },
+      "dependencies": {
+        "kind-of": {
+          "version": "6.0.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
+          "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
+          "dev": true
+        }
       }
     },
     "miller-rabin": {
       }
     },
     "mime-db": {
-      "version": "1.37.0",
-      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz",
-      "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==",
+      "version": "1.40.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz",
+      "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==",
       "dev": true
     },
     "mime-types": {
-      "version": "2.1.21",
-      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz",
-      "integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==",
+      "version": "2.1.24",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz",
+      "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==",
       "dev": true,
       "requires": {
-        "mime-db": "~1.37.0"
+        "mime-db": "1.40.0"
       }
     },
     "mimic-fn": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz",
-      "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==",
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+      "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
       "dev": true
     },
-    "mini-css-extract-plugin": {
-      "version": "0.5.0",
-      "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.5.0.tgz",
-      "integrity": "sha512-IuaLjruM0vMKhUUT51fQdQzBYTX49dLj8w68ALEAe2A4iYNpIC4eMac67mt3NzycvjOlf07/kYxJDc0RTl1Wqw==",
-      "dev": true,
-      "requires": {
-        "loader-utils": "^1.1.0",
-        "schema-utils": "^1.0.0",
-        "webpack-sources": "^1.1.0"
-      },
-      "dependencies": {
-        "ajv": {
-          "version": "6.8.1",
-          "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.8.1.tgz",
-          "integrity": "sha512-eqxCp82P+JfqL683wwsL73XmFs1eG6qjw+RD3YHx+Jll1r0jNd4dh8QG9NYAeNGA/hnZjeEDgtTskgJULbxpWQ==",
-          "dev": true,
-          "requires": {
-            "fast-deep-equal": "^2.0.1",
-            "fast-json-stable-stringify": "^2.0.0",
-            "json-schema-traverse": "^0.4.1",
-            "uri-js": "^4.2.2"
-          }
-        },
-        "fast-deep-equal": {
-          "version": "2.0.1",
-          "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
-          "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=",
-          "dev": true
-        },
-        "json-schema-traverse": {
-          "version": "0.4.1",
-          "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
-          "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
-          "dev": true
-        },
-        "schema-utils": {
-          "version": "1.0.0",
-          "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz",
-          "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==",
-          "dev": true,
-          "requires": {
-            "ajv": "^6.1.0",
-            "ajv-errors": "^1.0.0",
-            "ajv-keywords": "^3.1.0"
-          }
-        }
+    "mini-css-extract-plugin": {
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.7.0.tgz",
+      "integrity": "sha512-RQIw6+7utTYn8DBGsf/LpRgZCJMpZt+kuawJ/fju0KiOL6nAaTBNmCJwS7HtwSCXfS47gCkmtBFS7HdsquhdxQ==",
+      "dev": true,
+      "requires": {
+        "loader-utils": "^1.1.0",
+        "normalize-url": "1.9.1",
+        "schema-utils": "^1.0.0",
+        "webpack-sources": "^1.1.0"
       }
     },
     "minimalistic-assert": {
         "pumpify": "^1.3.3",
         "stream-each": "^1.1.0",
         "through2": "^2.0.0"
-      },
-      "dependencies": {
-        "pump": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
-          "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
-          "dev": true,
-          "requires": {
-            "end-of-stream": "^1.1.0",
-            "once": "^1.3.1"
-          }
-        }
       }
     },
     "mixin-deep": {
-      "version": "1.3.1",
-      "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz",
-      "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==",
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz",
+      "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==",
       "dev": true,
       "requires": {
         "for-in": "^1.0.2",
     "ms": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
-      "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+      "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+      "dev": true
     },
     "nan": {
-      "version": "2.11.1",
-      "resolved": "https://registry.npmjs.org/nan/-/nan-2.11.1.tgz",
-      "integrity": "sha512-iji6k87OSXa0CcrLl9z+ZiYSuR2o+c0bGuNmXdrhTQTakxytAFsC56SArGYoiHlJlFoHSnvmhpceZJaXkVuOtA==",
+      "version": "2.14.0",
+      "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz",
+      "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==",
       "dev": true
     },
     "nanomatch": {
         "to-regex": "^3.0.1"
       },
       "dependencies": {
-        "arr-diff": {
-          "version": "4.0.0",
-          "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
-          "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=",
-          "dev": true
-        },
-        "array-unique": {
-          "version": "0.3.2",
-          "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz",
-          "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=",
-          "dev": true
-        },
         "kind-of": {
           "version": "6.0.2",
           "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
       }
     },
     "node-libs-browser": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.1.0.tgz",
-      "integrity": "sha512-5AzFzdoIMb89hBGMZglEegffzgRg+ZFoUmisQ8HI4j1KDdpx13J0taNp2y9xPbur6W61gepGDDotGBVQ7mfUCg==",
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.0.tgz",
+      "integrity": "sha512-5MQunG/oyOaBdttrL40dA7bUfPORLRWMUJLQtMg7nluxUvk5XwnLdL9twQHFAjRx/y7mIMkLKT9++qPbbk6BZA==",
       "dev": true,
       "requires": {
         "assert": "^1.1.1",
         "constants-browserify": "^1.0.0",
         "crypto-browserify": "^3.11.0",
         "domain-browser": "^1.1.1",
-        "events": "^1.0.0",
+        "events": "^3.0.0",
         "https-browserify": "^1.0.0",
         "os-browserify": "^0.3.0",
         "path-browserify": "0.0.0",
         "timers-browserify": "^2.0.4",
         "tty-browserify": "0.0.0",
         "url": "^0.11.0",
-        "util": "^0.10.3",
+        "util": "^0.11.0",
         "vm-browserify": "0.0.4"
       }
     },
-    "node-releases": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.0.4.tgz",
-      "integrity": "sha512-GqRV9GcHw8JCRDaP/JoeNMNzEGzHAknMvIHqMb2VeTOmg1Cf9+ej8bkV12tHfzWHQMCkQ5zUFgwFUkfraynNCw==",
-      "dev": true,
-      "requires": {
-        "semver": "^5.3.0"
-      }
-    },
     "node-sass": {
-      "version": "4.10.0",
-      "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.10.0.tgz",
-      "integrity": "sha512-fDQJfXszw6vek63Fe/ldkYXmRYK/QS6NbvM3i5oEo9ntPDy4XX7BcKZyTKv+/kSSxRtXXc7l+MSwEmYc0CSy6Q==",
+      "version": "4.12.0",
+      "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.12.0.tgz",
+      "integrity": "sha512-A1Iv4oN+Iel6EPv77/HddXErL2a+gZ4uBeZUy+a8O35CFYTXhgA8MgLCWBtwpGZdCvTvQ9d+bQxX/QC36GDPpQ==",
       "dev": true,
       "requires": {
         "async-foreach": "^0.1.3",
         "get-stdin": "^4.0.1",
         "glob": "^7.0.3",
         "in-publish": "^2.0.0",
-        "lodash.assign": "^4.2.0",
-        "lodash.clonedeep": "^4.3.2",
-        "lodash.mergewith": "^4.6.0",
+        "lodash": "^4.17.11",
         "meow": "^3.7.0",
         "mkdirp": "^0.5.1",
-        "nan": "^2.10.0",
+        "nan": "^2.13.2",
         "node-gyp": "^3.8.0",
         "npmlog": "^4.0.0",
         "request": "^2.88.0",
       }
     },
     "normalize-path": {
-      "version": "2.1.1",
-      "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
-      "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=",
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+      "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+      "dev": true
+    },
+    "normalize-url": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-1.9.1.tgz",
+      "integrity": "sha1-LMDWazHqIwNkWENuNiDYWVTGbDw=",
       "dev": true,
       "requires": {
-        "remove-trailing-separator": "^1.0.1"
+        "object-assign": "^4.0.1",
+        "prepend-http": "^1.0.0",
+        "query-string": "^4.1.0",
+        "sort-keys": "^1.0.0"
       }
     },
-    "normalize-range": {
-      "version": "0.1.2",
-      "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
-      "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=",
-      "dev": true
-    },
     "npm-run-all": {
       "version": "4.1.5",
       "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz",
         "set-blocking": "~2.0.0"
       }
     },
-    "num2fraction": {
-      "version": "1.2.2",
-      "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz",
-      "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=",
-      "dev": true
-    },
     "number-is-nan": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
       "dev": true,
       "requires": {
         "isobject": "^3.0.0"
-      },
-      "dependencies": {
-        "isobject": {
-          "version": "3.0.1",
-          "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
-          "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
-          "dev": true
-        }
-      }
-    },
-    "object.getownpropertydescriptors": {
-      "version": "2.0.3",
-      "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz",
-      "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=",
-      "dev": true,
-      "requires": {
-        "define-properties": "^1.1.2",
-        "es-abstract": "^1.5.1"
-      }
-    },
-    "object.omit": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz",
-      "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=",
-      "dev": true,
-      "requires": {
-        "for-own": "^0.1.4",
-        "is-extendable": "^0.1.1"
       }
     },
     "object.pick": {
       "dev": true,
       "requires": {
         "isobject": "^3.0.1"
-      },
-      "dependencies": {
-        "isobject": {
-          "version": "3.0.1",
-          "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
-          "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
-          "dev": true
-        }
       }
     },
     "once": {
       "dev": true
     },
     "p-is-promise": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-1.1.0.tgz",
-      "integrity": "sha1-nJRWmJ6fZYgBewQ01WCXZ1w9oF4=",
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz",
+      "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==",
       "dev": true
     },
     "p-limit": {
-      "version": "1.3.0",
-      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz",
-      "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==",
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz",
+      "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==",
       "dev": true,
       "requires": {
-        "p-try": "^1.0.0"
+        "p-try": "^2.0.0"
       }
     },
     "p-locate": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz",
-      "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=",
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
+      "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
       "dev": true,
       "requires": {
-        "p-limit": "^1.1.0"
+        "p-limit": "^2.0.0"
       }
     },
     "p-try": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz",
-      "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=",
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+      "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
       "dev": true
     },
     "pako": {
-      "version": "1.0.6",
-      "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.6.tgz",
-      "integrity": "sha512-lQe48YPsMJAig+yngZ87Lus+NF+3mtu7DVOBu6b/gHO1YpKwIj5AWjZ/TOS7i46HD/UixzWb1zeWDZfGZ3iYcg==",
+      "version": "1.0.10",
+      "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.10.tgz",
+      "integrity": "sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw==",
       "dev": true
     },
     "parallel-transform": {
       }
     },
     "parse-asn1": {
-      "version": "5.1.1",
-      "resolved": "http://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz",
-      "integrity": "sha512-KPx7flKXg775zZpnp9SxJlz00gTd4BmJ2yJufSc44gMCRrRQ7NSzAcSJQfifuOLgW6bEi+ftrALtsgALeB2Adw==",
+      "version": "5.1.4",
+      "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.4.tgz",
+      "integrity": "sha512-Qs5duJcuvNExRfFZ99HDD3z4mAi3r9Wl/FOjEOijlxwCZs7E7mW2vjTpgQ4J8LpTF8x5v+1Vn5UQFejmWT11aw==",
       "dev": true,
       "requires": {
         "asn1.js": "^4.0.0",
         "browserify-aes": "^1.0.0",
         "create-hash": "^1.1.0",
         "evp_bytestokey": "^1.0.0",
-        "pbkdf2": "^3.0.3"
-      }
-    },
-    "parse-glob": {
-      "version": "3.0.4",
-      "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz",
-      "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=",
-      "dev": true,
-      "requires": {
-        "glob-base": "^0.3.0",
-        "is-dotfile": "^1.0.0",
-        "is-extglob": "^1.0.0",
-        "is-glob": "^2.0.0"
+        "pbkdf2": "^3.0.3",
+        "safe-buffer": "^5.1.1"
       }
     },
     "parse-json": {
         "error-ex": "^1.2.0"
       }
     },
+    "parse-passwd": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz",
+      "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=",
+      "dev": true
+    },
     "pascalcase": {
       "version": "0.1.1",
       "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz",
       "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=",
       "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==",
-      "dev": true
-    },
     "path-type": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz",
       }
     },
     "pkg-dir": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz",
-      "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=",
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz",
+      "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==",
       "dev": true,
       "requires": {
-        "find-up": "^2.1.0"
+        "find-up": "^3.0.0"
       }
     },
     "posix-character-classes": {
       "dev": true
     },
     "postcss": {
-      "version": "7.0.14",
-      "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.14.tgz",
-      "integrity": "sha512-NsbD6XUUMZvBxtQAJuWDJeeC4QFsmWsfozWxCJPWf3M55K9iu2iMDaKqyoOdTJ1R4usBXuxlVFAIo8rZPQD4Bg==",
+      "version": "7.0.16",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.16.tgz",
+      "integrity": "sha512-MOo8zNSlIqh22Uaa3drkdIAgUGEL+AD1ESiSdmElLUmE2uVDo1QloiT/IfW9qRw8Gw+Y/w69UVMGwbufMSftxA==",
       "dev": true,
       "requires": {
         "chalk": "^2.4.2",
         }
       }
     },
-    "postcss-load-config": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-2.0.0.tgz",
-      "integrity": "sha512-V5JBLzw406BB8UIfsAWSK2KSwIJ5yoEIVFb4gVkXci0QdKgA24jLmHZ/ghe/GgX0lJ0/D1uUK1ejhzEY94MChQ==",
-      "dev": true,
-      "requires": {
-        "cosmiconfig": "^4.0.0",
-        "import-cwd": "^2.0.0"
-      }
-    },
-    "postcss-loader": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-3.0.0.tgz",
-      "integrity": "sha512-cLWoDEY5OwHcAjDnkyRQzAXfs2jrKjXpO/HQFcc5b5u/r7aa471wdmChmwfnv7x2u840iat/wi0lQ5nbRgSkUA==",
-      "dev": true,
-      "requires": {
-        "loader-utils": "^1.1.0",
-        "postcss": "^7.0.0",
-        "postcss-load-config": "^2.0.0",
-        "schema-utils": "^1.0.0"
-      },
-      "dependencies": {
-        "ajv": {
-          "version": "6.8.1",
-          "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.8.1.tgz",
-          "integrity": "sha512-eqxCp82P+JfqL683wwsL73XmFs1eG6qjw+RD3YHx+Jll1r0jNd4dh8QG9NYAeNGA/hnZjeEDgtTskgJULbxpWQ==",
-          "dev": true,
-          "requires": {
-            "fast-deep-equal": "^2.0.1",
-            "fast-json-stable-stringify": "^2.0.0",
-            "json-schema-traverse": "^0.4.1",
-            "uri-js": "^4.2.2"
-          }
-        },
-        "fast-deep-equal": {
-          "version": "2.0.1",
-          "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
-          "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=",
-          "dev": true
-        },
-        "json-schema-traverse": {
-          "version": "0.4.1",
-          "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
-          "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
-          "dev": true
-        },
-        "schema-utils": {
-          "version": "1.0.0",
-          "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz",
-          "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==",
-          "dev": true,
-          "requires": {
-            "ajv": "^6.1.0",
-            "ajv-errors": "^1.0.0",
-            "ajv-keywords": "^3.1.0"
-          }
-        }
-      }
-    },
     "postcss-modules-extract-imports": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz",
       }
     },
     "postcss-modules-local-by-default": {
-      "version": "2.0.4",
-      "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-2.0.4.tgz",
-      "integrity": "sha512-WvuSaTKXUqYJbnT7R3YrsNrHv/C5vRfr5VglS4bFOk0MYT4CLBfc/xgExA+x2RftlYgiBDvWmVs191Xv8S8gZQ==",
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-2.0.6.tgz",
+      "integrity": "sha512-oLUV5YNkeIBa0yQl7EYnxMgy4N6noxmiwZStaEJUSe2xPMcdNc8WmBQuQCx18H5psYbVxz8zoHk0RAAYZXP9gA==",
       "dev": true,
       "requires": {
-        "css-selector-tokenizer": "^0.7.0",
         "postcss": "^7.0.6",
+        "postcss-selector-parser": "^6.0.0",
         "postcss-value-parser": "^3.3.1"
       }
     },
     "postcss-modules-scope": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-2.0.1.tgz",
-      "integrity": "sha512-7+6k9c3/AuZ5c596LJx9n923A/j3nF3ormewYBF1RrIQvjvjXe1xE8V8A1KFyFwXbvnshT6FBZFX0k/F1igneg==",
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-2.1.0.tgz",
+      "integrity": "sha512-91Rjps0JnmtUB0cujlc8KIKCsJXWjzuxGeT/+Q2i2HXKZ7nBUeF9YQTZZTNvHVoNYj1AthsjnGLtqDUE0Op79A==",
       "dev": true,
       "requires": {
-        "css-selector-tokenizer": "^0.7.0",
-        "postcss": "^7.0.6"
+        "postcss": "^7.0.6",
+        "postcss-selector-parser": "^6.0.0"
       }
     },
     "postcss-modules-values": {
         "postcss": "^7.0.6"
       }
     },
+    "postcss-selector-parser": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz",
+      "integrity": "sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg==",
+      "dev": true,
+      "requires": {
+        "cssesc": "^3.0.0",
+        "indexes-of": "^1.0.1",
+        "uniq": "^1.0.1"
+      }
+    },
     "postcss-value-parser": {
       "version": "3.3.1",
       "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
       "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==",
       "dev": true
     },
-    "preserve": {
-      "version": "0.2.0",
-      "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz",
-      "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=",
-      "dev": true
-    },
-    "private": {
-      "version": "0.1.8",
-      "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz",
-      "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==",
+    "prepend-http": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz",
+      "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=",
       "dev": true
     },
     "process": {
       "dev": true
     },
     "psl": {
-      "version": "1.1.29",
-      "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.29.tgz",
-      "integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==",
+      "version": "1.1.32",
+      "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.32.tgz",
+      "integrity": "sha512-MHACAkHpihU/REGGPLj4sEfc/XKW2bheigvHO1dUqjaKigMp1C8+WLQYRGgeKFMsw5PMfegZcaN8IDXK/cD0+g==",
       "dev": true
     },
     "public-encrypt": {
       }
     },
     "pump": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz",
-      "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==",
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
+      "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
       "dev": true,
       "requires": {
         "end-of-stream": "^1.1.0",
         "duplexify": "^3.6.0",
         "inherits": "^2.0.3",
         "pump": "^2.0.0"
+      },
+      "dependencies": {
+        "pump": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz",
+          "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==",
+          "dev": true,
+          "requires": {
+            "end-of-stream": "^1.1.0",
+            "once": "^1.3.1"
+          }
+        }
       }
     },
     "punycode": {
       "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
       "dev": true
     },
+    "query-string": {
+      "version": "4.3.4",
+      "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz",
+      "integrity": "sha1-u7aTucqRXCMlFbIosaArYJBD2+s=",
+      "dev": true,
+      "requires": {
+        "object-assign": "^4.1.0",
+        "strict-uri-encode": "^1.0.0"
+      }
+    },
     "querystring": {
       "version": "0.2.0",
       "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
       "dev": true
     },
     "randombytes": {
-      "version": "2.0.6",
-      "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.6.tgz",
-      "integrity": "sha512-CIQ5OFxf4Jou6uOKe9t1AOgqpeU5fd70A8NPdHSGeYXqXsPe6peOwI0cUl88RWZ6sP1vPMV3avd/R6cZ5/sP1A==",
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
+      "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
       "dev": true,
       "requires": {
         "safe-buffer": "^5.1.0"
       }
     },
     "readdirp": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz",
-      "integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=",
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz",
+      "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==",
       "dev": true,
       "requires": {
-        "graceful-fs": "^4.1.2",
-        "minimatch": "^3.0.2",
-        "readable-stream": "^2.0.2",
-        "set-immediate-shim": "^1.0.1"
+        "graceful-fs": "^4.1.11",
+        "micromatch": "^3.1.10",
+        "readable-stream": "^2.0.2"
       }
     },
     "redent": {
         "strip-indent": "^1.0.1"
       }
     },
-    "regenerate": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz",
-      "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==",
-      "dev": true
-    },
-    "regenerate-unicode-properties": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-7.0.0.tgz",
-      "integrity": "sha512-s5NGghCE4itSlUS+0WUj88G6cfMVMmH8boTPNvABf8od+2dhT9WDlWu8n01raQAJZMOK8Ch6jSexaRO7swd6aw==",
-      "dev": true,
-      "requires": {
-        "regenerate": "^1.4.0"
-      }
-    },
-    "regenerator-runtime": {
-      "version": "0.11.1",
-      "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz",
-      "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==",
-      "dev": true
-    },
-    "regenerator-transform": {
-      "version": "0.13.3",
-      "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.13.3.tgz",
-      "integrity": "sha512-5ipTrZFSq5vU2YoGoww4uaRVAK4wyYC4TSICibbfEPOruUu8FFP7ErV0BjmbIOEpn3O/k9na9UEdYR/3m7N6uA==",
-      "dev": true,
-      "requires": {
-        "private": "^0.1.6"
-      }
-    },
-    "regex-cache": {
-      "version": "0.4.4",
-      "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz",
-      "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==",
-      "dev": true,
-      "requires": {
-        "is-equal-shallow": "^0.1.3"
-      }
-    },
     "regex-not": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz",
         "safe-regex": "^1.1.0"
       }
     },
-    "regexpu-core": {
-      "version": "4.2.0",
-      "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.2.0.tgz",
-      "integrity": "sha512-Z835VSnJJ46CNBttalHD/dB+Sj2ezmY6Xp38npwU87peK6mqOzOpV8eYktdkLTEkzzD+JsTcxd84ozd8I14+rw==",
-      "dev": true,
-      "requires": {
-        "regenerate": "^1.4.0",
-        "regenerate-unicode-properties": "^7.0.0",
-        "regjsgen": "^0.4.0",
-        "regjsparser": "^0.3.0",
-        "unicode-match-property-ecmascript": "^1.0.4",
-        "unicode-match-property-value-ecmascript": "^1.0.2"
-      }
-    },
-    "regjsgen": {
-      "version": "0.4.0",
-      "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.4.0.tgz",
-      "integrity": "sha512-X51Lte1gCYUdlwhF28+2YMO0U6WeN0GLpgpA7LK7mbdDnkQYiwvEpmpe0F/cv5L14EbxgrdayAG3JETBv0dbXA==",
-      "dev": true
-    },
-    "regjsparser": {
-      "version": "0.3.0",
-      "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.3.0.tgz",
-      "integrity": "sha512-zza72oZBBHzt64G7DxdqrOo/30bhHkwMUoT0WqfGu98XLd7N+1tsy5MJ96Bk4MD0y74n629RhmrGW6XlnLLwCA==",
-      "dev": true,
-      "requires": {
-        "jsesc": "~0.5.0"
-      },
-      "dependencies": {
-        "jsesc": {
-          "version": "0.5.0",
-          "resolved": "http://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz",
-          "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=",
-          "dev": true
-        }
-      }
-    },
     "remove-trailing-separator": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
       "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
       "dev": true
     },
-    "require-from-string": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
-      "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
-      "dev": true
-    },
     "require-main-filename": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz",
       "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=",
       "dev": true
     },
-    "resolve": {
-      "version": "1.8.1",
-      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz",
-      "integrity": "sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA==",
-      "dev": true,
-      "requires": {
-        "path-parse": "^1.0.5"
-      }
-    },
     "resolve-cwd": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz",
         "resolve-from": "^3.0.0"
       }
     },
+    "resolve-dir": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz",
+      "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=",
+      "dev": true,
+      "requires": {
+        "expand-tilde": "^2.0.0",
+        "global-modules": "^1.0.0"
+      }
+    },
     "resolve-from": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz",
       }
     },
     "schema-utils": {
-      "version": "0.4.5",
-      "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.5.tgz",
-      "integrity": "sha512-yYrjb9TX2k/J1Y5UNy3KYdZq10xhYcF8nMpAW6o3hy6Q8WSIEf9lJHG/ePnOBfziPM3fvQwfOwa13U/Fh8qTfA==",
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz",
+      "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==",
       "dev": true,
       "requires": {
         "ajv": "^6.1.0",
+        "ajv-errors": "^1.0.0",
         "ajv-keywords": "^3.1.0"
-      },
-      "dependencies": {
-        "ajv": {
-          "version": "6.2.1",
-          "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.2.1.tgz",
-          "integrity": "sha1-KKarxJOiq+D7TIUHrK7bQ/pVBnE=",
-          "dev": true,
-          "requires": {
-            "fast-deep-equal": "^1.0.0",
-            "fast-json-stable-stringify": "^2.0.0",
-            "json-schema-traverse": "^0.3.0"
-          }
-        }
       }
     },
     "scss-tokenizer": {
       "dev": true
     },
     "serialize-javascript": {
-      "version": "1.5.0",
-      "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.5.0.tgz",
-      "integrity": "sha512-Ga8c8NjAAp46Br4+0oZ2WxJCwIzwP60Gq1YPgU+39PiTVxyed/iKE/zyZI6+UlVYH5Q4PaQdHhcegIFPZTUfoQ==",
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.7.0.tgz",
+      "integrity": "sha512-ke8UG8ulpFOxO8f8gRYabHQe/ZntKlcig2Mp+8+URDP1D8vJZ0KUt7LYo07q25Z/+JVSgpr/cui9PIp5H6/+nA==",
       "dev": true
     },
     "set-blocking": {
       "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
       "dev": true
     },
-    "set-immediate-shim": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz",
-      "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=",
-      "dev": true
-    },
     "set-value": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz",
-      "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==",
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz",
+      "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==",
       "dev": true,
       "requires": {
         "extend-shallow": "^2.0.1",
         "use": "^3.1.0"
       },
       "dependencies": {
-        "debug": {
-          "version": "2.6.9",
-          "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
-          "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
-          "dev": true,
-          "requires": {
-            "ms": "2.0.0"
-          }
-        },
         "define-property": {
           "version": "0.2.5",
           "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
             "kind-of": "^6.0.2"
           }
         },
-        "isobject": {
-          "version": "3.0.1",
-          "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
-          "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
-          "dev": true
-        },
         "kind-of": {
           "version": "6.0.2",
           "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
         "kind-of": "^3.2.0"
       }
     },
+    "sort-keys": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz",
+      "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=",
+      "dev": true,
+      "requires": {
+        "is-plain-obj": "^1.0.0"
+      }
+    },
     "sortablejs": {
-      "version": "1.7.0",
-      "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.7.0.tgz",
-      "integrity": "sha1-gKKyNwq9Vo4c7IwnETHvMKkE+ig="
+      "version": "1.9.0",
+      "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.9.0.tgz",
+      "integrity": "sha512-Ot6bYJ6PoqPmpsqQYXjn1+RKrY2NWQvQt/o4jfd/UYwVWndyO5EPO8YHbnm5HIykf8ENsm4JUrdAvolPT86yYA=="
     },
     "source-list-map": {
       "version": "2.0.0",
       }
     },
     "source-map-support": {
-      "version": "0.5.9",
-      "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.9.tgz",
-      "integrity": "sha512-gR6Rw4MvUlYy83vP0vxoVNzM6t8MUXqNuRsuBmBHQDu1Fh6X015FrLdgoDKcNdkwGubozq0P4N0Q37UyFVr1EA==",
+      "version": "0.5.12",
+      "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.12.tgz",
+      "integrity": "sha512-4h2Pbvyy15EE02G+JOZpUCmqWJuqrs+sEkzewTm++BPi7Hvn/HwcqLAcNxYAyI0x13CpPPn+kMjl+hplXMHITQ==",
       "dev": true,
       "requires": {
         "buffer-from": "^1.0.0",
       "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
     },
     "sshpk": {
-      "version": "1.15.2",
-      "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.15.2.tgz",
-      "integrity": "sha512-Ra/OXQtuh0/enyl4ETZAfTaeksa6BXks5ZcjpSUNrjBr0DvrJKX+1fsKDPpT9TBXgHAFsa4510aNVgI8g/+SzA==",
+      "version": "1.16.1",
+      "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz",
+      "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==",
       "dev": true,
       "requires": {
         "asn1": "~0.2.3",
       }
     },
     "stream-browserify": {
-      "version": "2.0.1",
-      "resolved": "http://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz",
-      "integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=",
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz",
+      "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==",
       "dev": true,
       "requires": {
         "inherits": "~2.0.1",
       "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=",
       "dev": true
     },
+    "strict-uri-encode": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz",
+      "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=",
+      "dev": true
+    },
     "string-width": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
       "integrity": "sha512-XK+uv9kWwhZMZ1y7mysB+zoihsEj4wneFWAS5qoiLwzW0WzSqMrrsIy+a3zkQJq0ipFtBpX5W3MqyRIBF/WFGg==",
       "dev": true,
       "requires": {
-        "loader-utils": "^1.1.0",
-        "schema-utils": "^1.0.0"
-      },
-      "dependencies": {
-        "ajv": {
-          "version": "6.8.1",
-          "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.8.1.tgz",
-          "integrity": "sha512-eqxCp82P+JfqL683wwsL73XmFs1eG6qjw+RD3YHx+Jll1r0jNd4dh8QG9NYAeNGA/hnZjeEDgtTskgJULbxpWQ==",
-          "dev": true,
-          "requires": {
-            "fast-deep-equal": "^2.0.1",
-            "fast-json-stable-stringify": "^2.0.0",
-            "json-schema-traverse": "^0.4.1",
-            "uri-js": "^4.2.2"
-          }
-        },
-        "fast-deep-equal": {
-          "version": "2.0.1",
-          "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
-          "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=",
-          "dev": true
-        },
-        "json-schema-traverse": {
-          "version": "0.4.1",
-          "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
-          "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
-          "dev": true
-        },
-        "schema-utils": {
-          "version": "1.0.0",
-          "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz",
-          "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==",
-          "dev": true,
-          "requires": {
-            "ajv": "^6.1.0",
-            "ajv-errors": "^1.0.0",
-            "ajv-keywords": "^3.1.0"
-          }
-        }
+        "loader-utils": "^1.1.0",
+        "schema-utils": "^1.0.0"
       }
     },
     "supports-color": {
       }
     },
     "tapable": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.0.tgz",
-      "integrity": "sha512-IlqtmLVaZA2qab8epUXbVWRn3aB1imbDMJtjB3nu4X0NqPkcY/JH9ZtCBWKHWPxs8Svi9tyo8w2dBoi07qZbBA==",
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz",
+      "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==",
       "dev": true
     },
     "tar": {
-      "version": "2.2.1",
-      "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz",
-      "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=",
+      "version": "2.2.2",
+      "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz",
+      "integrity": "sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==",
       "dev": true,
       "requires": {
         "block-stream": "*",
-        "fstream": "^1.0.2",
+        "fstream": "^1.0.12",
         "inherits": "2"
       }
     },
     "terser": {
-      "version": "3.10.12",
-      "resolved": "https://registry.npmjs.org/terser/-/terser-3.10.12.tgz",
-      "integrity": "sha512-3ODPC1eVt25EVNb04s/PkHxOmzKBQUF6bwwuR6h2DbEF8/j265Y1UkwNtOk9am/pRxfJ5HPapOlUlO6c16mKQQ==",
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/terser/-/terser-4.0.0.tgz",
+      "integrity": "sha512-dOapGTU0hETFl1tCo4t56FN+2jffoKyER9qBGoUFyZ6y7WLoKT0bF+lAYi6B6YsILcGF3q1C2FBh8QcKSCgkgA==",
       "dev": true,
       "requires": {
-        "commander": "~2.17.1",
+        "commander": "^2.19.0",
         "source-map": "~0.6.1",
-        "source-map-support": "~0.5.6"
-      },
-      "dependencies": {
-        "commander": {
-          "version": "2.17.1",
-          "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz",
-          "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==",
-          "dev": true
-        }
+        "source-map-support": "~0.5.10"
       }
     },
     "terser-webpack-plugin": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.1.0.tgz",
-      "integrity": "sha512-61lV0DSxMAZ8AyZG7/A4a3UPlrbOBo8NIQ4tJzLPAdGOQ+yoNC7l5ijEow27lBAL2humer01KLS6bGIMYQxKoA==",
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.3.0.tgz",
+      "integrity": "sha512-W2YWmxPjjkUcOWa4pBEv4OP4er1aeQJlSo2UhtCFQCuRXEHjOFscO8VyWHj9JLlA0RzQb8Y2/Ta78XZvT54uGg==",
       "dev": true,
       "requires": {
-        "cacache": "^11.0.2",
+        "cacache": "^11.3.2",
         "find-cache-dir": "^2.0.0",
+        "is-wsl": "^1.1.0",
+        "loader-utils": "^1.2.3",
         "schema-utils": "^1.0.0",
-        "serialize-javascript": "^1.4.0",
+        "serialize-javascript": "^1.7.0",
         "source-map": "^0.6.1",
-        "terser": "^3.8.1",
-        "webpack-sources": "^1.1.0",
-        "worker-farm": "^1.5.2"
+        "terser": "^4.0.0",
+        "webpack-sources": "^1.3.0",
+        "worker-farm": "^1.7.0"
       },
       "dependencies": {
-        "ajv": {
-          "version": "6.5.5",
-          "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.5.tgz",
-          "integrity": "sha512-7q7gtRQDJSyuEHjuVgHoUa2VuemFiCMrfQc9Tc08XTAc4Zj/5U1buQJ0HU6i7fKjXU09SVgSmxa4sLvuvS8Iyg==",
-          "dev": true,
-          "requires": {
-            "fast-deep-equal": "^2.0.1",
-            "fast-json-stable-stringify": "^2.0.0",
-            "json-schema-traverse": "^0.4.1",
-            "uri-js": "^4.2.2"
-          }
-        },
-        "cacache": {
-          "version": "11.3.1",
-          "resolved": "https://registry.npmjs.org/cacache/-/cacache-11.3.1.tgz",
-          "integrity": "sha512-2PEw4cRRDu+iQvBTTuttQifacYjLPhET+SYO/gEFMy8uhi+jlJREDAjSF5FWSdV/Aw5h18caHA7vMTw2c+wDzA==",
-          "dev": true,
-          "requires": {
-            "bluebird": "^3.5.1",
-            "chownr": "^1.0.1",
-            "figgy-pudding": "^3.1.0",
-            "glob": "^7.1.2",
-            "graceful-fs": "^4.1.11",
-            "lru-cache": "^4.1.3",
-            "mississippi": "^3.0.0",
-            "mkdirp": "^0.5.1",
-            "move-concurrently": "^1.0.1",
-            "promise-inflight": "^1.0.1",
-            "rimraf": "^2.6.2",
-            "ssri": "^6.0.0",
-            "unique-filename": "^1.1.0",
-            "y18n": "^4.0.0"
-          }
-        },
-        "fast-deep-equal": {
-          "version": "2.0.1",
-          "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
-          "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=",
+        "big.js": {
+          "version": "5.2.2",
+          "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
+          "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==",
           "dev": true
         },
-        "find-cache-dir": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.0.0.tgz",
-          "integrity": "sha512-LDUY6V1Xs5eFskUVYtIwatojt6+9xC9Chnlk/jYOOvn3FAFfSaWddxahDGyNHh0b2dMXa6YW2m0tk8TdVaXHlA==",
+        "json5": {
+          "version": "1.0.1",
+          "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
+          "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
           "dev": true,
           "requires": {
-            "commondir": "^1.0.1",
-            "make-dir": "^1.0.0",
-            "pkg-dir": "^3.0.0"
+            "minimist": "^1.2.0"
           }
         },
-        "find-up": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
-          "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
+        "loader-utils": {
+          "version": "1.2.3",
+          "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz",
+          "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==",
           "dev": true,
           "requires": {
-            "locate-path": "^3.0.0"
+            "big.js": "^5.2.2",
+            "emojis-list": "^2.0.0",
+            "json5": "^1.0.1"
           }
         },
-        "json-schema-traverse": {
-          "version": "0.4.1",
-          "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
-          "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+        "minimist": {
+          "version": "1.2.0",
+          "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+          "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
           "dev": true
-        },
-        "locate-path": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
-          "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
-          "dev": true,
-          "requires": {
-            "p-locate": "^3.0.0",
-            "path-exists": "^3.0.0"
-          }
-        },
-        "mississippi": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz",
-          "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==",
-          "dev": true,
-          "requires": {
-            "concat-stream": "^1.5.0",
-            "duplexify": "^3.4.2",
-            "end-of-stream": "^1.1.0",
-            "flush-write-stream": "^1.0.0",
-            "from2": "^2.1.0",
-            "parallel-transform": "^1.1.0",
-            "pump": "^3.0.0",
-            "pumpify": "^1.3.3",
-            "stream-each": "^1.1.0",
-            "through2": "^2.0.0"
-          }
-        },
-        "p-limit": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.0.0.tgz",
-          "integrity": "sha512-fl5s52lI5ahKCernzzIyAP0QAZbGIovtVHGwpcu1Jr/EpzLVDI2myISHwGqK7m8uQFugVWSrbxH7XnhGtvEc+A==",
-          "dev": true,
-          "requires": {
-            "p-try": "^2.0.0"
-          }
-        },
-        "p-locate": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
-          "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
-          "dev": true,
-          "requires": {
-            "p-limit": "^2.0.0"
-          }
-        },
-        "p-try": {
+        }
+      }
+    },
+    "through2": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz",
+      "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==",
+      "dev": true,
+      "requires": {
+        "readable-stream": "~2.3.6",
+        "xtend": "~4.0.1"
+      },
+      "dependencies": {
+        "process-nextick-args": {
           "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.0.0.tgz",
-          "integrity": "sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ==",
+          "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
+          "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==",
           "dev": true
         },
-        "pkg-dir": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz",
-          "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==",
-          "dev": true,
-          "requires": {
-            "find-up": "^3.0.0"
-          }
-        },
-        "pump": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
-          "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
-          "dev": true,
-          "requires": {
-            "end-of-stream": "^1.1.0",
-            "once": "^1.3.1"
-          }
-        },
-        "schema-utils": {
-          "version": "1.0.0",
-          "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz",
-          "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==",
+        "readable-stream": {
+          "version": "2.3.6",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
+          "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
           "dev": true,
           "requires": {
-            "ajv": "^6.1.0",
-            "ajv-errors": "^1.0.0",
-            "ajv-keywords": "^3.1.0"
+            "core-util-is": "~1.0.0",
+            "inherits": "~2.0.3",
+            "isarray": "~1.0.0",
+            "process-nextick-args": "~2.0.0",
+            "safe-buffer": "~5.1.1",
+            "string_decoder": "~1.1.1",
+            "util-deprecate": "~1.0.1"
           }
         },
-        "ssri": {
-          "version": "6.0.1",
-          "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz",
-          "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==",
+        "string_decoder": {
+          "version": "1.1.1",
+          "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+          "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
           "dev": true,
           "requires": {
-            "figgy-pudding": "^3.5.1"
+            "safe-buffer": "~5.1.0"
           }
-        },
-        "y18n": {
-          "version": "4.0.0",
-          "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
-          "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==",
-          "dev": true
         }
       }
     },
-    "through2": {
-      "version": "2.0.3",
-      "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz",
-      "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=",
-      "dev": true,
-      "requires": {
-        "readable-stream": "^2.1.5",
-        "xtend": "~4.0.1"
-      }
-    },
     "timers-browserify": {
       "version": "2.0.10",
       "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.10.tgz",
       "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=",
       "dev": true
     },
-    "to-fast-properties": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
-      "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=",
-      "dev": true
-    },
     "to-object-path": {
       "version": "0.3.0",
       "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz",
       "requires": {
         "is-number": "^3.0.0",
         "repeat-string": "^1.6.1"
-      },
-      "dependencies": {
-        "is-number": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
-          "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
-          "dev": true,
-          "requires": {
-            "kind-of": "^3.0.2"
-          }
-        }
       }
     },
     "tough-cookie": {
-      "version": "2.4.3",
-      "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
-      "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==",
-      "dev": true,
-      "requires": {
-        "psl": "^1.1.24",
-        "punycode": "^1.4.1"
-      }
-    },
-    "trim-newlines": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz",
-      "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=",
-      "dev": true
-    },
-    "trim-right": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz",
-      "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=",
-      "dev": true
-    },
-    "true-case-path": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.3.tgz",
-      "integrity": "sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew==",
-      "dev": true,
-      "requires": {
-        "glob": "^7.1.2"
-      }
-    },
-    "tslib": {
-      "version": "1.9.3",
-      "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz",
-      "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==",
-      "dev": true
-    },
-    "tty-browserify": {
-      "version": "0.0.0",
-      "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz",
-      "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=",
-      "dev": true
-    },
-    "tunnel-agent": {
-      "version": "0.6.0",
-      "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
-      "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
-      "dev": true,
-      "requires": {
-        "safe-buffer": "^5.0.1"
-      }
-    },
-    "tweetnacl": {
-      "version": "0.14.5",
-      "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
-      "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
-      "dev": true
-    },
-    "typedarray": {
-      "version": "0.0.6",
-      "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
-      "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=",
-      "dev": true
-    },
-    "uc.micro": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.5.tgz",
-      "integrity": "sha512-JoLI4g5zv5qNyT09f4YAvEZIIV1oOjqnewYg5D38dkQljIzpPT296dbIGvKro3digYI1bkb7W6EP1y4uDlmzLg=="
-    },
-    "uglify-js": {
-      "version": "3.4.9",
-      "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.9.tgz",
-      "integrity": "sha512-8CJsbKOtEbnJsTyv6LE6m6ZKniqMiFWmm9sRbopbkGs3gMPPfd3Fh8iIA4Ykv5MgaTbqHr4BaoGLJLZNhsrW1Q==",
-      "dev": true,
-      "requires": {
-        "commander": "~2.17.1",
-        "source-map": "~0.6.1"
-      }
-    },
-    "uglifyjs-webpack-plugin": {
-      "version": "2.1.1",
-      "resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-2.1.1.tgz",
-      "integrity": "sha512-TQEcyMNkObX/H+FfcKjiDgs5RcXX8vW2UUUrDTOfQgg3lrafztfeM5WAwXo+AzqozJK6NP9w98xNpG/dutzSsg==",
-      "dev": true,
-      "requires": {
-        "cacache": "^11.2.0",
-        "find-cache-dir": "^2.0.0",
-        "schema-utils": "^1.0.0",
-        "serialize-javascript": "^1.4.0",
-        "source-map": "^0.6.1",
-        "uglify-js": "^3.0.0",
-        "webpack-sources": "^1.1.0",
-        "worker-farm": "^1.5.2"
-      },
-      "dependencies": {
-        "ajv": {
-          "version": "6.8.1",
-          "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.8.1.tgz",
-          "integrity": "sha512-eqxCp82P+JfqL683wwsL73XmFs1eG6qjw+RD3YHx+Jll1r0jNd4dh8QG9NYAeNGA/hnZjeEDgtTskgJULbxpWQ==",
-          "dev": true,
-          "requires": {
-            "fast-deep-equal": "^2.0.1",
-            "fast-json-stable-stringify": "^2.0.0",
-            "json-schema-traverse": "^0.4.1",
-            "uri-js": "^4.2.2"
-          }
-        },
-        "fast-deep-equal": {
-          "version": "2.0.1",
-          "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
-          "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=",
-          "dev": true
-        },
-        "find-cache-dir": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.0.0.tgz",
-          "integrity": "sha512-LDUY6V1Xs5eFskUVYtIwatojt6+9xC9Chnlk/jYOOvn3FAFfSaWddxahDGyNHh0b2dMXa6YW2m0tk8TdVaXHlA==",
-          "dev": true,
-          "requires": {
-            "commondir": "^1.0.1",
-            "make-dir": "^1.0.0",
-            "pkg-dir": "^3.0.0"
-          }
-        },
-        "find-up": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
-          "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
-          "dev": true,
-          "requires": {
-            "locate-path": "^3.0.0"
-          }
-        },
-        "json-schema-traverse": {
-          "version": "0.4.1",
-          "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
-          "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
-          "dev": true
-        },
-        "locate-path": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
-          "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
-          "dev": true,
-          "requires": {
-            "p-locate": "^3.0.0",
-            "path-exists": "^3.0.0"
-          }
-        },
-        "p-limit": {
-          "version": "2.1.0",
-          "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.1.0.tgz",
-          "integrity": "sha512-NhURkNcrVB+8hNfLuysU8enY5xn2KXphsHBaC2YmRNTZRc7RWusw6apSpdEj3jo4CMb6W9nrF6tTnsJsJeyu6g==",
-          "dev": true,
-          "requires": {
-            "p-try": "^2.0.0"
-          }
-        },
-        "p-locate": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
-          "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
-          "dev": true,
-          "requires": {
-            "p-limit": "^2.0.0"
-          }
-        },
-        "p-try": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.0.0.tgz",
-          "integrity": "sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ==",
-          "dev": true
-        },
-        "pkg-dir": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz",
-          "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==",
-          "dev": true,
-          "requires": {
-            "find-up": "^3.0.0"
-          }
-        },
-        "schema-utils": {
-          "version": "1.0.0",
-          "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz",
-          "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==",
-          "dev": true,
-          "requires": {
-            "ajv": "^6.1.0",
-            "ajv-errors": "^1.0.0",
-            "ajv-keywords": "^3.1.0"
-          }
-        }
+      "version": "2.4.3",
+      "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
+      "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==",
+      "dev": true,
+      "requires": {
+        "psl": "^1.1.24",
+        "punycode": "^1.4.1"
       }
     },
-    "ultron": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.0.2.tgz",
-      "integrity": "sha1-rOEWq1V80Zc4ak6I9GhTeMiy5Po=",
+    "trim-newlines": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz",
+      "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=",
       "dev": true
     },
-    "unicode-canonical-property-names-ecmascript": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz",
-      "integrity": "sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==",
+    "true-case-path": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.3.tgz",
+      "integrity": "sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew==",
+      "dev": true,
+      "requires": {
+        "glob": "^7.1.2"
+      }
+    },
+    "tslib": {
+      "version": "1.9.3",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz",
+      "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==",
       "dev": true
     },
-    "unicode-match-property-ecmascript": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz",
-      "integrity": "sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==",
+    "tty-browserify": {
+      "version": "0.0.0",
+      "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz",
+      "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=",
+      "dev": true
+    },
+    "tunnel-agent": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
+      "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
       "dev": true,
       "requires": {
-        "unicode-canonical-property-names-ecmascript": "^1.0.4",
-        "unicode-property-aliases-ecmascript": "^1.0.4"
+        "safe-buffer": "^5.0.1"
       }
     },
-    "unicode-match-property-value-ecmascript": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.0.2.tgz",
-      "integrity": "sha512-Rx7yODZC1L/T8XKo/2kNzVAQaRE88AaMvI1EF/Xnj3GW2wzN6fop9DDWuFAKUVFH7vozkz26DzP0qyWLKLIVPQ==",
+    "tweetnacl": {
+      "version": "0.14.5",
+      "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
+      "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
       "dev": true
     },
-    "unicode-property-aliases-ecmascript": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.4.tgz",
-      "integrity": "sha512-2WSLa6OdYd2ng8oqiGIWnJqyFArvhn+5vgx5GTxMbUYjCYKUcuKS62YLFF0R/BDGlB1yzXjQOLtPAfHsgirEpg==",
+    "typedarray": {
+      "version": "0.0.6",
+      "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
+      "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=",
+      "dev": true
+    },
+    "uc.micro": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.5.tgz",
+      "integrity": "sha512-JoLI4g5zv5qNyT09f4YAvEZIIV1oOjqnewYg5D38dkQljIzpPT296dbIGvKro3digYI1bkb7W6EP1y4uDlmzLg=="
+    },
+    "ultron": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.0.2.tgz",
+      "integrity": "sha1-rOEWq1V80Zc4ak6I9GhTeMiy5Po=",
       "dev": true
     },
     "union-value": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz",
-      "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=",
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",
+      "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==",
       "dev": true,
       "requires": {
         "arr-union": "^3.1.0",
         "get-value": "^2.0.6",
         "is-extendable": "^0.1.1",
-        "set-value": "^0.4.3"
-      },
-      "dependencies": {
-        "extend-shallow": {
-          "version": "2.0.1",
-          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
-          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
-          "dev": true,
-          "requires": {
-            "is-extendable": "^0.1.0"
-          }
-        },
-        "set-value": {
-          "version": "0.4.3",
-          "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz",
-          "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=",
-          "dev": true,
-          "requires": {
-            "extend-shallow": "^2.0.1",
-            "is-extendable": "^0.1.1",
-            "is-plain-object": "^2.0.1",
-            "to-object-path": "^0.3.0"
-          }
-        }
+        "set-value": "^2.0.1"
       }
     },
+    "uniq": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz",
+      "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=",
+      "dev": true
+    },
     "unique-filename": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz",
           "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz",
           "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=",
           "dev": true
-        },
-        "isobject": {
-          "version": "3.0.1",
-          "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
-          "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
-          "dev": true
         }
       }
     },
     "upath": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.0.tgz",
-      "integrity": "sha512-bzpH/oBhoS/QI/YtbkqCg6VEiPYjSZtrHQM6/QnJS6OL9pKUFLqb3aFh4Scvwm45+7iAgiMkLhSbaZxUqmrprw==",
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.2.tgz",
+      "integrity": "sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q==",
       "dev": true
     },
     "uri-js": {
           "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=",
           "dev": true
         }
-      }
-    },
-    "use": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
-      "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==",
-      "dev": true
-    },
-    "util": {
-      "version": "0.10.4",
-      "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz",
-      "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==",
-      "dev": true,
-      "requires": {
-        "inherits": "2.0.3"
-      }
-    },
-    "util-deprecate": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
-      "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
-      "dev": true
-    },
-    "util.promisify": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz",
-      "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==",
-      "dev": true,
-      "requires": {
-        "define-properties": "^1.1.2",
-        "object.getownpropertydescriptors": "^2.0.3"
-      }
-    },
-    "uuid": {
-      "version": "3.3.2",
-      "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
-      "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==",
-      "dev": true
-    },
-    "v8-compile-cache": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.0.2.tgz",
-      "integrity": "sha512-1wFuMUIM16MDJRCrpbpuEPTUGmM5QMUg0cr3KFwra2XgOgFcPGDQHDh3CszSCD2Zewc/dh/pamNEW8CbfDebUw==",
-      "dev": true
-    },
-    "validate-npm-package-license": {
-      "version": "3.0.4",
-      "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
-      "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
-      "dev": true,
-      "requires": {
-        "spdx-correct": "^3.0.0",
-        "spdx-expression-parse": "^3.0.0"
-      }
-    },
-    "verror": {
-      "version": "1.10.0",
-      "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
-      "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
-      "dev": true,
-      "requires": {
-        "assert-plus": "^1.0.0",
-        "core-util-is": "1.0.2",
-        "extsprintf": "^1.2.0"
-      }
-    },
-    "vm-browserify": {
-      "version": "0.0.4",
-      "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz",
-      "integrity": "sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=",
-      "dev": true,
-      "requires": {
-        "indexof": "0.0.1"
-      }
-    },
-    "vue": {
-      "version": "2.5.17",
-      "resolved": "https://registry.npmjs.org/vue/-/vue-2.5.17.tgz",
-      "integrity": "sha512-mFbcWoDIJi0w0Za4emyLiW72Jae0yjANHbCVquMKijcavBGypqlF7zHRgMa5k4sesdv7hv2rB4JPdZfR+TPfhQ=="
-    },
-    "vuedraggable": {
-      "version": "2.16.0",
-      "resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-2.16.0.tgz",
-      "integrity": "sha512-fr9gcWKXMJlzbbtJcrQs4kU7qdOZqd4SEpAcx+r0nykbW8AygZN0aKVpadEtI53T8A2azhzCdXMvEqrLuKE2fA==",
-      "requires": {
-        "sortablejs": "^1.7.0"
-      }
-    },
-    "watchpack": {
-      "version": "1.6.0",
-      "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.0.tgz",
-      "integrity": "sha512-i6dHe3EyLjMmDlU1/bGQpEw25XSjkJULPuAVKCbNRefQVq48yXKUpwg538F7AZTf9kyr57zj++pQFltUa5H7yA==",
-      "dev": true,
-      "requires": {
-        "chokidar": "^2.0.2",
-        "graceful-fs": "^4.1.2",
-        "neo-async": "^2.5.0"
-      },
-      "dependencies": {
-        "anymatch": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz",
-          "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==",
-          "dev": true,
-          "requires": {
-            "micromatch": "^3.1.4",
-            "normalize-path": "^2.1.1"
-          }
-        },
-        "arr-diff": {
-          "version": "4.0.0",
-          "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
-          "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=",
-          "dev": true
-        },
-        "array-unique": {
-          "version": "0.3.2",
-          "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz",
-          "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=",
-          "dev": true
-        },
-        "braces": {
-          "version": "2.3.2",
-          "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz",
-          "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==",
-          "dev": true,
-          "requires": {
-            "arr-flatten": "^1.1.0",
-            "array-unique": "^0.3.2",
-            "extend-shallow": "^2.0.1",
-            "fill-range": "^4.0.0",
-            "isobject": "^3.0.1",
-            "repeat-element": "^1.1.2",
-            "snapdragon": "^0.8.1",
-            "snapdragon-node": "^2.0.1",
-            "split-string": "^3.0.2",
-            "to-regex": "^3.0.1"
-          },
-          "dependencies": {
-            "extend-shallow": {
-              "version": "2.0.1",
-              "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
-              "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
-              "dev": true,
-              "requires": {
-                "is-extendable": "^0.1.0"
-              }
-            }
-          }
-        },
-        "chokidar": {
-          "version": "2.0.4",
-          "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz",
-          "integrity": "sha512-z9n7yt9rOvIJrMhvDtDictKrkFHeihkNl6uWMmZlmL6tJtX9Cs+87oK+teBx+JIgzvbX3yZHT3eF8vpbDxHJXQ==",
-          "dev": true,
-          "requires": {
-            "anymatch": "^2.0.0",
-            "async-each": "^1.0.0",
-            "braces": "^2.3.0",
-            "fsevents": "^1.2.2",
-            "glob-parent": "^3.1.0",
-            "inherits": "^2.0.1",
-            "is-binary-path": "^1.0.0",
-            "is-glob": "^4.0.0",
-            "lodash.debounce": "^4.0.8",
-            "normalize-path": "^2.1.1",
-            "path-is-absolute": "^1.0.0",
-            "readdirp": "^2.0.0",
-            "upath": "^1.0.5"
-          }
-        },
-        "debug": {
-          "version": "2.6.9",
-          "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
-          "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
-          "dev": true,
-          "requires": {
-            "ms": "2.0.0"
-          }
-        },
-        "expand-brackets": {
-          "version": "2.1.4",
-          "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz",
-          "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=",
-          "dev": true,
-          "requires": {
-            "debug": "^2.3.3",
-            "define-property": "^0.2.5",
-            "extend-shallow": "^2.0.1",
-            "posix-character-classes": "^0.1.0",
-            "regex-not": "^1.0.0",
-            "snapdragon": "^0.8.1",
-            "to-regex": "^3.0.1"
-          },
-          "dependencies": {
-            "define-property": {
-              "version": "0.2.5",
-              "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
-              "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
-              "dev": true,
-              "requires": {
-                "is-descriptor": "^0.1.0"
-              }
-            },
-            "extend-shallow": {
-              "version": "2.0.1",
-              "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
-              "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
-              "dev": true,
-              "requires": {
-                "is-extendable": "^0.1.0"
-              }
-            },
-            "is-accessor-descriptor": {
-              "version": "0.1.6",
-              "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
-              "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=",
-              "dev": true,
-              "requires": {
-                "kind-of": "^3.0.2"
-              },
-              "dependencies": {
-                "kind-of": {
-                  "version": "3.2.2",
-                  "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
-                  "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
-                  "dev": true,
-                  "requires": {
-                    "is-buffer": "^1.1.5"
-                  }
-                }
-              }
-            },
-            "is-data-descriptor": {
-              "version": "0.1.4",
-              "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz",
-              "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=",
-              "dev": true,
-              "requires": {
-                "kind-of": "^3.0.2"
-              },
-              "dependencies": {
-                "kind-of": {
-                  "version": "3.2.2",
-                  "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
-                  "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
-                  "dev": true,
-                  "requires": {
-                    "is-buffer": "^1.1.5"
-                  }
-                }
-              }
-            },
-            "is-descriptor": {
-              "version": "0.1.6",
-              "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz",
-              "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==",
-              "dev": true,
-              "requires": {
-                "is-accessor-descriptor": "^0.1.6",
-                "is-data-descriptor": "^0.1.4",
-                "kind-of": "^5.0.0"
-              }
-            },
-            "kind-of": {
-              "version": "5.1.0",
-              "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz",
-              "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==",
-              "dev": true
-            }
-          }
-        },
-        "extglob": {
-          "version": "2.0.4",
-          "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz",
-          "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==",
-          "dev": true,
-          "requires": {
-            "array-unique": "^0.3.2",
-            "define-property": "^1.0.0",
-            "expand-brackets": "^2.1.4",
-            "extend-shallow": "^2.0.1",
-            "fragment-cache": "^0.2.1",
-            "regex-not": "^1.0.0",
-            "snapdragon": "^0.8.1",
-            "to-regex": "^3.0.1"
-          },
-          "dependencies": {
-            "define-property": {
-              "version": "1.0.0",
-              "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
-              "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
-              "dev": true,
-              "requires": {
-                "is-descriptor": "^1.0.0"
-              }
-            },
-            "extend-shallow": {
-              "version": "2.0.1",
-              "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
-              "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
-              "dev": true,
-              "requires": {
-                "is-extendable": "^0.1.0"
-              }
-            }
-          }
-        },
-        "fill-range": {
-          "version": "4.0.0",
-          "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
-          "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=",
-          "dev": true,
-          "requires": {
-            "extend-shallow": "^2.0.1",
-            "is-number": "^3.0.0",
-            "repeat-string": "^1.6.1",
-            "to-regex-range": "^2.1.0"
-          },
-          "dependencies": {
-            "extend-shallow": {
-              "version": "2.0.1",
-              "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
-              "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
-              "dev": true,
-              "requires": {
-                "is-extendable": "^0.1.0"
-              }
-            }
-          }
-        },
-        "glob-parent": {
-          "version": "3.1.0",
-          "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz",
-          "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=",
-          "dev": true,
-          "requires": {
-            "is-glob": "^3.1.0",
-            "path-dirname": "^1.0.0"
-          },
-          "dependencies": {
-            "is-glob": {
-              "version": "3.1.0",
-              "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz",
-              "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=",
-              "dev": true,
-              "requires": {
-                "is-extglob": "^2.1.0"
-              }
-            }
-          }
-        },
+      }
+    },
+    "use": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
+      "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==",
+      "dev": true
+    },
+    "util": {
+      "version": "0.11.1",
+      "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz",
+      "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==",
+      "dev": true,
+      "requires": {
+        "inherits": "2.0.3"
+      }
+    },
+    "util-deprecate": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+      "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
+      "dev": true
+    },
+    "uuid": {
+      "version": "3.3.2",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
+      "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==",
+      "dev": true
+    },
+    "v8-compile-cache": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz",
+      "integrity": "sha512-CNmdbwQMBjwr9Gsmohvm0pbL954tJrNzf6gWL3K+QMQf00PF7ERGrEiLgjuU3mKreLC2MeGhUsNV9ybTbLgd3w==",
+      "dev": true
+    },
+    "validate-npm-package-license": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
+      "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
+      "dev": true,
+      "requires": {
+        "spdx-correct": "^3.0.0",
+        "spdx-expression-parse": "^3.0.0"
+      }
+    },
+    "verror": {
+      "version": "1.10.0",
+      "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
+      "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
+      "dev": true,
+      "requires": {
+        "assert-plus": "^1.0.0",
+        "core-util-is": "1.0.2",
+        "extsprintf": "^1.2.0"
+      }
+    },
+    "vm-browserify": {
+      "version": "0.0.4",
+      "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz",
+      "integrity": "sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=",
+      "dev": true,
+      "requires": {
+        "indexof": "0.0.1"
+      }
+    },
+    "vue": {
+      "version": "2.6.10",
+      "resolved": "https://registry.npmjs.org/vue/-/vue-2.6.10.tgz",
+      "integrity": "sha512-ImThpeNU9HbdZL3utgMCq0oiMzAkt1mcgy3/E6zWC/G6AaQoeuFdsl9nDhTDU3X1R6FK7nsIUuRACVcjI+A2GQ=="
+    },
+    "vuedraggable": {
+      "version": "2.21.0",
+      "resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-2.21.0.tgz",
+      "integrity": "sha512-UDp0epjaZikuInoJA9rlEIJaSTQThabq0R9x7TqBdl0qGVFKKzo6glP6ubfzWBmV4iRIfbSOs2DV06s3B5h5tA==",
+      "requires": {
+        "sortablejs": "^1.9.0"
+      }
+    },
+    "watchpack": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.0.tgz",
+      "integrity": "sha512-i6dHe3EyLjMmDlU1/bGQpEw25XSjkJULPuAVKCbNRefQVq48yXKUpwg538F7AZTf9kyr57zj++pQFltUa5H7yA==",
+      "dev": true,
+      "requires": {
+        "chokidar": "^2.0.2",
+        "graceful-fs": "^4.1.2",
+        "neo-async": "^2.5.0"
+      },
+      "dependencies": {
         "is-accessor-descriptor": {
           "version": "1.0.0",
           "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
           "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
-          "dev": true,
           "requires": {
             "kind-of": "^6.0.0"
           }
           "version": "1.0.0",
           "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
           "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
-          "dev": true,
           "requires": {
             "kind-of": "^6.0.0"
           }
           "version": "1.0.2",
           "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
           "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
-          "dev": true,
           "requires": {
             "is-accessor-descriptor": "^1.0.0",
             "is-data-descriptor": "^1.0.0",
             "kind-of": "^6.0.2"
           }
         },
-        "is-extglob": {
-          "version": "2.1.1",
-          "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
-          "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
-          "dev": true
-        },
-        "is-glob": {
-          "version": "4.0.0",
-          "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz",
-          "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=",
-          "dev": true,
-          "requires": {
-            "is-extglob": "^2.1.1"
-          }
-        },
-        "is-number": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
-          "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
-          "dev": true,
-          "requires": {
-            "kind-of": "^3.0.2"
-          },
-          "dependencies": {
-            "kind-of": {
-              "version": "3.2.2",
-              "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
-              "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
-              "dev": true,
-              "requires": {
-                "is-buffer": "^1.1.5"
-              }
-            }
-          }
-        },
-        "isobject": {
-          "version": "3.0.1",
-          "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
-          "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
-          "dev": true
-        },
         "kind-of": {
           "version": "6.0.2",
           "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
-          "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
-          "dev": true
-        },
-        "micromatch": {
-          "version": "3.1.10",
-          "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
-          "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==",
-          "dev": true,
-          "requires": {
-            "arr-diff": "^4.0.0",
-            "array-unique": "^0.3.2",
-            "braces": "^2.3.1",
-            "define-property": "^2.0.2",
-            "extend-shallow": "^3.0.2",
-            "extglob": "^2.0.4",
-            "fragment-cache": "^0.2.1",
-            "kind-of": "^6.0.2",
-            "nanomatch": "^1.2.9",
-            "object.pick": "^1.3.0",
-            "regex-not": "^1.0.0",
-            "snapdragon": "^0.8.1",
-            "to-regex": "^3.0.2"
-          }
+          "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA=="
         }
       }
     },
     "webpack": {
-      "version": "4.26.1",
-      "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.26.1.tgz",
-      "integrity": "sha512-i2oOvEvuvLLSuSCkdVrknaxAhtUZ9g+nLSoHCWV0gDzqGX2DXaCrMmMUpbRsTSSLrUqAI56PoEiyMUZIZ1msug==",
+      "version": "4.32.2",
+      "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.32.2.tgz",
+      "integrity": "sha512-F+H2Aa1TprTQrpodRAWUMJn7A8MgDx82yQiNvYMaj3d1nv3HetKU0oqEulL9huj8enirKi8KvEXQ3QtuHF89Zg==",
       "dev": true,
       "requires": {
-        "@webassemblyjs/ast": "1.7.11",
-        "@webassemblyjs/helper-module-context": "1.7.11",
-        "@webassemblyjs/wasm-edit": "1.7.11",
-        "@webassemblyjs/wasm-parser": "1.7.11",
-        "acorn": "^5.6.2",
-        "acorn-dynamic-import": "^3.0.0",
+        "@webassemblyjs/ast": "1.8.5",
+        "@webassemblyjs/helper-module-context": "1.8.5",
+        "@webassemblyjs/wasm-edit": "1.8.5",
+        "@webassemblyjs/wasm-parser": "1.8.5",
+        "acorn": "^6.0.5",
+        "acorn-dynamic-import": "^4.0.0",
         "ajv": "^6.1.0",
         "ajv-keywords": "^3.1.0",
         "chrome-trace-event": "^1.0.0",
         "mkdirp": "~0.5.0",
         "neo-async": "^2.5.0",
         "node-libs-browser": "^2.0.0",
-        "schema-utils": "^0.4.4",
+        "schema-utils": "^1.0.0",
         "tapable": "^1.1.0",
         "terser-webpack-plugin": "^1.1.0",
         "watchpack": "^1.5.0",
         "webpack-sources": "^1.3.0"
       },
       "dependencies": {
-        "ajv": {
-          "version": "6.5.5",
-          "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.5.tgz",
-          "integrity": "sha512-7q7gtRQDJSyuEHjuVgHoUa2VuemFiCMrfQc9Tc08XTAc4Zj/5U1buQJ0HU6i7fKjXU09SVgSmxa4sLvuvS8Iyg==",
-          "dev": true,
-          "requires": {
-            "fast-deep-equal": "^2.0.1",
-            "fast-json-stable-stringify": "^2.0.0",
-            "json-schema-traverse": "^0.4.1",
-            "uri-js": "^4.2.2"
-          }
-        },
-        "arr-diff": {
-          "version": "4.0.0",
-          "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
-          "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=",
-          "dev": true
-        },
-        "array-unique": {
-          "version": "0.3.2",
-          "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz",
-          "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=",
-          "dev": true
-        },
-        "braces": {
-          "version": "2.3.2",
-          "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz",
-          "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==",
-          "dev": true,
-          "requires": {
-            "arr-flatten": "^1.1.0",
-            "array-unique": "^0.3.2",
-            "extend-shallow": "^2.0.1",
-            "fill-range": "^4.0.0",
-            "isobject": "^3.0.1",
-            "repeat-element": "^1.1.2",
-            "snapdragon": "^0.8.1",
-            "snapdragon-node": "^2.0.1",
-            "split-string": "^3.0.2",
-            "to-regex": "^3.0.1"
-          },
-          "dependencies": {
-            "extend-shallow": {
-              "version": "2.0.1",
-              "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
-              "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
-              "dev": true,
-              "requires": {
-                "is-extendable": "^0.1.0"
-              }
-            }
-          }
-        },
-        "debug": {
-          "version": "2.6.9",
-          "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
-          "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
-          "dev": true,
-          "requires": {
-            "ms": "2.0.0"
-          }
-        },
-        "expand-brackets": {
-          "version": "2.1.4",
-          "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz",
-          "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=",
-          "dev": true,
-          "requires": {
-            "debug": "^2.3.3",
-            "define-property": "^0.2.5",
-            "extend-shallow": "^2.0.1",
-            "posix-character-classes": "^0.1.0",
-            "regex-not": "^1.0.0",
-            "snapdragon": "^0.8.1",
-            "to-regex": "^3.0.1"
-          },
-          "dependencies": {
-            "define-property": {
-              "version": "0.2.5",
-              "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
-              "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
-              "dev": true,
-              "requires": {
-                "is-descriptor": "^0.1.0"
-              }
-            },
-            "extend-shallow": {
-              "version": "2.0.1",
-              "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
-              "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
-              "dev": true,
-              "requires": {
-                "is-extendable": "^0.1.0"
-              }
-            },
-            "is-accessor-descriptor": {
-              "version": "0.1.6",
-              "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
-              "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=",
-              "dev": true,
-              "requires": {
-                "kind-of": "^3.0.2"
-              },
-              "dependencies": {
-                "kind-of": {
-                  "version": "3.2.2",
-                  "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
-                  "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
-                  "dev": true,
-                  "requires": {
-                    "is-buffer": "^1.1.5"
-                  }
-                }
-              }
-            },
-            "is-data-descriptor": {
-              "version": "0.1.4",
-              "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz",
-              "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=",
-              "dev": true,
-              "requires": {
-                "kind-of": "^3.0.2"
-              },
-              "dependencies": {
-                "kind-of": {
-                  "version": "3.2.2",
-                  "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
-                  "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
-                  "dev": true,
-                  "requires": {
-                    "is-buffer": "^1.1.5"
-                  }
-                }
-              }
-            },
-            "is-descriptor": {
-              "version": "0.1.6",
-              "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz",
-              "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==",
-              "dev": true,
-              "requires": {
-                "is-accessor-descriptor": "^0.1.6",
-                "is-data-descriptor": "^0.1.4",
-                "kind-of": "^5.0.0"
-              }
-            },
-            "kind-of": {
-              "version": "5.1.0",
-              "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz",
-              "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==",
-              "dev": true
-            }
-          }
-        },
-        "extglob": {
-          "version": "2.0.4",
-          "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz",
-          "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==",
-          "dev": true,
-          "requires": {
-            "array-unique": "^0.3.2",
-            "define-property": "^1.0.0",
-            "expand-brackets": "^2.1.4",
-            "extend-shallow": "^2.0.1",
-            "fragment-cache": "^0.2.1",
-            "regex-not": "^1.0.0",
-            "snapdragon": "^0.8.1",
-            "to-regex": "^3.0.1"
-          },
-          "dependencies": {
-            "define-property": {
-              "version": "1.0.0",
-              "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
-              "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
-              "dev": true,
-              "requires": {
-                "is-descriptor": "^1.0.0"
-              }
-            },
-            "extend-shallow": {
-              "version": "2.0.1",
-              "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
-              "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
-              "dev": true,
-              "requires": {
-                "is-extendable": "^0.1.0"
-              }
-            }
-          }
-        },
-        "fast-deep-equal": {
-          "version": "2.0.1",
-          "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
-          "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=",
-          "dev": true
-        },
-        "fill-range": {
-          "version": "4.0.0",
-          "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
-          "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=",
-          "dev": true,
-          "requires": {
-            "extend-shallow": "^2.0.1",
-            "is-number": "^3.0.0",
-            "repeat-string": "^1.6.1",
-            "to-regex-range": "^2.1.0"
-          },
-          "dependencies": {
-            "extend-shallow": {
-              "version": "2.0.1",
-              "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
-              "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
-              "dev": true,
-              "requires": {
-                "is-extendable": "^0.1.0"
-              }
-            }
-          }
-        },
         "is-accessor-descriptor": {
           "version": "1.0.0",
           "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
           "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
-          "dev": true,
           "requires": {
             "kind-of": "^6.0.0"
           }
           "version": "1.0.0",
           "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
           "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
-          "dev": true,
           "requires": {
             "kind-of": "^6.0.0"
           }
           "version": "1.0.2",
           "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
           "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
-          "dev": true,
           "requires": {
             "is-accessor-descriptor": "^1.0.0",
             "is-data-descriptor": "^1.0.0",
             "kind-of": "^6.0.2"
           }
         },
-        "is-number": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
-          "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
-          "dev": true,
-          "requires": {
-            "kind-of": "^3.0.2"
-          },
-          "dependencies": {
-            "kind-of": {
-              "version": "3.2.2",
-              "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
-              "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
-              "dev": true,
-              "requires": {
-                "is-buffer": "^1.1.5"
-              }
-            }
-          }
-        },
-        "isobject": {
-          "version": "3.0.1",
-          "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
-          "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
-          "dev": true
-        },
-        "json-schema-traverse": {
-          "version": "0.4.1",
-          "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
-          "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
-          "dev": true
-        },
         "kind-of": {
           "version": "6.0.2",
           "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
-          "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
-          "dev": true
-        },
-        "micromatch": {
-          "version": "3.1.10",
-          "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
-          "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==",
-          "dev": true,
-          "requires": {
-            "arr-diff": "^4.0.0",
-            "array-unique": "^0.3.2",
-            "braces": "^2.3.1",
-            "define-property": "^2.0.2",
-            "extend-shallow": "^3.0.2",
-            "extglob": "^2.0.4",
-            "fragment-cache": "^0.2.1",
-            "kind-of": "^6.0.2",
-            "nanomatch": "^1.2.9",
-            "object.pick": "^1.3.0",
-            "regex-not": "^1.0.0",
-            "snapdragon": "^0.8.1",
-            "to-regex": "^3.0.2"
-          }
-        },
-        "webpack-sources": {
-          "version": "1.3.0",
-          "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.3.0.tgz",
-          "integrity": "sha512-OiVgSrbGu7NEnEvQJJgdSFPl2qWKkWq5lHMhgiToIiN9w34EBnjYzSYs+VbL5KoYiLNtFFa7BZIKxRED3I32pA==",
-          "dev": true,
-          "requires": {
-            "source-list-map": "^2.0.0",
-            "source-map": "~0.6.1"
-          }
+          "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA=="
         }
       }
     },
     "webpack-cli": {
-      "version": "3.1.2",
-      "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.1.2.tgz",
-      "integrity": "sha512-Cnqo7CeqeSvC6PTdts+dywNi5CRlIPbLx1AoUPK2T6vC1YAugMG3IOoO9DmEscd+Dghw7uRlnzV1KwOe5IrtgQ==",
+      "version": "3.3.2",
+      "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.3.2.tgz",
+      "integrity": "sha512-FLkobnaJJ+03j5eplxlI0TUxhGCOdfewspIGuvDVtpOlrAuKMFC57K42Ukxqs1tn8947/PM6tP95gQc0DCzRYA==",
       "dev": true,
       "requires": {
         "chalk": "^2.4.1",
         "cross-spawn": "^6.0.5",
         "enhanced-resolve": "^4.1.0",
-        "global-modules-path": "^2.3.0",
+        "findup-sync": "^2.0.0",
+        "global-modules": "^1.0.0",
         "import-local": "^2.0.0",
         "interpret": "^1.1.0",
         "loader-utils": "^1.1.0",
         "supports-color": "^5.5.0",
         "v8-compile-cache": "^2.0.2",
-        "yargs": "^12.0.2"
+        "yargs": "^12.0.5"
       },
       "dependencies": {
         "ansi-regex": {
           "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
           "dev": true
         },
-        "camelcase": {
-          "version": "4.1.0",
-          "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz",
-          "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=",
-          "dev": true
-        },
         "cliui": {
           "version": "4.1.0",
           "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz",
             "which": "^1.2.9"
           }
         },
-        "decamelize": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-2.0.0.tgz",
-          "integrity": "sha512-Ikpp5scV3MSYxY39ymh45ZLEecsTdv/Xj2CaQfI8RLMuwi7XvjX9H/fhraiSuU+C5w5NTDu4ZU72xNiZnurBPg==",
-          "dev": true,
-          "requires": {
-            "xregexp": "4.0.0"
-          }
-        },
-        "find-up": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
-          "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
-          "dev": true,
-          "requires": {
-            "locate-path": "^3.0.0"
-          }
-        },
         "invert-kv": {
           "version": "2.0.0",
           "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz",
             "invert-kv": "^2.0.0"
           }
         },
-        "locate-path": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
-          "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
-          "dev": true,
-          "requires": {
-            "p-locate": "^3.0.0",
-            "path-exists": "^3.0.0"
-          }
-        },
         "os-locale": {
-          "version": "3.0.1",
-          "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.0.1.tgz",
-          "integrity": "sha512-7g5e7dmXPtzcP4bgsZ8ixDVqA7oWYuEz4lOSujeWyliPai4gfVDiFIcwBg3aGCPnmSGfzOKTK3ccPn0CKv3DBw==",
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz",
+          "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==",
           "dev": true,
           "requires": {
-            "execa": "^0.10.0",
+            "execa": "^1.0.0",
             "lcid": "^2.0.0",
             "mem": "^4.0.0"
           }
         },
-        "p-limit": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.0.0.tgz",
-          "integrity": "sha512-fl5s52lI5ahKCernzzIyAP0QAZbGIovtVHGwpcu1Jr/EpzLVDI2myISHwGqK7m8uQFugVWSrbxH7XnhGtvEc+A==",
-          "dev": true,
-          "requires": {
-            "p-try": "^2.0.0"
-          }
-        },
-        "p-locate": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
-          "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
-          "dev": true,
-          "requires": {
-            "p-limit": "^2.0.0"
-          }
-        },
-        "p-try": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.0.0.tgz",
-          "integrity": "sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ==",
-          "dev": true
-        },
         "string-width": {
           "version": "2.1.1",
           "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
           "dev": true
         },
         "yargs": {
-          "version": "12.0.2",
-          "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.2.tgz",
-          "integrity": "sha512-e7SkEx6N6SIZ5c5H22RTZae61qtn3PYUE8JYbBFlK9sYmh3DMQ6E5ygtaG/2BW0JZi4WGgTR2IV5ChqlqrDGVQ==",
+          "version": "12.0.5",
+          "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz",
+          "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==",
           "dev": true,
           "requires": {
             "cliui": "^4.0.0",
-            "decamelize": "^2.0.0",
+            "decamelize": "^1.2.0",
             "find-up": "^3.0.0",
             "get-caller-file": "^1.0.1",
             "os-locale": "^3.0.0",
             "string-width": "^2.0.0",
             "which-module": "^2.0.0",
             "y18n": "^3.2.1 || ^4.0.0",
-            "yargs-parser": "^10.1.0"
+            "yargs-parser": "^11.1.1"
           }
         },
         "yargs-parser": {
-          "version": "10.1.0",
-          "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz",
-          "integrity": "sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==",
+          "version": "11.1.1",
+          "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz",
+          "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==",
           "dev": true,
           "requires": {
-            "camelcase": "^4.1.0"
+            "camelcase": "^5.0.0",
+            "decamelize": "^1.2.0"
           }
         }
       }
     },
     "webpack-sources": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.1.0.tgz",
-      "integrity": "sha512-aqYp18kPphgoO5c/+NaUvEeACtZjMESmDChuD3NBciVpah3XpMEU9VAAtIaB1BsfJWWTSdv8Vv1m3T0aRk2dUw==",
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.3.0.tgz",
+      "integrity": "sha512-OiVgSrbGu7NEnEvQJJgdSFPl2qWKkWq5lHMhgiToIiN9w34EBnjYzSYs+VbL5KoYiLNtFFa7BZIKxRED3I32pA==",
       "dev": true,
       "requires": {
         "source-list-map": "^2.0.0",
       }
     },
     "worker-farm": {
-      "version": "1.6.0",
-      "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.6.0.tgz",
-      "integrity": "sha512-6w+3tHbM87WnSWnENBUvA2pxJPLhQUg5LKwUQHq3r+XPhIM+Gh2R5ycbwPCyuGbNg+lPgdcnQUhuC02kJCvffQ==",
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.7.0.tgz",
+      "integrity": "sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==",
       "dev": true,
       "requires": {
         "errno": "~0.1.7"
         "ultron": "1.0.x"
       }
     },
-    "xregexp": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.0.0.tgz",
-      "integrity": "sha512-PHyM+sQouu7xspQQwELlGwwd05mXUFqwFYfqPO0cC7x4fxyHnnuetmQr6CjJiafIDoH4MogHb9dOoJzR/Y4rFg==",
-      "dev": true
-    },
     "xtend": {
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz",
index 1cbfdb3e9ba9d62eef4801419ac0ebea20e94d45..0d1afbb0c05031a44f702399e00f0cfb6f8f5c63 100644 (file)
     "permissions": "chown -R $USER:$USER bootstrap/cache storage public/uploads"
   },
   "devDependencies": {
-    "@babel/core": "^7.1.6",
-    "@babel/polyfill": "^7.0.0",
-    "@babel/preset-env": "^7.1.6",
-    "autoprefixer": "^9.4.7",
-    "babel-loader": "^8.0.4",
-    "css-loader": "^2.1.0",
-    "livereload": "^0.7.0",
-    "mini-css-extract-plugin": "^0.5.0",
-    "node-sass": "^4.10.0",
+    "css-loader": "^2.1.1",
+    "livereload": "^0.8.0",
+    "mini-css-extract-plugin": "^0.7.0",
+    "node-sass": "^4.12.0",
     "npm-run-all": "^4.1.5",
-    "postcss-loader": "^3.0.0",
     "sass-loader": "^7.1.0",
     "style-loader": "^0.23.1",
-    "uglifyjs-webpack-plugin": "^2.1.1",
-    "webpack": "^4.26.1",
-    "webpack-cli": "^3.1.2"
+    "webpack": "^4.32.2",
+    "webpack-cli": "^3.3.2"
   },
   "dependencies": {
-    "axios": "^0.18.0",
     "clipboard": "^2.0.4",
-    "codemirror": "^5.42.0",
+    "codemirror": "^5.47.0",
     "dropzone": "^5.5.1",
-    "jquery": "^3.3.1",
-    "jquery-sortable": "^0.9.13",
     "markdown-it": "^8.4.2",
     "markdown-it-task-lists": "^2.1.1",
-    "vue": "^2.5.17",
-    "vuedraggable": "^2.16.0"
+    "sortablejs": "^1.9.0",
+    "vue": "^2.6.10",
+    "vuedraggable": "^2.21.0"
   },
   "browser": {
     "vue": "vue/dist/vue.common.js"
index 804afcf5d3ed2ab681792aaffbea10e168a0e1ce..9f83e95ff238ea30a9672077b9fef6add7a885ab 100644 (file)
@@ -7,8 +7,7 @@
          convertNoticesToExceptions="true"
          convertWarningsToExceptions="true"
          processIsolation="false"
-         stopOnFailure="false"
-         syntaxCheck="false">
+         stopOnFailure="false">
     <testsuites>
         <testsuite name="Application Test Suite">
             <directory>./tests/</directory>
         </whitelist>
     </filter>
     <php>
-        <env name="APP_ENV" value="testing"/>
-        <env name="APP_DEBUG" value="false"/>
-        <env name="APP_LANG" value="en"/>
-        <env name="APP_AUTO_LANG_PUBLIC" value="true"/>
-        <env name="CACHE_DRIVER" value="array"/>
-        <env name="SESSION_DRIVER" value="array"/>
-        <env name="QUEUE_DRIVER" value="sync"/>
-        <env name="DB_CONNECTION" value="mysql_testing"/>
-        <env name="MAIL_DRIVER" value="log"/>
-        <env name="AUTH_METHOD" value="standard"/>
-        <env name="DISABLE_EXTERNAL_SERVICES" value="true"/>
-        <env name="AVATAR_URL" value=""/>
-        <env name="LDAP_VERSION" value="3"/>
-        <env name="STORAGE_TYPE" value="local"/>
-        <env name="GITHUB_APP_ID" value="aaaaaaaaaaaaaa"/>
-        <env name="GITHUB_APP_SECRET" value="aaaaaaaaaaaaaa"/>
-        <env name="GITHUB_AUTO_REGISTER" value=""/>
-        <env name="GITHUB_AUTO_CONFIRM_EMAIL" value=""/>
-        <env name="GOOGLE_APP_ID" value="aaaaaaaaaaaaaa"/>
-        <env name="GOOGLE_APP_SECRET" value="aaaaaaaaaaaaaa"/>
-        <env name="GOOGLE_AUTO_REGISTER" value=""/>
-        <env name="GOOGLE_AUTO_CONFIRM_EMAIL" value=""/>
-        <env name="GOOGLE_SELECT_ACCOUNT" value=""/>
-        <env name="APP_URL" value="http://bookstack.dev"/>
-        <env name="DEBUGBAR_ENABLED" value="false"/>
+        <server name="APP_ENV" value="testing"/>
+        <server name="APP_DEBUG" value="false"/>
+        <server name="APP_LANG" value="en"/>
+        <server name="APP_AUTO_LANG_PUBLIC" value="true"/>
+        <server name="CACHE_DRIVER" value="array"/>
+        <server name="SESSION_DRIVER" value="array"/>
+        <server name="QUEUE_CONNECTION" value="sync"/>
+        <server name="DB_CONNECTION" value="mysql_testing"/>
+        <server name="BCRYPT_ROUNDS" value="4"/>
+        <server name="MAIL_DRIVER" value="array"/>
+        <server name="LOG_CHANNEL" value="single"/>
+        <server name="AUTH_METHOD" value="standard"/>
+        <server name="DISABLE_EXTERNAL_SERVICES" value="true"/>
+        <server name="AVATAR_URL" value=""/>
+        <server name="LDAP_VERSION" value="3"/>
+        <server name="STORAGE_TYPE" value="local"/>
+        <server name="STORAGE_ATTACHMENT_TYPE" value="local"/>
+        <server name="STORAGE_IMAGE_TYPE" value="local"/>
+        <server name="GITHUB_APP_ID" value="aaaaaaaaaaaaaa"/>
+        <server name="GITHUB_APP_SECRET" value="aaaaaaaaaaaaaa"/>
+        <server name="GITHUB_AUTO_REGISTER" value=""/>
+        <server name="GITHUB_AUTO_CONFIRM_EMAIL" value=""/>
+        <server name="GOOGLE_APP_ID" value="aaaaaaaaaaaaaa"/>
+        <server name="GOOGLE_APP_SECRET" value="aaaaaaaaaaaaaa"/>
+        <server name="GOOGLE_AUTO_REGISTER" value=""/>
+        <server name="GOOGLE_AUTO_CONFIRM_EMAIL" value=""/>
+        <server name="GOOGLE_SELECT_ACCOUNT" value=""/>
+        <server name="APP_URL" value="http://bookstack.dev"/>
+        <server name="DEBUGBAR_ENABLED" value="false"/>
     </php>
 </phpunit>
index 8eb2dd0ddfa5f7b57bd0f351684dfa2e4e3ac0d3..0d55354ec6fb756086be2bcace7be11078064f29 100644 (file)
@@ -1,6 +1,6 @@
 <IfModule mod_rewrite.c>
     <IfModule mod_negotiation.c>
-        Options -MultiViews
+        Options -MultiViews -Indexes
     </IfModule>
 
     RewriteEngine On
index ad378d7e071ae46d8873169bac808b874615536b..8205764728cdb1dc6bd8bdfb78f20ebec5525ac3 100644 (file)
@@ -34,6 +34,7 @@ require __DIR__.'/../bootstrap/init.php';
 */
 
 $app = require_once __DIR__.'/../bootstrap/app.php';
+$app->alias('request', \BookStack\Http\Request::class);
 
 /*
 |--------------------------------------------------------------------------
@@ -50,7 +51,7 @@ $app = require_once __DIR__.'/../bootstrap/app.php';
 $kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
 
 $response = $kernel->handle(
-    $request = Illuminate\Http\Request::capture()
+    $request = \BookStack\Http\Request::capture()
 );
 
 $response->send();
diff --git a/public/libs/jq-color-picker/tiny-color-picker.min.js b/public/libs/jq-color-picker/tiny-color-picker.min.js
deleted file mode 100644 (file)
index 8402407..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-/*! tinyColorPicker - v1.0.0 2016-02-28 */
-// https://github.com/PitPik/tinyColorPicker
-// http://www.dematte.at/tinyColorPicker/index.html?type=small#demo
-!function(a,b){"object"==typeof exports?module.exports=b(a):"function"==typeof define&&define.amd?define([],function(){return b(a)}):a.Colors=b(a)}(this,function(a,b){"use strict";function c(a,c,d,f,g){if("string"==typeof c){var c=t.txt2color(c);d=c.type,n[d]=c[d],g=g!==b?g:c.alpha}else if(c)for(var h in c)a[d][h]=k(c[h]/l[d][h][1],0,1);return g!==b&&(a.alpha=k(+g,0,1)),e(d,f?a:b)}function d(a,b,c){var d=m.options.grey,e={};return e.RGB={r:a.r,g:a.g,b:a.b},e.rgb={r:b.r,g:b.g,b:b.b},e.alpha=c,e.equivalentGrey=Math.round(d.r*a.r+d.g*a.g+d.b*a.b),e.rgbaMixBlack=i(b,{r:0,g:0,b:0},c,1),e.rgbaMixWhite=i(b,{r:1,g:1,b:1},c,1),e.rgbaMixBlack.luminance=h(e.rgbaMixBlack,!0),e.rgbaMixWhite.luminance=h(e.rgbaMixWhite,!0),m.options.customBG&&(e.rgbaMixCustom=i(b,m.options.customBG,c,1),e.rgbaMixCustom.luminance=h(e.rgbaMixCustom,!0),m.options.customBG.luminance=h(m.options.customBG,!0)),e}function e(a,b){var c,e,k,o=b||n,p=t,q=m.options,r=l,s=o.RND,u="",v="",w={hsl:"hsv",rgb:a},x=s.rgb;if("alpha"!==a){for(var y in r)if(!r[y][y]){a!==y&&(v=w[y]||"rgb",o[y]=p[v+"2"+y](o[v])),s[y]||(s[y]={}),c=o[y];for(u in c)s[y][u]=Math.round(c[u]*r[y][u][1])}x=s.rgb,o.HEX=p.RGB2HEX(x),o.equivalentGrey=q.grey.r*o.rgb.r+q.grey.g*o.rgb.g+q.grey.b*o.rgb.b,o.webSave=e=f(x,51),o.webSmart=k=f(x,17),o.saveColor=x.r===e.r&&x.g===e.g&&x.b===e.b?"web save":x.r===k.r&&x.g===k.g&&x.b===k.b?"web smart":"",o.hueRGB=t.hue2RGB(o.hsv.h),b&&(o.background=d(x,o.rgb,o.alpha))}var z,A,B,C=o.rgb,D=o.alpha,E="luminance",F=o.background;return z=i(C,{r:0,g:0,b:0},D,1),z[E]=h(z,!0),o.rgbaMixBlack=z,A=i(C,{r:1,g:1,b:1},D,1),A[E]=h(A,!0),o.rgbaMixWhite=A,q.customBG&&(B=i(C,F.rgbaMixCustom,D,1),B[E]=h(B,!0),B.WCAG2Ratio=j(B[E],F.rgbaMixCustom[E]),o.rgbaMixBGMixCustom=B,B.luminanceDelta=Math.abs(B[E]-F.rgbaMixCustom[E]),B.hueDelta=g(F.rgbaMixCustom,B,!0)),o.RGBLuminance=h(x),o.HUELuminance=h(o.hueRGB),q.convertCallback&&q.convertCallback(o,a),o}function f(a,b){var c={},d=0,e=b/2;for(var f in a)d=a[f]%b,c[f]=a[f]+(d>e?b-d:-d);return c}function g(a,b,c){return(Math.max(a.r-b.r,b.r-a.r)+Math.max(a.g-b.g,b.g-a.g)+Math.max(a.b-b.b,b.b-a.b))*(c?255:1)/765}function h(a,b){for(var c=b?1:255,d=[a.r/c,a.g/c,a.b/c],e=m.options.luminance,f=d.length;f--;)d[f]=d[f]<=.03928?d[f]/12.92:Math.pow((d[f]+.055)/1.055,2.4);return e.r*d[0]+e.g*d[1]+e.b*d[2]}function i(a,c,d,e){var f={},g=d!==b?d:1,h=e!==b?e:1,i=g+h*(1-g);for(var j in a)f[j]=(a[j]*g+c[j]*h*(1-g))/i;return f.a=i,f}function j(a,b){var c=1;return c=a>=b?(a+.05)/(b+.05):(b+.05)/(a+.05),Math.round(100*c)/100}function k(a,b,c){return a>c?c:b>a?b:a}var l={rgb:{r:[0,255],g:[0,255],b:[0,255]},hsv:{h:[0,360],s:[0,100],v:[0,100]},hsl:{h:[0,360],s:[0,100],l:[0,100]},alpha:{alpha:[0,1]},HEX:{HEX:[0,16777215]}},m={},n={},o={r:.298954,g:.586434,b:.114612},p={r:.2126,g:.7152,b:.0722},q=function(a){this.colors={RND:{}},this.options={color:"rgba(204, 82, 37, 0.8)",grey:o,luminance:p,valueRanges:l},r(this,a||{})},r=function(a,d){var e,f=a.options;s(a);for(var g in d)d[g]!==b&&(f[g]=d[g]);e=f.customBG,f.customBG="string"==typeof e?t.txt2color(e).rgb:e,n=c(a.colors,f.color,b,!0)},s=function(a){m!==a&&(m=a,n=a.colors)};q.prototype.setColor=function(a,d,f){return s(this),a?c(this.colors,a,d,b,f):(f!==b&&(this.colors.alpha=k(f,0,1)),e(d))},q.prototype.setCustomBackground=function(a){return s(this),this.options.customBG="string"==typeof a?t.txt2color(a).rgb:a,c(this.colors,b,"rgb")},q.prototype.saveAsBackground=function(){return s(this),c(this.colors,b,"rgb",!0)};var t={txt2color:function(a){var b={},c=a.replace(/(?:#|\)|%)/g,"").split("("),d=(c[1]||"").split(/,\s*/),e=c[1]?c[0].substr(0,3):"rgb",f="";if(b.type=e,b[e]={},c[1])for(var g=3;g--;)f=e[g]||e.charAt(g),b[e][f]=+d[g]/l[e][f][1];else b.rgb=t.HEX2rgb(c[0]);return b.alpha=d[3]?+d[3]:1,b},RGB2HEX:function(a){return((a.r<16?"0":"")+a.r.toString(16)+(a.g<16?"0":"")+a.g.toString(16)+(a.b<16?"0":"")+a.b.toString(16)).toUpperCase()},HEX2rgb:function(a){return a=a.split(""),{r:parseInt(a[0]+a[a[3]?1:0],16)/255,g:parseInt(a[a[3]?2:1]+(a[3]||a[1]),16)/255,b:parseInt((a[4]||a[2])+(a[5]||a[2]),16)/255}},hue2RGB:function(a){var b=6*a,c=~~b%6,d=6===b?0:b-c;return{r:Math.round(255*[1,1-d,0,0,d,1][c]),g:Math.round(255*[d,1,1,1-d,0,0][c]),b:Math.round(255*[0,0,d,1,1,1-d][c])}},rgb2hsv:function(a){var b,c,d,e=a.r,f=a.g,g=a.b,h=0;return g>f&&(f=g+(g=f,0),h=-1),c=g,f>e&&(e=f+(f=e,0),h=-2/6-h,c=Math.min(f,g)),b=e-c,d=e?b/e:0,{h:1e-15>d?n&&n.hsl&&n.hsl.h||0:b?Math.abs(h+(f-g)/(6*b)):0,s:e?b/e:n&&n.hsv&&n.hsv.s||0,v:e}},hsv2rgb:function(a){var b=6*a.h,c=a.s,d=a.v,e=~~b,f=b-e,g=d*(1-c),h=d*(1-f*c),i=d*(1-(1-f)*c),j=e%6;return{r:[d,h,g,g,i,d][j],g:[i,d,d,h,g,g][j],b:[g,g,i,d,d,h][j]}},hsv2hsl:function(a){var b=(2-a.s)*a.v,c=a.s*a.v;return c=a.s?1>b?b?c/b:0:c/(2-b):0,{h:a.h,s:a.v||c?c:n&&n.hsl&&n.hsl.s||0,l:b/2}},rgb2hsl:function(a,b){var c=t.rgb2hsv(a);return t.hsv2hsl(b?c:n.hsv=c)},hsl2rgb:function(a){var b=6*a.h,c=a.s,d=a.l,e=.5>d?d*(1+c):d+c-c*d,f=d+d-e,g=e?(e-f)/e:0,h=~~b,i=b-h,j=e*g*i,k=f+j,l=e-j,m=h%6;return{r:[e,l,f,f,k,e][m],g:[k,e,e,l,f,f][m],b:[f,f,k,e,e,l][m]}}};return q}),function(a,b){"object"==typeof exports?module.exports=b(a,require("jquery"),require("colors")):"function"==typeof define&&define.amd?define(["jquery","colors"],function(c,d){return b(a,c,d)}):b(a,a.jQuery,a.Colors)}(this,function(a,b,c,d){"use strict";function e(a){return a.value||a.getAttribute("value")||b(a).css("background-color")||"#fff"}function f(a){return a=a.originalEvent&&a.originalEvent.touches?a.originalEvent.touches[0]:a,a.originalEvent?a.originalEvent:a}function g(a){return b(a.find(r.doRender)[0]||a[0])}function h(c){var d=b(this),f=d.offset(),h=b(a),j=r.gap;c?(s=g(d),s._colorMode=s.data("colorMode"),p.$trigger=d,(t||i()).css({left:(t[0]._left=f.left)-((t[0]._left=t[0]._left+t[0]._width-(h.scrollLeft()+h.width()))+j>0?t[0]._left+j:0),top:(t[0]._top=f.top+d.outerHeight())-((t[0]._top=t[0]._top+t[0]._height-(h.scrollTop()+h.height()))+j>0?t[0]._top+j:0)}).show(r.animationSpeed,function(){c!==!0&&(x._width=x.width(),u._width=u.width(),u._height=u.height(),q.setColor(e(s[0])),n(!0))})):b(t).hide(r.animationSpeed,function(){n(!1),p.$trigger=null})}function i(){return b("head").append('<style type="text/css">'+(r.css||I)+(r.cssAddon||"")+"</style>"),p.$UI=t=b(H).css({margin:r.margin}).appendTo("body").show(0,function(){var a=b(this);E=r.GPU&&a.css("perspective")!==d,u=b(".cp-xy-slider",this),v=b(".cp-xy-cursor",this),w=b(".cp-z-cursor",this),x=b(".cp-alpha",this).toggle(!!r.opacity),y=b(".cp-alpha-cursor",this),r.buildCallback.call(p,a),a.prepend("<div>").children().eq(0).css("width",a.children().eq(0).width()),this._width=this.offsetWidth,this._height=this.offsetHeight}).hide().on(C,".cp-xy-slider,.cp-z-slider,.cp-alpha",j)}function j(a){var c=this.className.replace(/cp-(.*?)(?:\s*|$)/,"$1").replace("-","_");(a.button||a.which)>1||(a.preventDefault&&a.preventDefault(),a.returnValue=!1,s._offset=b(this).offset(),(c="xy_slider"===c?k:"z_slider"===c?l:m)(a),n(),z.on(D,function(){z.off(".a")}).on(B,function(a){c(a),n()}))}function k(a){var b=f(a),c=b.pageX-s._offset.left,d=b.pageY-s._offset.top;q.setColor({s:c/u._width*100,v:100-d/u._height*100},"hsv")}function l(a){var b=f(a).pageY-s._offset.top;q.setColor({h:360-b/u._height*360},"hsv")}function m(a){var b=f(a).pageX-s._offset.left,c=b/x._width;q.setColor({},"rgb",c)}function n(a){var b=q.colors,c=b.hueRGB,e=b.RND.rgb,f=b.RND.hsl,g="#222",h="#ddd",i=s._colorMode,j=1!==b.alpha,k=F(100*b.alpha)/100,l=e.r+", "+e.g+", "+e.b,m="HEX"!==i||j?"rgb"===i||"HEX"===i&&j?j?"rgba("+l+", "+k+")":"rgb("+l+")":"hsl"+(j?"a(":"(")+f.h+", "+f.s+"%, "+f.l+"%"+(j?", "+k:"")+")":"#"+b.HEX,n=b.HUELuminance>.22?g:h,p=b.rgbaMixBlack.luminance>.22?g:h,r=(1-b.hsv.h)*u._height,t=b.hsv.s*u._width,z=(1-b.hsv.v)*u._height,A=k*x._width,B=E?"translate3d":"",C=s[0].value,D=s[0].hasAttribute("value")&&""===C&&a!==d;u._css={backgroundColor:"rgb("+c.r+","+c.g+","+c.b+")"},v._css={transform:B+"("+t+"px, "+z+"px, 0)",left:E?"":t,top:E?"":z,borderColor:b.RGBLuminance>.22?g:h},w._css={transform:B+"(0, "+r+"px, 0)",top:E?"":r,borderColor:"transparent "+n},x._css={backgroundColor:"rgb("+l+")"},y._css={transform:B+"("+A+"px, 0, 0)",left:E?"":A,borderColor:p+" transparent"},s._css={backgroundColor:D?"":m,color:D?"":b.rgbaMixBGMixCustom.luminance>.22?g:h},s.text=D?"":C!==m?m:"",a!==d?o(a):G(o)}function o(a){u.css(u._css),v.css(v._css),w.css(w._css),x.css(x._css),y.css(y._css),r.doRender&&s.css(s._css),s.text&&s.val(s.text),r.renderCallback.call(p,s,"boolean"==typeof a?a:d)}var p,q,r,s,t,u,v,w,x,y,z=b(document),A=b(),B="touchmove.a mousemove.a pointermove.a",C="touchstart.a mousedown.a pointerdown.a",D="touchend.a mouseup.a pointerup.a",E=!1,F=Math.round,G=a.requestAnimationFrame||a.webkitRequestAnimationFrame||function(a){a()},H='<div class="cp-color-picker"><div class="cp-z-slider"><div class="cp-z-cursor"></div></div><div class="cp-xy-slider"><div class="cp-white"></div><div class="cp-xy-cursor"></div></div><div class="cp-alpha"><div class="cp-alpha-cursor"></div></div></div>',I=".cp-color-picker{position:absolute;overflow:hidden;padding:6px 6px 0;background-color:#444;color:#bbb;font-family:Arial,Helvetica,sans-serif;font-size:12px;font-weight:400;cursor:default;border-radius:5px}.cp-color-picker>div{position:relative;overflow:hidden}.cp-xy-slider{float:left;height:128px;width:128px;margin-bottom:6px;background:linear-gradient(to right,#FFF,rgba(255,255,255,0))}.cp-white{height:100%;width:100%;background:linear-gradient(rgba(0,0,0,0),#000)}.cp-xy-cursor{position:absolute;top:0;width:10px;height:10px;margin:-5px;border:1px solid #fff;border-radius:100%;box-sizing:border-box}.cp-z-slider{float:right;margin-left:6px;height:128px;width:20px;background:linear-gradient(red 0,#f0f 17%,#00f 33%,#0ff 50%,#0f0 67%,#ff0 83%,red 100%)}.cp-z-cursor{position:absolute;margin-top:-4px;width:100%;border:4px solid #fff;border-color:transparent #fff;box-sizing:border-box}.cp-alpha{clear:both;width:100%;height:16px;margin:6px 0;background:linear-gradient(to right,#444,rgba(0,0,0,0))}.cp-alpha-cursor{position:absolute;margin-left:-4px;height:100%;border:4px solid #fff;border-color:#fff transparent;box-sizing:border-box}",J=function(a){q=this.color=new c(a),r=q.options,p=this};return J.prototype={render:n,toggle:h},b.fn.colorPicker=function(c){var d=function(){};return c=b.extend({animationSpeed:150,GPU:!0,doRender:!0,customBG:"#FFF",opacity:!0,renderCallback:d,buildCallback:d,body:document.body,scrollResize:!0,gap:4},c),!p&&c.scrollResize&&b(a).on("resize.a scroll.a",function(){p.$trigger&&p.toggle.call(p.$trigger[0],!0)}),A=A.add(this),this.colorPicker=A.colorPicker=p||new J(c),b(c.body).off(".a").on(C,function(a){!A.add(t).find(a.target).add(A.filter(a.target))[0]&&h()}),this.on("focusin.a click.a",h).on("change.a",function(){q.setColor(this.value||"#FFF"),A.colorPicker.render(!0)}).each(function(){var a=e(this),d=a.split("("),f=g(b(this));f.data("colorMode",d[1]?d[0].substr(0,3):"HEX").attr("readonly",r.preventFocus),c.doRender&&f.css({"background-color":a,color:function(){return q.setColor(a).rgbaMixBGMixCustom.luminance>.22?"#222":"#ddd"}})})},b.fn.colorPicker.destroy=function(){A.add(r.body).off(".a"),p.toggle(!1),A=b()},b});
\ No newline at end of file
diff --git a/public/libs/jquery-sortable/jquery-sortable.min.js b/public/libs/jquery-sortable/jquery-sortable.min.js
deleted file mode 100644 (file)
index 4b483e5..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-!function(d,B,m,f){function v(a,b){var c=Math.max(0,a[0]-b[0],b[0]-a[1]),e=Math.max(0,a[2]-b[1],b[1]-a[3]);return c+e}function w(a,b,c,e){var k=a.length;e=e?"offset":"position";for(c=c||0;k--;){var g=a[k].el?a[k].el:d(a[k]),l=g[e]();l.left+=parseInt(g.css("margin-left"),10);l.top+=parseInt(g.css("margin-top"),10);b[k]=[l.left-c,l.left+g.outerWidth()+c,l.top-c,l.top+g.outerHeight()+c]}}function p(a,b){var c=b.offset();return{left:a.left-c.left,top:a.top-c.top}}function x(a,b,c){b=[b.left,b.top];c=
-c&&[c.left,c.top];for(var e,k=a.length,d=[];k--;)e=a[k],d[k]=[k,v(e,b),c&&v(e,c)];return d=d.sort(function(a,b){return b[1]-a[1]||b[2]-a[2]||b[0]-a[0]})}function q(a){this.options=d.extend({},n,a);this.containers=[];this.options.rootGroup||(this.scrollProxy=d.proxy(this.scroll,this),this.dragProxy=d.proxy(this.drag,this),this.dropProxy=d.proxy(this.drop,this),this.placeholder=d(this.options.placeholder),a.isValidTarget||(this.options.isValidTarget=f))}function t(a,b){this.el=a;this.options=d.extend({},
-z,b);this.group=q.get(this.options);this.rootGroup=this.options.rootGroup||this.group;this.handle=this.rootGroup.options.handle||this.rootGroup.options.itemSelector;var c=this.rootGroup.options.itemPath;this.target=c?this.el.find(c):this.el;this.target.on(r.start,this.handle,d.proxy(this.dragInit,this));this.options.drop&&this.group.containers.push(this)}var r,z={drag:!0,drop:!0,exclude:"",nested:!0,vertical:!0},n={afterMove:function(a,b,c){},containerPath:"",containerSelector:"ol, ul",distance:0,
-delay:0,handle:"",itemPath:"",itemSelector:"li",bodyClass:"dragging",draggedClass:"dragged",isValidTarget:function(a,b){return!0},onCancel:function(a,b,c,e){},onDrag:function(a,b,c,e){a.css(b)},onDragStart:function(a,b,c,e){a.css({height:a.outerHeight(),width:a.outerWidth()});a.addClass(b.group.options.draggedClass);d("body").addClass(b.group.options.bodyClass)},onDrop:function(a,b,c,e){a.removeClass(b.group.options.draggedClass).removeAttr("style");d("body").removeClass(b.group.options.bodyClass)},
-onMousedown:function(a,b,c){if(!c.target.nodeName.match(/^(input|select|textarea)$/i))return c.preventDefault(),!0},placeholderClass:"placeholder",placeholder:'<li class="placeholder"></li>',pullPlaceholder:!0,serialize:function(a,b,c){a=d.extend({},a.data());if(c)return[b];b[0]&&(a.children=b);delete a.subContainers;delete a.sortable;return a},tolerance:0},s={},y=0,A={left:0,top:0,bottom:0,right:0};r={start:"touchstart.sortable mousedown.sortable",drop:"touchend.sortable touchcancel.sortable mouseup.sortable",
-drag:"touchmove.sortable mousemove.sortable",scroll:"scroll.sortable"};q.get=function(a){s[a.group]||(a.group===f&&(a.group=y++),s[a.group]=new q(a));return s[a.group]};q.prototype={dragInit:function(a,b){this.$document=d(b.el[0].ownerDocument);var c=d(a.target).closest(this.options.itemSelector);c.length&&(this.item=c,this.itemContainer=b,!this.item.is(this.options.exclude)&&this.options.onMousedown(this.item,n.onMousedown,a)&&(this.setPointer(a),this.toggleListeners("on"),this.setupDelayTimer(),
-this.dragInitDone=!0))},drag:function(a){if(!this.dragging){if(!this.distanceMet(a)||!this.delayMet)return;this.options.onDragStart(this.item,this.itemContainer,n.onDragStart,a);this.item.before(this.placeholder);this.dragging=!0}this.setPointer(a);this.options.onDrag(this.item,p(this.pointer,this.item.offsetParent()),n.onDrag,a);a=this.getPointer(a);var b=this.sameResultBox,c=this.options.tolerance;(!b||b.top-c>a.top||b.bottom+c<a.top||b.left-c>a.left||b.right+c<a.left)&&!this.searchValidTarget()&&
-(this.placeholder.detach(),this.lastAppendedItem=f)},drop:function(a){this.toggleListeners("off");this.dragInitDone=!1;if(this.dragging){if(this.placeholder.closest("html")[0])this.placeholder.before(this.item).detach();else this.options.onCancel(this.item,this.itemContainer,n.onCancel,a);this.options.onDrop(this.item,this.getContainer(this.item),n.onDrop,a);this.clearDimensions();this.clearOffsetParent();this.lastAppendedItem=this.sameResultBox=f;this.dragging=!1}},searchValidTarget:function(a,b){a||
-(a=this.relativePointer||this.pointer,b=this.lastRelativePointer||this.lastPointer);for(var c=x(this.getContainerDimensions(),a,b),e=c.length;e--;){var d=c[e][0];if(!c[e][1]||this.options.pullPlaceholder)if(d=this.containers[d],!d.disabled){if(!this.$getOffsetParent()){var g=d.getItemOffsetParent();a=p(a,g);b=p(b,g)}if(d.searchValidTarget(a,b))return!0}}this.sameResultBox&&(this.sameResultBox=f)},movePlaceholder:function(a,b,c,e){var d=this.lastAppendedItem;if(e||!d||d[0]!==b[0])b[c](this.placeholder),
-this.lastAppendedItem=b,this.sameResultBox=e,this.options.afterMove(this.placeholder,a,b)},getContainerDimensions:function(){this.containerDimensions||w(this.containers,this.containerDimensions=[],this.options.tolerance,!this.$getOffsetParent());return this.containerDimensions},getContainer:function(a){return a.closest(this.options.containerSelector).data(m)},$getOffsetParent:function(){if(this.offsetParent===f){var a=this.containers.length-1,b=this.containers[a].getItemOffsetParent();if(!this.options.rootGroup)for(;a--;)if(b[0]!=
-this.containers[a].getItemOffsetParent()[0]){b=!1;break}this.offsetParent=b}return this.offsetParent},setPointer:function(a){a=this.getPointer(a);if(this.$getOffsetParent()){var b=p(a,this.$getOffsetParent());this.lastRelativePointer=this.relativePointer;this.relativePointer=b}this.lastPointer=this.pointer;this.pointer=a},distanceMet:function(a){a=this.getPointer(a);return Math.max(Math.abs(this.pointer.left-a.left),Math.abs(this.pointer.top-a.top))>=this.options.distance},getPointer:function(a){var b=
-a.originalEvent||a.originalEvent.touches&&a.originalEvent.touches[0];return{left:a.pageX||b.pageX,top:a.pageY||b.pageY}},setupDelayTimer:function(){var a=this;this.delayMet=!this.options.delay;this.delayMet||(clearTimeout(this._mouseDelayTimer),this._mouseDelayTimer=setTimeout(function(){a.delayMet=!0},this.options.delay))},scroll:function(a){this.clearDimensions();this.clearOffsetParent()},toggleListeners:function(a){var b=this;d.each(["drag","drop","scroll"],function(c,e){b.$document[a](r[e],b[e+
-"Proxy"])})},clearOffsetParent:function(){this.offsetParent=f},clearDimensions:function(){this.traverse(function(a){a._clearDimensions()})},traverse:function(a){a(this);for(var b=this.containers.length;b--;)this.containers[b].traverse(a)},_clearDimensions:function(){this.containerDimensions=f},_destroy:function(){s[this.options.group]=f}};t.prototype={dragInit:function(a){var b=this.rootGroup;!this.disabled&&!b.dragInitDone&&this.options.drag&&this.isValidDrag(a)&&b.dragInit(a,this)},isValidDrag:function(a){return 1==
-a.which||"touchstart"==a.type&&1==a.originalEvent.touches.length},searchValidTarget:function(a,b){var c=x(this.getItemDimensions(),a,b),e=c.length,d=this.rootGroup,g=!d.options.isValidTarget||d.options.isValidTarget(d.item,this);if(!e&&g)return d.movePlaceholder(this,this.target,"append"),!0;for(;e--;)if(d=c[e][0],!c[e][1]&&this.hasChildGroup(d)){if(this.getContainerGroup(d).searchValidTarget(a,b))return!0}else if(g)return this.movePlaceholder(d,a),!0},movePlaceholder:function(a,b){var c=d(this.items[a]),
-e=this.itemDimensions[a],k="after",g=c.outerWidth(),f=c.outerHeight(),h=c.offset(),h={left:h.left,right:h.left+g,top:h.top,bottom:h.top+f};this.options.vertical?b.top<=(e[2]+e[3])/2?(k="before",h.bottom-=f/2):h.top+=f/2:b.left<=(e[0]+e[1])/2?(k="before",h.right-=g/2):h.left+=g/2;this.hasChildGroup(a)&&(h=A);this.rootGroup.movePlaceholder(this,c,k,h)},getItemDimensions:function(){this.itemDimensions||(this.items=this.$getChildren(this.el,"item").filter(":not(."+this.group.options.placeholderClass+
-", ."+this.group.options.draggedClass+")").get(),w(this.items,this.itemDimensions=[],this.options.tolerance));return this.itemDimensions},getItemOffsetParent:function(){var a=this.el;return"relative"===a.css("position")||"absolute"===a.css("position")||"fixed"===a.css("position")?a:a.offsetParent()},hasChildGroup:function(a){return this.options.nested&&this.getContainerGroup(a)},getContainerGroup:function(a){var b=d.data(this.items[a],"subContainers");if(b===f){var c=this.$getChildren(this.items[a],
-"container"),b=!1;c[0]&&(b=d.extend({},this.options,{rootGroup:this.rootGroup,group:y++}),b=c[m](b).data(m).group);d.data(this.items[a],"subContainers",b)}return b},$getChildren:function(a,b){var c=this.rootGroup.options,e=c[b+"Path"],c=c[b+"Selector"];a=d(a);e&&(a=a.find(e));return a.children(c)},_serialize:function(a,b){var c=this,e=this.$getChildren(a,b?"item":"container").not(this.options.exclude).map(function(){return c._serialize(d(this),!b)}).get();return this.rootGroup.options.serialize(a,
-e,b)},traverse:function(a){d.each(this.items||[],function(b){(b=d.data(this,"subContainers"))&&b.traverse(a)});a(this)},_clearDimensions:function(){this.itemDimensions=f},_destroy:function(){var a=this;this.target.off(r.start,this.handle);this.el.removeData(m);this.options.drop&&(this.group.containers=d.grep(this.group.containers,function(b){return b!=a}));d.each(this.items||[],function(){d.removeData(this,"subContainers")})}};var u={enable:function(){this.traverse(function(a){a.disabled=!1})},disable:function(){this.traverse(function(a){a.disabled=
-!0})},serialize:function(){return this._serialize(this.el,!0)},refresh:function(){this.traverse(function(a){a._clearDimensions()})},destroy:function(){this.traverse(function(a){a._destroy()})}};d.extend(t.prototype,u);d.fn[m]=function(a){var b=Array.prototype.slice.call(arguments,1);return this.map(function(){var c=d(this),e=c.data(m);if(e&&u[a])return u[a].apply(e,b)||this;e||a!==f&&"object"!==typeof a||c.data(m,new t(c,a));return this})}}(jQuery,window,"sortable");
index c96a04f008ee21e260b28f7701595ed59e2839e3..cb7328e1934841ba2afd78b3b7ceb100961337bb 100755 (executable)
@@ -1,2 +1,3 @@
 *
-!.gitignore
\ No newline at end of file
+!.gitignore
+!.htaccess
\ No newline at end of file
diff --git a/public/uploads/.htaccess b/public/uploads/.htaccess
new file mode 100755 (executable)
index 0000000..45552cb
--- /dev/null
@@ -0,0 +1 @@
+Options -Indexes
\ No newline at end of file
diff --git a/public/web.config b/public/web.config
new file mode 100644 (file)
index 0000000..474eb68
--- /dev/null
@@ -0,0 +1,28 @@
+<!--
+    Rewrites requires Microsoft URL Rewrite Module for IIS
+    Download: https://www.microsoft.com/en-us/download/details.aspx?id=47337
+    Debug Help: https://docs.microsoft.com/en-us/iis/extensions/url-rewrite-module/using-failed-request-tracing-to-trace-rewrite-rules
+-->
+<configuration>
+  <system.webServer>
+    <rewrite>
+      <rules>
+        <rule name="Imported Rule 1" stopProcessing="true">
+          <match url="^(.*)/$" ignoreCase="false" />
+          <conditions>
+            <add input="{REQUEST_FILENAME}" matchType="IsDirectory" ignoreCase="false" negate="true" />
+          </conditions>
+          <action type="Redirect" redirectType="Permanent" url="/{R:1}" />
+        </rule>
+        <rule name="Imported Rule 2" stopProcessing="true">
+          <match url="^" ignoreCase="false" />
+          <conditions>
+            <add input="{REQUEST_FILENAME}" matchType="IsDirectory" ignoreCase="false" negate="true" />
+            <add input="{REQUEST_FILENAME}" matchType="IsFile" ignoreCase="false" negate="true" />
+          </conditions>
+          <action type="Rewrite" url="index.php" />
+        </rule>
+      </rules>
+    </rewrite>
+  </system.webServer>
+</configuration>
\ No newline at end of file
index 940deb04ca2b029b753207f544f86129eb835669..ca90be3053af821b307e8b77bd72548f846056fa 100644 (file)
--- a/readme.md
+++ b/readme.md
@@ -2,7 +2,8 @@
 
 [![GitHub release](https://img.shields.io/github/release/BookStackApp/BookStack.svg)](https://github.com/BookStackApp/BookStack/releases/latest)
 [![license](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/BookStackApp/BookStack/blob/master/LICENSE)
-[![Build Status](https://travis-ci.org/BookStackApp/BookStack.svg)](https://travis-ci.org/BookStackApp/BookStack)
+[![Build Status](https://github.com/BookStackApp/BookStack/workflows/phpunit/badge.svg)](https://github.com/BookStackApp/BookStack/actions)
+[![Discord](https://img.shields.io/static/v1?label=Chat&message=Discord&color=738adb&logo=discord)](https://discord.gg/ztkBqR2)
 
 A platform for storing and organising information and documentation. General information and documentation for BookStack can be found at https://www.bookstackapp.com/.
 
@@ -12,7 +13,7 @@ A platform for storing and organising information and documentation. General inf
     * [Admin Login](https://demo.bookstackapp.com/[email protected]&password=password)
 * [BookStack Blog](https://www.bookstackapp.com/blog)
 
-## Project Definition
+## 📚 Project Definition
 
 BookStack is an opinionated wiki system that provides a pleasant and simple out of the box experience. New users to an instance should find the experience intuitive and only basic word-processing skills should be required to get involved in creating content on BookStack. The platform should provide advanced power features to those that desire it but they should not interfere with the core simple user experience.
 
@@ -20,13 +21,11 @@ BookStack is not designed as an extensible platform to be used for purposes that
 
 In regards to development philosophy, BookStack has a relaxed, open & positive approach. At the end of the day this is free software developed and maintained by people donating their own free time.
 
-## Road Map
+## 🛣️ Road Map
 
 Below is a high-level road map view for BookStack to provide a sense of direction of where the project is going. This can change at any point and does not reflect many features and improvements that will also be included as part of the journey along this road map. For more granular detail of what will be included in upcoming releases you can review the project milestones as defined in the "Release Process" section below.
 
-- **Design Revamp** *[(In Progress)](https://github.com/BookStackApp/BookStack/pull/1153)*
-    - *A more organised modern design to clean things up, make BookStack more efficient to use and increase mobile usability.*
-- **Platform REST API**
+- **Platform REST API** *(In Design)*
     - *A REST API covering, at minimum, control of core content models (Books, Chapters, Pages) for automation and platform extension.*
 - **Editor Alignment & Review**
     - *Review the page editors with goal of achieving increased interoperability & feature parity while also considering collaborative editing potential.*
@@ -35,7 +34,7 @@ Below is a high-level road map view for BookStack to provide a sense of directio
 - **Installation & Deployment Process Revamp**
     - *Creation of a streamlined & secure process for users to deploy & update BookStack with reduced development requirements (No git or composer requirement).*
 
-## Release Versioning & Process
+## 🚀 Release Versioning & Process
 
 BookStack releases are each assigned a version number, such as "v0.25.2", in the format `v<phase>.<feature>.<patch>`. A change only in the `patch` number indicates a fairly minor release that mainly contains fixes and therefore is very unlikely to cause breakages upon update. A change in the `feature` number indicates a release which will generally bring new features in addition to fixes and enhancements. These releases have a small chance of introducing breaking changes upon update so it's worth checking for any notes in the [update guide](https://www.bookstackapp.com/docs/admin/updates/). A change in the `phase` indicates a much large change in BookStack that will likely incur breakages requiring manual intervention.
 
@@ -43,7 +42,7 @@ Each BookStack release will have a [milestone](https://github.com/BookStackApp/B
 
 For feature releases, and some patch releases, the release will be accompanied by a post on the [BookStack blog](https://www.bookstackapp.com/blog/) which will provide additional detail on features, changes & updates otherwise the [GitHub release page](https://github.com/BookStackApp/BookStack/releases) will show a list of changes. You can sign up to be alerted to new BookStack blogs posts (once per week maximum) [at this link](http://eepurl.com/cmmq5j).
 
-## Development & Testing
+## 🛠️ Development & Testing
 
 All development on BookStack is currently done on the master branch. When it's time for a release the master branch is merged into release with built & minified CSS & JS then tagged at its version. Here are the current development requirements:
 
@@ -65,7 +64,7 @@ npm run production
 npm run dev
 ```
 
-BookStack has many integration tests that use Laravel's built-in testing capabilities which makes use of PHPUnit. To use you will need PHPUnit 6 installed and accessible via command line, Directly running the composer-installed version will not work. There is a `mysql_testing` database defined within the app config which is what is used by PHPUnit. This database is set with the following database name, user name and password defined as `bookstack-test`. You will have to create that database and credentials before testing.
+BookStack has many integration tests that use Laravel's built-in testing capabilities which makes use of PHPUnit. There is a `mysql_testing` database defined within the app config which is what is used by PHPUnit. This database is set with the database name, user name and password all defined as `bookstack-test`. You will have to create that database and that set of credentials before testing.
 
 The testing database will also need migrating and seeding beforehand. This can be done with the following commands:
 
@@ -74,9 +73,39 @@ php artisan migrate --database=mysql_testing
 php artisan db:seed --class=DummyContentSeeder --database=mysql_testing
 ```
 
-Once done you can run `phpunit` in the application root directory to run all tests.
+Once done you can run `php vendor/bin/phpunit` in the application root directory to run all tests.
 
-## Translations
+### 📜 Code Standards
+
+PHP code within BookStack is generally to [PSR-2](http://www.php-fig.org/psr/psr-2/) standards. From the BookStack root folder you can run `./vendor/bin/phpcs` to check code is formatted correctly and `./vendor/bin/phpcbf` to auto-fix non-PSR-2 code.
+
+### 🐋 Development using Docker
+
+This repository ships with a Docker Compose configuration intended for development purposes. It'll build a PHP image with all needed extensions installed and start up a MySQL server and a Node image watching the UI assets.
+
+To get started, make sure you meet the following requirements:
+
+- Docker and Docker Compose are installed
+- Your user is part of the `docker` group
+
+If all the conditions are met, you can proceed with the following steps:
+
+1. Install PHP/Composer dependencies with **`docker-compose run app composer install`** (first time can take a while because the image has to be built).
+2. **Copy `.env.example` to `.env`** and change `APP_KEY` to a random 32 char string.
+3. Make sure **port 8080 is unused** *or else* change `DEV_PORT` to a free port on your host.
+4. **Run `chgrp -R docker storage`**. The development container will chown the `storage` directory to the `www-data` user inside the container so BookStack can write to it. You need to change the group to your host's `docker` group here to not lose access to the `storage` directory.
+5. **Run `docker-compose up`** and wait until all database migrations have been done.
+6. You can now login with `[email protected]` and `password` as password on `localhost:8080` (or another port if specified).
+
+If needed, You'll be able to run any artisan commands via docker-compose like so:
+
+ ```shell script
+docker-compose run app php artisan list 
+```
+
+The docker-compose setup runs an instance of [MailHog](https://github.com/mailhog/MailHog) and sets environment variables to redirect any BookStack-sent emails to MailHog. You can view this mail via the MailHog web interface on `localhost:8025`. You can change the port MailHog is accessible on by setting a `DEV_MAIL_PORT` environment variable.
+
+## 🌎 Translations
 
 All text strings can be found in the `resources/lang` folder where each language option has its own folder. To add a new language you should copy the `en` folder to an new folder (eg. `fr` for french) then go through and translate all text strings in those files, leaving the keys and file-names intact. If a language string is missing then the `en` translation will be used. To show the language option in the user preferences language drop-down you will need to add your language to the options found at the bottom of the `resources/lang/en/settings.php` file. A system-wide language can also be set in the `.env` file like so: `APP_LANG=en`.
 
@@ -95,58 +124,48 @@ php resources/lang/check.php pt_BR
 
 Some strings have colon-prefixed variables in such as `:userName`. Leave these values as they are as they will be replaced at run-time.
 
-## Contributing & Maintenance
+## 🎁 Contributing, Issues & Pull Requests
 
-Feel free to create issues to request new features or to report bugs and problems. Just please follow the template given when creating the issue.
+Feel free to create issues to request new features or to report bugs & problems. Just please follow the template given when creating the issue.
+
+Pull requests are welcome. Unless a small tweak or language update, It may be best to open the pull request early or create an issue for your intended change to discuss how it will fit in to the project and plan out the merge. Pull requests should be created from the `master` branch since they will be merged back into `master` once done. Please do not build from or request a merge into the `release` branch as this is only for publishing releases. If you are looking to alter CSS or JavaScript content please edit the source files found in `resources/assets`. Any CSS or JS files within `public` are built from these source files and therefore should not be edited directly.
 
 The project's code of conduct [can be found here](https://github.com/BookStackApp/BookStack/blob/master/.github/CODE_OF_CONDUCT.md).
 
-### Code Standards
+## 🔒 Security
 
-PHP code within BookStack is generally to [PSR-2](http://www.php-fig.org/psr/psr-2/) standards. From the BookStack root folder you can run `./vendor/bin/phpcs` to check code is formatted correctly and `./vendor/bin/phpcbf` to auto-fix non-PSR-2 code.
+Security information for administering a BookStack instance can be found on the [documentation site here](https://www.bookstackapp.com/docs/admin/security/).
 
-### Pull Requests
+If you'd like to be notified of new potential security concerns you can [sign-up to the BookStack security mailing list](http://eepurl.com/glIh8z).
 
-Pull requests are welcome. Unless a small tweak or language update, It may be best to open the pull request early or create an issue for your intended change to discuss how it will fit in to the project and plan out the merge.
+If you would like to report a security concern in a more confidential manner than via a GitHub issue, You can directly email the lead maintainer [ssddanbrown](https://github.com/ssddanbrown). You will need to login to be able to see the email address on the [GitHub profile page](https://github.com/ssddanbrown). Alternatively you can send a DM via twitter to [@ssddanbrown](https://twitter.com/ssddanbrown).
 
-Pull requests should be created from the `master` branch since they will be merged back into `master` once done. Please do not build from or request a merge into the `release` branch as this is only for publishing releases.
+## ♿ Accessibility
 
-If you are looking to alter CSS or JavaScript content please edit the source files found in `resources/assets`. Any CSS or JS files within `public` are built from these source files and therefore should not be edited directly.
+We want BookStack to remain accessible to as many people as possible. We aim for at least WCAG 2.1 Level A standards where possible although we do not strictly test this upon each release. If you come across any accessibility issues please feel free to open an issue.
 
-## Website, Docs & Blog
+## 🖥️ Website, Docs & Blog
 
 The website which contains the project docs & Blog can be found in the [BookStackApp/website](https://github.com/BookStackApp/website) repo.
 
-## Security
-
-Security information for administering a BookStack instance can be found on the [documentation site here](https://www.bookstackapp.com/docs/admin/security/).
-
-If you'd like to be notified of new potential security concerns you can [sign-up to the BookStack security mailing list](http://eepurl.com/glIh8z).
-
-If you would like to report a security concern in a more confidential manner than via a GitHub issue, You can directly email the lead maintainer [ssddanbrown](https://github.com/ssddanbrown). You will need to login to be able to see the email address on the [GitHub profile page](https://github.com/ssddanbrown). Alternatively you can send a DM via twitter to [@ssddanbrown](https://twitter.com/ssddanbrown).
-
-
-## License
+## ⚖️ License
 
-The BookStack source is provided under the MIT License.
+The BookStack source is provided under the MIT License. The libraries used by, and included with, BookStack are provided under their own licenses.
 
-## Attribution
+## 👪 Attribution
 
 The great people that have worked to build and improve BookStack can [be seen here](https://github.com/BookStackApp/BookStack/graphs/contributors).
 
 These are the great open-source projects used to help build BookStack:
 
 * [Laravel](http://laravel.com/)
-* [jQuery](https://jquery.com/)
 * [TinyMCE](https://www.tinymce.com/)
 * [CodeMirror](https://codemirror.net)
 * [Vue.js](http://vuejs.org/)
-* [Axios](https://github.com/mzabriskie/axios)
-* [jQuery Sortable](https://johnny.github.io/jquery-sortable/)
+* [Sortable](https://github.com/SortableJS/Sortable) & [Vue.Draggable](https://github.com/SortableJS/Vue.Draggable)
 * [Google Material Icons](https://material.io/icons/)
 * [Dropzone.js](http://www.dropzonejs.com/)
 * [clipboard.js](https://clipboardjs.com/)
-* [TinyColorPicker](http://www.dematte.at/tinyColorPicker/index.html)
 * [markdown-it](https://github.com/markdown-it/markdown-it) and [markdown-it-task-lists](https://github.com/revin/markdown-it-task-lists)
 * [BarryVD](https://github.com/barryvdh)
     * [Debugbar](https://github.com/barryvdh/laravel-debugbar)
@@ -155,3 +174,4 @@ These are the great open-source projects used to help build BookStack:
     * [Laravel IDE helper](https://github.com/barryvdh/laravel-ide-helper)
 * [WKHTMLtoPDF](http://wkhtmltopdf.org/index.html)
 * [Draw.io](https://github.com/jgraph/drawio)
+* [Laravel Stats](https://github.com/stefanzweifel/laravel-stats)
\ No newline at end of file
diff --git a/resources/assets/js/components/chapter-toggle.js b/resources/assets/js/components/chapter-toggle.js
deleted file mode 100644 (file)
index 350c453..0000000
+++ /dev/null
@@ -1,72 +0,0 @@
-
-class ChapterToggle {
-
-    constructor(elem) {
-        this.elem = elem;
-        this.isOpen = elem.classList.contains('open');
-        elem.addEventListener('click', this.click.bind(this));
-    }
-
-    open() {
-        const list = this.elem.parentNode.querySelector('.inset-list');
-
-        this.elem.classList.add('open');
-        list.style.display = 'block';
-        list.style.maxHeight = '';
-        const maxHeight = list.getBoundingClientRect().height + 10;
-        list.style.maxHeight = '0px';
-        list.style.overflow = 'hidden';
-        list.style.transition = 'max-height ease-in-out 240ms';
-
-        let transitionEndBound = onTransitionEnd.bind(this);
-        function onTransitionEnd() {
-            list.style.overflow = '';
-            list.style.maxHeight = '';
-            list.style.transition = '';
-            list.style.display = `block`;
-            list.removeEventListener('transitionend', transitionEndBound);
-        }
-
-        setTimeout(() => {
-            requestAnimationFrame(() => {
-                list.style.maxHeight = `${maxHeight}px`;
-                list.addEventListener('transitionend', transitionEndBound)
-            });
-        }, 1);
-    }
-
-    close() {
-        const list = this.elem.parentNode.querySelector('.inset-list');
-
-        list.style.display =  'block';
-        this.elem.classList.remove('open');
-        list.style.maxHeight = list.getBoundingClientRect().height + 'px';
-        list.style.overflow = 'hidden';
-        list.style.transition = 'max-height ease-in-out 240ms';
-
-        const transitionEndBound = onTransitionEnd.bind(this);
-        function onTransitionEnd() {
-            list.style.overflow = '';
-            list.style.maxHeight = '';
-            list.style.transition = '';
-            list.style.display =  'none';
-            list.removeEventListener('transitionend', transitionEndBound);
-        }
-
-        setTimeout(() => {
-            requestAnimationFrame(() => {
-                list.style.maxHeight = `0px`;
-                list.addEventListener('transitionend', transitionEndBound)
-            });
-        }, 1);
-    }
-
-    click(event) {
-        event.preventDefault();
-        this.isOpen ?  this.close() : this.open();
-        this.isOpen = !this.isOpen;
-    }
-
-}
-
-export default ChapterToggle;
diff --git a/resources/assets/js/components/dropdown.js b/resources/assets/js/components/dropdown.js
deleted file mode 100644 (file)
index 3887e84..0000000
+++ /dev/null
@@ -1,84 +0,0 @@
-/**
- * Dropdown
- * Provides some simple logic to create simple dropdown menus.
- */
-class DropDown {
-
-    constructor(elem) {
-        this.container = elem;
-        this.menu = elem.querySelector('.dropdown-menu, [dropdown-menu]');
-        this.moveMenu = elem.hasAttribute('dropdown-move-menu');
-        this.toggle = elem.querySelector('[dropdown-toggle]');
-        this.body = document.body;
-        this.setupListeners();
-    }
-
-    show(event) {
-        this.hideAll();
-
-        this.menu.style.display = 'block';
-        this.menu.classList.add('anim', 'menuIn');
-
-        if (this.moveMenu) {
-            // Move to body to prevent being trapped within scrollable sections
-            this.rect = this.menu.getBoundingClientRect();
-            this.body.appendChild(this.menu);
-            this.menu.style.position = 'fixed';
-            this.menu.style.left = `${this.rect.left}px`;
-            this.menu.style.top = `${this.rect.top}px`;
-            this.menu.style.width = `${this.rect.width}px`;
-        }
-
-        // Set listener to hide on mouse leave or window click
-        this.menu.addEventListener('mouseleave', this.hide.bind(this));
-        window.addEventListener('click', event => {
-            if (!this.menu.contains(event.target)) {
-                this.hide();
-            }
-        });
-
-        // Focus on first input if existing
-        let input = this.menu.querySelector('input');
-        if (input !== null) input.focus();
-
-        event.stopPropagation();
-    }
-
-    hideAll() {
-        for (let dropdown of window.components.dropdown) {
-            dropdown.hide();
-        }
-    }
-
-    hide() {
-        this.menu.style.display = 'none';
-        this.menu.classList.remove('anim', 'menuIn');
-        if (this.moveMenu) {
-            this.menu.style.position = '';
-            this.menu.style.left = '';
-            this.menu.style.top = '';
-            this.menu.style.width = '';
-            this.container.appendChild(this.menu);
-        }
-    }
-
-    setupListeners() {
-        // Hide menu on option click
-        this.container.addEventListener('click', event => {
-             let possibleChildren = Array.from(this.menu.querySelectorAll('a'));
-             if (possibleChildren.indexOf(event.target) !== -1) this.hide();
-        });
-        // Show dropdown on toggle click
-        this.toggle.addEventListener('click', this.show.bind(this));
-        // Hide menu on enter press
-        this.container.addEventListener('keypress', event => {
-                if (event.keyCode !== 13) return true;
-                event.preventDefault();
-                this.hide();
-                return false;
-        });
-    }
-
-}
-
-export default DropDown;
\ No newline at end of file
diff --git a/resources/assets/js/components/expand-toggle.js b/resources/assets/js/components/expand-toggle.js
deleted file mode 100644 (file)
index a6a3898..0000000
+++ /dev/null
@@ -1,79 +0,0 @@
-
-class ExpandToggle {
-
-    constructor(elem) {
-        this.elem = elem;
-
-        // Component state
-        this.isOpen = elem.getAttribute('expand-toggle-is-open') === 'yes';
-        this.updateEndpoint = elem.getAttribute('expand-toggle-update-endpoint');
-        this.selector = elem.getAttribute('expand-toggle');
-
-        // Listener setup
-        elem.addEventListener('click', this.click.bind(this));
-    }
-
-    open(elemToToggle) {
-        elemToToggle.style.display = 'block';
-        elemToToggle.style.height = '';
-        let height = elemToToggle.getBoundingClientRect().height;
-        elemToToggle.style.height = '0px';
-        elemToToggle.style.overflow = 'hidden';
-        elemToToggle.style.transition = 'height ease-in-out 240ms';
-
-        let transitionEndBound = onTransitionEnd.bind(this);
-        function onTransitionEnd() {
-            elemToToggle.style.overflow = '';
-            elemToToggle.style.height = '';
-            elemToToggle.style.transition = '';
-            elemToToggle.removeEventListener('transitionend', transitionEndBound);
-        }
-
-        setTimeout(() => {
-            elemToToggle.style.height = `${height}px`;
-            elemToToggle.addEventListener('transitionend', transitionEndBound)
-        }, 1);
-    }
-
-    close(elemToToggle) {
-        elemToToggle.style.display =  'block';
-        elemToToggle.style.height = elemToToggle.getBoundingClientRect().height + 'px';
-        elemToToggle.style.overflow = 'hidden';
-        elemToToggle.style.transition = 'all ease-in-out 240ms';
-
-        let transitionEndBound = onTransitionEnd.bind(this);
-        function onTransitionEnd() {
-            elemToToggle.style.overflow = '';
-            elemToToggle.style.height = '';
-            elemToToggle.style.transition = '';
-            elemToToggle.style.display =  'none';
-            elemToToggle.removeEventListener('transitionend', transitionEndBound);
-        }
-
-        setTimeout(() => {
-            elemToToggle.style.height = `0px`;
-            elemToToggle.addEventListener('transitionend', transitionEndBound)
-        }, 1);
-    }
-
-    click(event) {
-        event.preventDefault();
-
-        const matchingElems = document.querySelectorAll(this.selector);
-        for (let match of matchingElems) {
-            this.isOpen ?  this.close(match) : this.open(match);
-        }
-
-        this.isOpen = !this.isOpen;
-        this.updateSystemAjax(this.isOpen);
-    }
-
-    updateSystemAjax(isOpen) {
-        window.$http.patch(this.updateEndpoint, {
-            expand: isOpen ? 'true' : 'false'
-        });
-    }
-
-}
-
-export default ExpandToggle;
\ No newline at end of file
diff --git a/resources/assets/js/components/page-display.js b/resources/assets/js/components/page-display.js
deleted file mode 100644 (file)
index e87966d..0000000
+++ /dev/null
@@ -1,232 +0,0 @@
-import Clipboard from "clipboard/dist/clipboard.min";
-import Code from "../services/code";
-
-class PageDisplay {
-
-    constructor(elem) {
-        this.elem = elem;
-        this.pageId = elem.getAttribute('page-display');
-
-        Code.highlight();
-        this.setupPointer();
-        this.setupStickySidebar();
-        this.setupNavHighlighting();
-
-        // Check the hash on load
-        if (window.location.hash) {
-            let text = window.location.hash.replace(/\%20/g, ' ').substr(1);
-            this.goToText(text);
-        }
-
-        // Sidebar page nav click event
-        $('.sidebar-page-nav').on('click', 'a', event => {
-            this.goToText(event.target.getAttribute('href').substr(1));
-        });
-    }
-
-    goToText(text) {
-        let idElem = document.getElementById(text);
-        $('.page-content [data-highlighted]').attr('data-highlighted', '').css('background-color', '');
-        if (idElem !== null) {
-            window.scrollAndHighlight(idElem);
-        } else {
-            $('.page-content').find(':contains("' + text + '")').smoothScrollTo();
-        }
-    }
-
-    setupPointer() {
-        if (document.getElementById('pointer') === null) return;
-        // Set up pointer
-        let $pointer = $('#pointer').detach();
-        let pointerShowing = false;
-        let $pointerInner = $pointer.children('div.pointer').first();
-        let isSelection = false;
-        let pointerModeLink = true;
-        let pointerSectionId = '';
-
-        // Select all contents on input click
-        $pointer.on('click', 'input', event => {
-            $(this).select();
-            event.stopPropagation();
-        });
-
-        $pointer.on('click focus', event => {
-            event.stopPropagation();
-        });
-
-        // Pointer mode toggle
-        $pointer.on('click', 'span.icon', event => {
-            event.stopPropagation();
-            let $icon = $(event.currentTarget);
-            pointerModeLink = !pointerModeLink;
-            $icon.find('[data-icon="include"]').toggle(!pointerModeLink);
-            $icon.find('[data-icon="link"]').toggle(pointerModeLink);
-            updatePointerContent();
-        });
-
-        // Set up clipboard
-        let clipboard = new Clipboard($pointer[0].querySelector('button'));
-
-        // Hide pointer when clicking away
-        $(document.body).find('*').on('click focus', event => {
-            if (!pointerShowing || isSelection) return;
-            $pointer.detach();
-            pointerShowing = false;
-        });
-
-        let updatePointerContent = ($elem) => {
-            let inputText = pointerModeLink ? window.baseUrl(`/link/${this.pageId}#${pointerSectionId}`) : `{{@${this.pageId}#${pointerSectionId}}}`;
-            if (pointerModeLink && inputText.indexOf('http') !== 0) inputText = window.location.protocol + "//" + window.location.host + inputText;
-
-            $pointer.find('input').val(inputText);
-
-            // update anchor if present
-            const $editAnchor = $pointer.find('#pointer-edit');
-            if ($editAnchor.length !== 0 && $elem) {
-                const editHref = $editAnchor.data('editHref');
-                const element = $elem[0];
-                const elementId = element.id;
-
-                // get the first 50 characters.
-                let queryContent = element.textContent && element.textContent.substring(0, 50);
-                $editAnchor[0].href = `${editHref}?content-id=${elementId}&content-text=${encodeURIComponent(queryContent)}`;
-            }
-        };
-
-        // Show pointer when selecting a single block of tagged content
-        $('.page-content [id^="bkmrk"]').on('mouseup keyup', function (e) {
-            e.stopPropagation();
-            let selection = window.getSelection();
-            if (selection.toString().length === 0) return;
-
-            // Show pointer and set link
-            let $elem = $(this);
-            pointerSectionId = $elem.attr('id');
-            updatePointerContent($elem);
-
-            $elem.before($pointer);
-            $pointer.show();
-            pointerShowing = true;
-
-            // Set pointer to sit near mouse-up position
-            let pointerLeftOffset = (e.pageX - $elem.offset().left - ($pointerInner.width() / 2));
-            if (pointerLeftOffset < 0) pointerLeftOffset = 0;
-            let pointerLeftOffsetPercent = (pointerLeftOffset / $elem.width()) * 100;
-            $pointerInner.css('left', pointerLeftOffsetPercent + '%');
-
-            isSelection = true;
-            setTimeout(() => {
-                isSelection = false;
-            }, 100);
-        });
-    }
-
-    setupStickySidebar() {
-        // Make the sidebar stick in view on scroll
-        const $window = $(window);
-        const $sidebar = $("#sidebar .scroll-body");
-        const $sidebarContainer = $sidebar.parent();
-        const sidebarHeight = $sidebar.height() + 32;
-
-        // Check the page is scrollable and the content is taller than the tree
-        const pageScrollable = ($(document).height() > ($window.height() + 40)) && (sidebarHeight < $('.page-content').height());
-
-        // Get current tree's width and header height
-        const headerHeight = $("#header").height() + $(".toolbar").height();
-        let isFixed = $window.scrollTop() > headerHeight;
-
-        // Fix the tree as a sidebar
-        function stickTree() {
-            $sidebar.width($sidebarContainer.width() + 15);
-            $sidebar.addClass("fixed");
-            isFixed = true;
-        }
-
-        // Un-fix the tree back into position
-        function unstickTree() {
-            $sidebar.css('width', 'auto');
-            $sidebar.removeClass("fixed");
-            isFixed = false;
-        }
-
-        // Checks if the tree stickiness state should change
-        function checkTreeStickiness(skipCheck) {
-            let shouldBeFixed = $window.scrollTop() > headerHeight;
-            if (shouldBeFixed && (!isFixed || skipCheck)) {
-                stickTree();
-            } else if (!shouldBeFixed && (isFixed || skipCheck)) {
-                unstickTree();
-            }
-        }
-        // The event ran when the window scrolls
-        function windowScrollEvent() {
-            checkTreeStickiness(false);
-        }
-
-        // If the page is scrollable and the window is wide enough listen to scroll events
-        // and evaluate tree stickiness.
-        if (pageScrollable && $window.width() > 1000) {
-            $window.on('scroll', windowScrollEvent);
-            checkTreeStickiness(true);
-        }
-
-        // Handle window resizing and switch between desktop/mobile views
-        $window.on('resize', event => {
-            if (pageScrollable && $window.width() > 1000) {
-                $window.on('scroll', windowScrollEvent);
-                checkTreeStickiness(true);
-            } else {
-                $window.off('scroll', windowScrollEvent);
-                unstickTree();
-            }
-        });
-    }
-
-    setupNavHighlighting() {
-        // Check if support is present for IntersectionObserver
-        if (!('IntersectionObserver' in window) ||
-            !('IntersectionObserverEntry' in window) ||
-            !('intersectionRatio' in window.IntersectionObserverEntry.prototype)) {
-            return;
-        }
-
-        let pageNav = document.querySelector('.sidebar-page-nav');
-
-        // fetch all the headings.
-        let headings = document.querySelector('.page-content').querySelectorAll('h1, h2, h3, h4, h5, h6');
-        // if headings are present, add observers.
-        if (headings.length > 0 && pageNav !== null) {
-            addNavObserver(headings);
-        }
-
-        function addNavObserver(headings) {
-            // Setup the intersection observer.
-            let intersectOpts = {
-                rootMargin: '0px 0px 0px 0px',
-                threshold: 1.0
-            };
-            let pageNavObserver = new IntersectionObserver(headingVisibilityChange, intersectOpts);
-
-            // observe each heading
-            for (let heading of headings) {
-                pageNavObserver.observe(heading);
-            }
-        }
-
-        function headingVisibilityChange(entries, observer) {
-            for (let entry of entries) {
-                let isVisible = (entry.intersectionRatio === 1);
-                toggleAnchorHighlighting(entry.target.id, isVisible);
-            }
-        }
-
-        function toggleAnchorHighlighting(elementId, shouldHighlight) {
-            const anchorsToHighlight = pageNav.querySelectorAll('a[href="#' + elementId + '"]');
-            for (let anchor of anchorsToHighlight) {
-                anchor.closest('li').classList.toggle('current-heading', shouldHighlight);
-            }
-        }
-    }
-}
-
-export default PageDisplay;
diff --git a/resources/assets/js/components/shelf-sort.js b/resources/assets/js/components/shelf-sort.js
deleted file mode 100644 (file)
index 86fb5e1..0000000
+++ /dev/null
@@ -1,71 +0,0 @@
-import "jquery-sortable";
-
-class ShelfSort {
-
-    constructor(elem) {
-        this.elem = elem;
-        this.sortGroup = this.initSortable();
-        this.input = document.getElementById('books-input');
-        this.setupListeners();
-    }
-
-    initSortable() {
-        const placeHolderContent = this.getPlaceholderHTML();
-        // TODO - Load sortable at this point
-        return $('.scroll-box').sortable({
-            group: 'shelf-books',
-            exclude: '.instruction,.scroll-box-placeholder',
-            containerSelector: 'div.scroll-box',
-            itemSelector: '.scroll-box-item',
-            placeholder: placeHolderContent,
-            onDrop: this.onDrop.bind(this)
-        });
-    }
-
-    setupListeners() {
-        this.elem.addEventListener('click', event => {
-            const sortItem = event.target.closest('.scroll-box-item:not(.instruction)');
-            if (sortItem) {
-                event.preventDefault();
-                this.sortItemClick(sortItem);
-            }
-        });
-    }
-
-    /**
-     * Called when a sort item is clicked.
-     * @param {Element} sortItem
-     */
-    sortItemClick(sortItem) {
-        const lists = this.elem.querySelectorAll('.scroll-box');
-        const newList = Array.from(lists).filter(list => sortItem.parentElement !== list);
-        if (newList.length > 0) {
-            newList[0].appendChild(sortItem);
-        }
-        this.onChange();
-    }
-
-    onDrop($item, container, _super) {
-        this.onChange();
-        _super($item, container);
-    }
-
-    onChange() {
-        const data = this.sortGroup.sortable('serialize').get();
-        this.input.value = data[0].map(item => item.id).join(',');
-        const instruction = this.elem.querySelector('.scroll-box-item.instruction');
-        instruction.parentNode.insertBefore(instruction, instruction.parentNode.children[0]);
-    }
-
-    getPlaceholderHTML() {
-        const placeHolder = document.querySelector('.scroll-box-placeholder');
-        placeHolder.style.display = 'block';
-        const placeHolderContent = placeHolder.outerHTML;
-        placeHolder.style.display = 'none';
-        return placeHolderContent;
-    }
-
-
-}
-
-export default ShelfSort;
\ No newline at end of file
diff --git a/resources/assets/js/components/toggle-switch.js b/resources/assets/js/components/toggle-switch.js
deleted file mode 100644 (file)
index 3be67d5..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-
-class ToggleSwitch {
-
-    constructor(elem) {
-        this.elem = elem;
-        this.input = elem.querySelector('input[type=hidden]');
-        this.checkbox = elem.querySelector('input[type=checkbox]');
-
-        this.checkbox.addEventListener('change', this.onClick.bind(this));
-    }
-
-    onClick(event) {
-        let checked = this.checkbox.checked;
-        this.input.value = checked ? 'true' : 'false';
-    }
-
-}
-
-export default ToggleSwitch;
\ No newline at end of file
diff --git a/resources/assets/js/services/dom-polyfills.js b/resources/assets/js/services/dom-polyfills.js
deleted file mode 100644 (file)
index d32af91..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-/**
- * Polyfills for DOM API's
- */
-
-// https://developer.mozilla.org/en-US/docs/Web/API/Element/matches
-if (!Element.prototype.matches) {
-    Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector;
-}
-
-// https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Browser_compatibility
-if (!Element.prototype.closest) {
-    Element.prototype.closest = function (s) {
-        var el = this;
-        var ancestor = this;
-        if (!document.documentElement.contains(el)) return null;
-        do {
-            if (ancestor.matches(s)) return ancestor;
-            ancestor = ancestor.parentElement;
-        } while (ancestor !== null);
-        return null;
-    };
-}
\ No newline at end of file
diff --git a/resources/assets/js/services/events.js b/resources/assets/js/services/events.js
deleted file mode 100644 (file)
index 1f97d0c..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-/**
- * Simple global events manager
- */
-class Events {
-    constructor() {
-        this.listeners = {};
-        this.stack = [];
-    }
-
-    emit(eventName, eventData) {
-        this.stack.push({name: eventName, data: eventData});
-        if (typeof this.listeners[eventName] === 'undefined') return this;
-        let eventsToStart = this.listeners[eventName];
-        for (let i = 0; i < eventsToStart.length; i++) {
-            let event = eventsToStart[i];
-            event(eventData);
-        }
-        return this;
-    }
-
-    listen(eventName, callback) {
-        if (typeof this.listeners[eventName] === 'undefined') this.listeners[eventName] = [];
-        this.listeners[eventName].push(callback);
-        return this;
-    }
-}
-
-export default Events;
\ No newline at end of file
diff --git a/resources/assets/js/services/global-ui.js b/resources/assets/js/services/global-ui.js
deleted file mode 100644 (file)
index 948e8e8..0000000
+++ /dev/null
@@ -1,58 +0,0 @@
-// Global jQuery Config & Extensions
-
-import jQuery from "jquery"
-window.jQuery = window.$ = jQuery;
-
-/**
- * Scroll the view to a specific element.
- * @param {HTMLElement} element
- */
-window.scrollToElement = function(element) {
-    if (!element) return;
-    let offset = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop;
-    let top = element.getBoundingClientRect().top + offset;
-    $('html, body').animate({
-        scrollTop: top - 60 // Adjust to change final scroll position top margin
-    }, 300);
-};
-
-/**
- * Scroll and highlight an element.
- * @param {HTMLElement} element
- */
-window.scrollAndHighlight = function(element) {
-    if (!element) return;
-    window.scrollToElement(element);
-    let color = document.getElementById('custom-styles').getAttribute('data-color-light');
-    let initColor = window.getComputedStyle(element).getPropertyValue('background-color');
-    element.style.backgroundColor = color;
-    setTimeout(() => {
-        element.classList.add('selectFade');
-        element.style.backgroundColor = initColor;
-    }, 10);
-    setTimeout(() => {
-        element.classList.remove('selectFade');
-        element.style.backgroundColor = '';
-    }, 3000);
-};
-
-// Smooth scrolling
-jQuery.fn.smoothScrollTo = function () {
-    if (this.length === 0) return;
-    window.scrollToElement(this[0]);
-    return this;
-};
-
-// Making contains text expression not worry about casing
-jQuery.expr[":"].contains = $.expr.createPseudo(function (arg) {
-    return function (elem) {
-        return $(elem).text().toUpperCase().indexOf(arg.toUpperCase()) >= 0;
-    };
-});
-
-// Detect IE for css
-if(navigator.userAgent.indexOf('MSIE')!==-1
-    || navigator.appVersion.indexOf('Trident/') > 0
-    || navigator.userAgent.indexOf('Safari') !== -1){
-    document.body.classList.add('flexbox-support');
-}
\ No newline at end of file
diff --git a/resources/assets/js/services/http.js b/resources/assets/js/services/http.js
deleted file mode 100644 (file)
index 1e50fe2..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-import axios from "axios"
-
-function instance() {
-    let axiosInstance = axios.create({
-        headers: {
-            'X-CSRF-TOKEN': document.querySelector('meta[name=token]').getAttribute('content'),
-            'baseURL': window.baseUrl('')
-        }
-    });
-    axiosInstance.interceptors.request.use(resp => {
-        return resp;
-    }, err => {
-        if (typeof err.response === "undefined" || typeof err.response.data === "undefined") return Promise.reject(err);
-        if (typeof err.response.data.error !== "undefined") window.$events.emit('error', err.response.data.error);
-        if (typeof err.response.data.message !== "undefined") window.$events.emit('error', err.response.data.message);
-    });
-    return axiosInstance;
-}
-
-
-export default instance;
\ No newline at end of file
diff --git a/resources/assets/sass/_colors.scss b/resources/assets/sass/_colors.scss
deleted file mode 100644 (file)
index 4dfc9d4..0000000
+++ /dev/null
@@ -1,72 +0,0 @@
-
-/*
- * Status text colors
- */
-.text-pos, .text-pos:hover, .text-pos-hover:hover {
-  color: $positive !important;
-  fill: $positive !important;
-}
-
-.text-warn, .text-warn:hover, .text-warn-hover:hover {
-  color: $warning !important;
-  fill: $warning !important;
-}
-
-.text-neg, .text-neg:hover, .text-neg-hover:hover  {
-  color: $negative !important;
-  fill: $negative !important;
-}
-
-/*
- * Style text colors
- */
-.text-primary, .text-primary:hover, .text-primary-hover:hover  {
-  color: $primary !important;
-  fill: $primary !important;
-}
-
-.text-muted {
-  color: lighten($text-dark, 26%) !important;
-  fill: lighten($text-dark, 26%) !important;
-  &.small, .small {
-    color: lighten($text-dark, 32%) !important;
-    fill: lighten($text-dark, 32%) !important;
-  }
-}
-
-/*
- * Entity text colors
- */
-.text-bookshelf, .text-bookshelf:hover {
-  color: $color-bookshelf;
-  fill: $color-bookshelf;
-}
-.text-book, .text-book:hover {
-  color: $color-book;
-  fill: $color-book;
-}
-.text-page, .text-page:hover {
-  color: $color-page;
-  fill: $color-page;
-}
-.text-page.draft, .text-page.draft:hover {
-  color: $color-page-draft;
-  fill: $color-page-draft;
-}
-.text-chapter, .text-chapter:hover {
-  color: $color-chapter;
-  fill: $color-chapter;
-}
-
-/*
- * Entity background colors
- */
-.bg-book {
-  background-color: $color-book;
-}
-.bg-chapter {
-  background-color: $color-chapter;
-}
-.bg-shelf {
-  background-color: $color-bookshelf;
-}
\ No newline at end of file
diff --git a/resources/assets/sass/print-styles.scss b/resources/assets/sass/print-styles.scss
deleted file mode 100644 (file)
index 44107f2..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-@import "variables";
-
-header {
-  display: none;
-}
-
-body {
-  font-size: 12px;
-}
-
-.page-content {
-  margin: 0 auto;
-}
-
-.flex-fill {
-  display: block;
-}
-
-.flex.sidebar + .flex.content {
-  border-left: none;
-}
-
-.print-hidden {
-  display: none;
-}
-
-.print-full-width {
-  width: 100%;
-  float: none;
-  display: block;
-}
-
-h2 {
-  font-size: 2em;
-  line-height: 1;
-  margin-top: 0.6em;
-  margin-bottom: 0.3em;
-}
-
-.comments-container {
-  display: none;
-}
\ No newline at end of file
diff --git a/resources/icons/chevron-down.svg b/resources/icons/chevron-down.svg
new file mode 100644 (file)
index 0000000..f08dfaf
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M7.41 8L12 12.58 16.59 8 18 9.41l-6 6-6-6z"/><path d="M0 0h24v24H0z" fill="none"/></svg>
\ No newline at end of file
similarity index 81%
rename from resources/assets/icons/copy.svg
rename to resources/icons/copy.svg
index 45e34a39bdb3db169569ef0e89d49fc4824d771d..3a52e2314a6f88c42b04f590602bf2d583ecb6e4 100644 (file)
@@ -1,4 +1,3 @@
 <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
-    <path d="M0 0h24v24H0z" fill="none"/>
     <path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/>
 </svg>
\ No newline at end of file
similarity index 85%
rename from resources/assets/icons/link.svg
rename to resources/icons/link.svg
index 94eea0ed877d18877aa75e67fedb6239fdf1f2ca..3e8be1ce7da2bff861bbeae974d9ea631bddc20a 100644 (file)
@@ -1,4 +1,3 @@
 <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
-    <path d="M0 0h24v24H0z" fill="none"/>
     <path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z"/>
 </svg>
\ No newline at end of file
diff --git a/resources/icons/template.svg b/resources/icons/template.svg
new file mode 100644 (file)
index 0000000..7c14212
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M4 4h7V2H4c-1.1 0-2 .9-2 2v7h2zm16-2h-7v2h7v7h2V4c0-1.1-.9-2-2-2zm0 18h-7v2h7c1.1 0 2-.9 2-2v-7h-2zM4 13H2v7c0 1.1.9 2 2 2h7v-2H4zM16.475 15.356h-8.95v-2.237h8.95zm0-4.475h-8.95V8.644h8.95z"/></svg>
\ No newline at end of file
diff --git a/resources/js/components/book-sort.js b/resources/js/components/book-sort.js
new file mode 100644 (file)
index 0000000..da2b28d
--- /dev/null
@@ -0,0 +1,204 @@
+import Sortable from "sortablejs";
+
+// Auto sort control
+const sortOperations = {
+    name: function(a, b) {
+        const aName = a.getAttribute('data-name').trim().toLowerCase();
+        const bName = b.getAttribute('data-name').trim().toLowerCase();
+        return aName.localeCompare(bName);
+    },
+    created: function(a, b) {
+        const aTime = Number(a.getAttribute('data-created'));
+        const bTime = Number(b.getAttribute('data-created'));
+        return bTime - aTime;
+    },
+    updated: function(a, b) {
+        const aTime = Number(a.getAttribute('data-updated'));
+        const bTime = Number(b.getAttribute('data-updated'));
+        return bTime - aTime;
+    },
+    chaptersFirst: function(a, b) {
+        const aType = a.getAttribute('data-type');
+        const bType = b.getAttribute('data-type');
+        if (aType === bType) {
+            return 0;
+        }
+        return (aType === 'chapter' ? -1 : 1);
+    },
+    chaptersLast: function(a, b) {
+        const aType = a.getAttribute('data-type');
+        const bType = b.getAttribute('data-type');
+        if (aType === bType) {
+            return 0;
+        }
+        return (aType === 'chapter' ? 1 : -1);
+    },
+};
+
+class BookSort {
+
+    constructor(elem) {
+        this.elem = elem;
+        this.sortContainer = elem.querySelector('[book-sort-boxes]');
+        this.input = elem.querySelector('[book-sort-input]');
+
+        const initialSortBox = elem.querySelector('.sort-box');
+        this.setupBookSortable(initialSortBox);
+        this.setupSortPresets();
+
+        window.$events.listen('entity-select-confirm', this.bookSelect.bind(this));
+    }
+
+    /**
+     * Setup the handlers for the preset sort type buttons.
+     */
+    setupSortPresets() {
+        let lastSort = '';
+        let reverse = false;
+        const reversibleTypes = ['name', 'created', 'updated'];
+
+        this.sortContainer.addEventListener('click', event => {
+            const sortButton = event.target.closest('.sort-box-options [data-sort]');
+            if (!sortButton) return;
+
+            event.preventDefault();
+            const sortLists = sortButton.closest('.sort-box').querySelectorAll('ul');
+            const sort = sortButton.getAttribute('data-sort');
+
+            reverse = (lastSort === sort) ? !reverse : false;
+            let sortFunction = sortOperations[sort];
+            if (reverse && reversibleTypes.includes(sort)) {
+                sortFunction = function(a, b) {
+                    return 0 - sortOperations[sort](a, b)
+                };
+            }
+
+            for (let list of sortLists) {
+                const directItems = Array.from(list.children).filter(child => child.matches('li'));
+                directItems.sort(sortFunction).forEach(sortedItem => {
+                    list.appendChild(sortedItem);
+                });
+            }
+
+            lastSort = sort;
+            this.updateMapInput();
+        });
+    }
+
+    /**
+     * Handle book selection from the entity selector.
+     * @param {Object} entityInfo
+     */
+    bookSelect(entityInfo) {
+        const alreadyAdded = this.elem.querySelector(`[data-type="book"][data-id="${entityInfo.id}"]`) !== null;
+        if (alreadyAdded) return;
+
+        const entitySortItemUrl = entityInfo.link + '/sort-item';
+        window.$http.get(entitySortItemUrl).then(resp => {
+            const wrap = document.createElement('div');
+            wrap.innerHTML = resp.data;
+            const newBookContainer = wrap.children[0];
+            this.sortContainer.append(newBookContainer);
+            this.setupBookSortable(newBookContainer);
+        });
+    }
+
+    /**
+     * Setup the given book container element to have sortable items.
+     * @param {Element} bookContainer
+     */
+    setupBookSortable(bookContainer) {
+        const sortElems = [bookContainer.querySelector('.sort-list')];
+        sortElems.push(...bookContainer.querySelectorAll('.entity-list-item + ul'));
+
+        const bookGroupConfig = {
+            name: 'book',
+            pull: ['book', 'chapter'],
+            put: ['book', 'chapter'],
+        };
+
+        const chapterGroupConfig = {
+            name: 'chapter',
+            pull: ['book', 'chapter'],
+            put: function(toList, fromList, draggedElem) {
+                return draggedElem.getAttribute('data-type') === 'page';
+            }
+        };
+
+        for (let sortElem of sortElems) {
+            new Sortable(sortElem, {
+                group: sortElem.classList.contains('sort-list') ? bookGroupConfig : chapterGroupConfig,
+                animation: 150,
+                fallbackOnBody: true,
+                swapThreshold: 0.65,
+                onSort: this.updateMapInput.bind(this),
+                dragClass: 'bg-white',
+                ghostClass: 'primary-background-light',
+            });
+        }
+    }
+
+    /**
+     * Update the input with our sort data.
+     */
+    updateMapInput() {
+        const pageMap = this.buildEntityMap();
+        this.input.value = JSON.stringify(pageMap);
+    }
+
+    /**
+     * Build up a mapping of entities with their ordering and nesting.
+     * @returns {Array}
+     */
+    buildEntityMap() {
+        const entityMap = [];
+        const lists = this.elem.querySelectorAll('.sort-list');
+
+        for (let list of lists) {
+            const bookId = list.closest('[data-type="book"]').getAttribute('data-id');
+            const directChildren = Array.from(list.children)
+                .filter(elem => elem.matches('[data-type="page"], [data-type="chapter"]'));
+            for (let i = 0; i < directChildren.length; i++) {
+                this.addBookChildToMap(directChildren[i], i, bookId, entityMap);
+            }
+        }
+
+        return entityMap;
+    }
+
+    /**
+     * Parse a sort item and add it to a data-map array.
+     * Parses sub0items if existing also.
+     * @param {Element} childElem
+     * @param {Number} index
+     * @param {Number} bookId
+     * @param {Array} entityMap
+     */
+    addBookChildToMap(childElem, index, bookId, entityMap) {
+        const type = childElem.getAttribute('data-type');
+        const parentChapter = false;
+        const childId = childElem.getAttribute('data-id');
+
+        entityMap.push({
+            id: childId,
+            sort: index,
+            parentChapter: parentChapter,
+            type: type,
+            book: bookId
+        });
+
+        const subPages = childElem.querySelectorAll('[data-type="page"]');
+        for (let i = 0; i < subPages.length; i++) {
+            entityMap.push({
+                id: subPages[i].getAttribute('data-id'),
+                sort: i,
+                parentChapter: childId,
+                type: 'page',
+                book: bookId
+            });
+        }
+    }
+
+}
+
+export default BookSort;
\ No newline at end of file
similarity index 86%
rename from resources/assets/js/components/breadcrumb-listing.js
rename to resources/js/components/breadcrumb-listing.js
index 1e2fe9ea463368a29273e547f2c1628835bd662a..7f4344b17d7f1b4452f920d30eda7a3d4d72049e 100644 (file)
@@ -7,14 +7,13 @@ class BreadcrumbListing {
         this.searchInput = elem.querySelector('input');
         this.loadingElem = elem.querySelector('.loading-container');
         this.entityListElem = elem.querySelector('.breadcrumb-listing-entity-list');
-        this.toggleElem = elem.querySelector('[dropdown-toggle]');
 
         // this.loadingElem.style.display = 'none';
         const entityDescriptor = elem.getAttribute('breadcrumb-listing').split(':');
         this.entityType = entityDescriptor[0];
         this.entityId = Number(entityDescriptor[1]);
 
-        this.toggleElem.addEventListener('click', this.onShow.bind(this));
+        this.elem.addEventListener('show', this.onShow.bind(this));
         this.searchInput.addEventListener('input', this.onSearch.bind(this));
     }
 
@@ -28,6 +27,7 @@ class BreadcrumbListing {
         for (let listItem of listItems) {
             const match = !input || listItem.textContent.toLowerCase().includes(input);
             listItem.style.display = match ? 'flex' : 'none';
+            listItem.classList.toggle('hidden', !match);
         }
     }
 
@@ -39,7 +39,7 @@ class BreadcrumbListing {
             'entity_type': this.entityType,
         };
 
-        window.$http.get('/search/entity/siblings', {params}).then(resp => {
+        window.$http.get('/search/entity/siblings', params).then(resp => {
             this.entityListElem.innerHTML = resp.data;
         }).catch(err => {
             console.error(err);
diff --git a/resources/js/components/chapter-toggle.js b/resources/js/components/chapter-toggle.js
new file mode 100644 (file)
index 0000000..bfd0ac7
--- /dev/null
@@ -0,0 +1,33 @@
+import {slideUp, slideDown} from "../services/animations";
+
+class ChapterToggle {
+
+    constructor(elem) {
+        this.elem = elem;
+        this.isOpen = elem.classList.contains('open');
+        elem.addEventListener('click', this.click.bind(this));
+    }
+
+    open() {
+        const list = this.elem.parentNode.querySelector('.inset-list');
+        this.elem.classList.add('open');
+        this.elem.setAttribute('aria-expanded', 'true');
+        slideDown(list, 240);
+    }
+
+    close() {
+        const list = this.elem.parentNode.querySelector('.inset-list');
+        this.elem.classList.remove('open');
+        this.elem.setAttribute('aria-expanded', 'false');
+        slideUp(list, 240);
+    }
+
+    click(event) {
+        event.preventDefault();
+        this.isOpen ?  this.close() : this.open();
+        this.isOpen = !this.isOpen;
+    }
+
+}
+
+export default ChapterToggle;
similarity index 70%
rename from resources/assets/js/components/collapsible.js
rename to resources/js/components/collapsible.js
index a13b367d3458dfa3ddb471fb581db6337600fa0c..464f394c1e7e42a8d1dc568c94568d53f1498819 100644 (file)
@@ -1,3 +1,5 @@
+import {slideDown, slideUp} from "../services/animations";
+
 /**
  * Collapsible
  * Provides some simple logic to allow collapsible sections.
@@ -16,12 +18,14 @@ class Collapsible {
 
     open() {
         this.elem.classList.add('open');
-        $(this.content).slideDown(400);
+        this.trigger.setAttribute('aria-expanded', 'true');
+        slideDown(this.content, 300);
     }
 
     close() {
         this.elem.classList.remove('open');
-        $(this.content).slideUp(400);
+        this.trigger.setAttribute('aria-expanded', 'false');
+        slideUp(this.content, 300);
     }
 
     toggle() {
diff --git a/resources/js/components/custom-checkbox.js b/resources/js/components/custom-checkbox.js
new file mode 100644 (file)
index 0000000..65ce8c1
--- /dev/null
@@ -0,0 +1,34 @@
+
+class CustomCheckbox {
+
+    constructor(elem) {
+        this.elem = elem;
+        this.checkbox = elem.querySelector('input[type=checkbox]');
+        this.display = elem.querySelector('[role="checkbox"]');
+
+        this.checkbox.addEventListener('change', this.stateChange.bind(this));
+        this.elem.addEventListener('keydown', this.onKeyDown.bind(this));
+    }
+
+    onKeyDown(event) {
+        const isEnterOrPress = event.keyCode === 32 || event.keyCode === 13;
+        if (isEnterOrPress) {
+            event.preventDefault();
+            this.toggle();
+        }
+    }
+
+    toggle() {
+        this.checkbox.checked = !this.checkbox.checked;
+        this.checkbox.dispatchEvent(new Event('change'));
+        this.stateChange();
+    }
+
+    stateChange() {
+        const checked = this.checkbox.checked ? 'true' : 'false';
+        this.display.setAttribute('aria-checked', checked);
+    }
+
+}
+
+export default CustomCheckbox;
\ No newline at end of file
diff --git a/resources/js/components/dropdown.js b/resources/js/components/dropdown.js
new file mode 100644 (file)
index 0000000..4de1e23
--- /dev/null
@@ -0,0 +1,152 @@
+import {onSelect} from "../services/dom";
+
+/**
+ * Dropdown
+ * Provides some simple logic to create simple dropdown menus.
+ */
+class DropDown {
+
+    constructor(elem) {
+        this.container = elem;
+        this.menu = elem.querySelector('.dropdown-menu, [dropdown-menu]');
+        this.moveMenu = elem.hasAttribute('dropdown-move-menu');
+        this.toggle = elem.querySelector('[dropdown-toggle]');
+        this.body = document.body;
+        this.showing = false;
+        this.setupListeners();
+    }
+
+    show(event = null) {
+        this.hideAll();
+
+        this.menu.style.display = 'block';
+        this.menu.classList.add('anim', 'menuIn');
+        this.toggle.setAttribute('aria-expanded', 'true');
+
+        if (this.moveMenu) {
+            // Move to body to prevent being trapped within scrollable sections
+            this.rect = this.menu.getBoundingClientRect();
+            this.body.appendChild(this.menu);
+            this.menu.style.position = 'fixed';
+            this.menu.style.left = `${this.rect.left}px`;
+            this.menu.style.top = `${this.rect.top}px`;
+            this.menu.style.width = `${this.rect.width}px`;
+        }
+
+        // Set listener to hide on mouse leave or window click
+        this.menu.addEventListener('mouseleave', this.hide.bind(this));
+        window.addEventListener('click', event => {
+            if (!this.menu.contains(event.target)) {
+                this.hide();
+            }
+        });
+
+        // Focus on first input if existing
+        const input = this.menu.querySelector('input');
+        if (input !== null) input.focus();
+
+        this.showing = true;
+
+        const showEvent = new Event('show');
+        this.container.dispatchEvent(showEvent);
+
+        if (event) {
+            event.stopPropagation();
+        }
+    }
+
+    hideAll() {
+        for (let dropdown of window.components.dropdown) {
+            dropdown.hide();
+        }
+    }
+
+    hide() {
+        this.menu.style.display = 'none';
+        this.menu.classList.remove('anim', 'menuIn');
+        this.toggle.setAttribute('aria-expanded', 'false');
+        if (this.moveMenu) {
+            this.menu.style.position = '';
+            this.menu.style.left = '';
+            this.menu.style.top = '';
+            this.menu.style.width = '';
+            this.container.appendChild(this.menu);
+        }
+        this.showing = false;
+    }
+
+    getFocusable() {
+        return Array.from(this.menu.querySelectorAll('[tabindex],[href],button,input:not([type=hidden])'));
+    }
+
+    focusNext() {
+        const focusable = this.getFocusable();
+        const currentIndex = focusable.indexOf(document.activeElement);
+        let newIndex = currentIndex + 1;
+        if (newIndex >= focusable.length) {
+            newIndex = 0;
+        }
+
+        focusable[newIndex].focus();
+    }
+
+    focusPrevious() {
+        const focusable = this.getFocusable();
+        const currentIndex = focusable.indexOf(document.activeElement);
+        let newIndex = currentIndex - 1;
+        if (newIndex < 0) {
+            newIndex = focusable.length - 1;
+        }
+
+        focusable[newIndex].focus();
+    }
+
+    setupListeners() {
+        // Hide menu on option click
+        this.container.addEventListener('click', event => {
+             const possibleChildren = Array.from(this.menu.querySelectorAll('a'));
+             if (possibleChildren.includes(event.target)) {
+                 this.hide();
+             }
+        });
+
+        onSelect(this.toggle, event => {
+            event.stopPropagation();
+            this.show(event);
+            if (event instanceof KeyboardEvent) {
+                this.focusNext();
+            }
+        });
+
+        // Keyboard navigation
+        const keyboardNavigation = event => {
+            if (event.key === 'ArrowDown' || event.key === 'ArrowRight') {
+                this.focusNext();
+                event.preventDefault();
+            } else if (event.key === 'ArrowUp' || event.key === 'ArrowLeft') {
+                this.focusPrevious();
+                event.preventDefault();
+            } else if (event.key === 'Escape') {
+                this.hide();
+                this.toggle.focus();
+                event.stopPropagation();
+            }
+        };
+        this.container.addEventListener('keydown', keyboardNavigation);
+        if (this.moveMenu) {
+            this.menu.addEventListener('keydown', keyboardNavigation);
+        }
+
+        // Hide menu on enter press or escape
+        this.menu.addEventListener('keydown ', event => {
+            if (event.key === 'Enter') {
+                event.preventDefault();
+                event.stopPropagation();
+                this.hide();
+            }
+        });
+    }
+
+}
+
+export default DropDown;
\ No newline at end of file
similarity index 90%
rename from resources/assets/js/components/editor-toolbox.js
rename to resources/js/components/editor-toolbox.js
index 10678edfaa4e880c5d3f076b061fd968abdb54a0..354bf0a86b3262f4a5e14a9e446ea87ee9405c65 100644 (file)
@@ -23,6 +23,8 @@ class EditorToolbox {
 
     toggle() {
         this.elem.classList.toggle('open');
+        const expanded = this.elem.classList.contains('open') ? 'true' : 'false';
+        this.toggleButton.setAttribute('aria-expanded', expanded);
     }
 
     setActiveTab(tabName, openToolbox = false) {
diff --git a/resources/js/components/entity-permissions-editor.js b/resources/js/components/entity-permissions-editor.js
new file mode 100644 (file)
index 0000000..a821792
--- /dev/null
@@ -0,0 +1,20 @@
+
+class EntityPermissionsEditor {
+
+  constructor(elem) {
+    this.permissionsTable = elem.querySelector('[permissions-table]');
+
+    // Handle toggle all event
+    this.restrictedCheckbox = elem.querySelector('[name=restricted]');
+    this.restrictedCheckbox.addEventListener('change', this.updateTableVisibility.bind(this));
+  }
+
+  updateTableVisibility() {
+    this.permissionsTable.style.display =
+      this.restrictedCheckbox.checked
+        ? null
+        : 'none';
+  }
+}
+
+export default EntityPermissionsEditor;
\ No newline at end of file
diff --git a/resources/js/components/expand-toggle.js b/resources/js/components/expand-toggle.js
new file mode 100644 (file)
index 0000000..cce1b21
--- /dev/null
@@ -0,0 +1,45 @@
+import {slideUp, slideDown} from "../services/animations";
+
+class ExpandToggle {
+
+    constructor(elem) {
+        this.elem = elem;
+
+        // Component state
+        this.isOpen = elem.getAttribute('expand-toggle-is-open') === 'yes';
+        this.updateEndpoint = elem.getAttribute('expand-toggle-update-endpoint');
+        this.selector = elem.getAttribute('expand-toggle');
+
+        // Listener setup
+        elem.addEventListener('click', this.click.bind(this));
+    }
+
+    open(elemToToggle) {
+        slideDown(elemToToggle, 200);
+    }
+
+    close(elemToToggle) {
+        slideUp(elemToToggle, 200);
+    }
+
+    click(event) {
+        event.preventDefault();
+
+        const matchingElems = document.querySelectorAll(this.selector);
+        for (let match of matchingElems) {
+            this.isOpen ?  this.close(match) : this.open(match);
+        }
+
+        this.isOpen = !this.isOpen;
+        this.updateSystemAjax(this.isOpen);
+    }
+
+    updateSystemAjax(isOpen) {
+        window.$http.patch(this.updateEndpoint, {
+            expand: isOpen ? 'true' : 'false'
+        });
+    }
+
+}
+
+export default ExpandToggle;
\ No newline at end of file
similarity index 85%
rename from resources/assets/js/components/index.js
rename to resources/js/components/index.js
index 355b96473801f47a50d7d12f720c42fd706605b1..14cf08ae2da41014e4703c57bfae6f79c4dc1c9e 100644 (file)
@@ -23,6 +23,12 @@ import listSortControl from "./list-sort-control";
 import triLayout from "./tri-layout";
 import breadcrumbListing from "./breadcrumb-listing";
 import permissionsTable from "./permissions-table";
+import customCheckbox from "./custom-checkbox";
+import bookSort from "./book-sort";
+import settingAppColorPicker from "./setting-app-color-picker";
+import entityPermissionsEditor from "./entity-permissions-editor";
+import templateManager from "./template-manager";
+import newUserPassword from "./new-user-password";
 
 const componentMapping = {
     'dropdown': dropdown,
@@ -50,6 +56,12 @@ const componentMapping = {
     'tri-layout': triLayout,
     'breadcrumb-listing': breadcrumbListing,
     'permissions-table': permissionsTable,
+    'custom-checkbox': customCheckbox,
+    'book-sort': bookSort,
+    'setting-app-color-picker': settingAppColorPicker,
+    'entity-permissions-editor': entityPermissionsEditor,
+    'template-manager': templateManager,
+    'new-user-password': newUserPassword,
 };
 
 window.components = {};
similarity index 81%
rename from resources/assets/js/components/markdown-editor.js
rename to resources/js/components/markdown-editor.js
index 5fa16ef6e78b522266ccf47c2a7f218357447780..de256a8466a727add893f5370167f3db4ef34320 100644 (file)
@@ -1,6 +1,7 @@
 import MarkdownIt from "markdown-it";
 import mdTasksLists from 'markdown-it-task-lists';
 import code from '../services/code';
+import {debounce} from "../services/util";
 
 import DrawIO from "../services/drawio";
 
@@ -17,19 +18,24 @@ class MarkdownEditor {
         this.markdown.use(mdTasksLists, {label: true});
 
         this.display = this.elem.querySelector('.markdown-display');
+
+        this.displayStylesLoaded = false;
         this.input = this.elem.querySelector('textarea');
         this.htmlInput = this.elem.querySelector('input[name=html]');
         this.cm = code.markdownEditor(this.input);
 
         this.onMarkdownScroll = this.onMarkdownScroll.bind(this);
-        this.init();
 
-        // Scroll to text if needed.
-        const queryParams = (new URL(window.location)).searchParams;
-        const scrollText = queryParams.get('content-text');
-        if (scrollText) {
-            this.scrollToText(scrollText);
-        }
+        this.display.addEventListener('load', () => {
+            this.displayDoc = this.display.contentDocument;
+            this.init();
+        });
+
+        window.$events.emitPublic(elem, 'editor-markdown::setup', {
+            markdownIt: this.markdown,
+            displayEl: this.display,
+            codeMirrorInstance: this.cm,
+        });
     }
 
     init() {
@@ -37,7 +43,7 @@ class MarkdownEditor {
         let lastClick = 0;
 
         // Prevent markdown display link click redirect
-        this.display.addEventListener('click', event => {
+        this.displayDoc.addEventListener('click', event => {
             let isDblClick = Date.now() - lastClick < 300;
 
             let link = event.target.closest('a');
@@ -90,28 +96,53 @@ class MarkdownEditor {
         });
 
         this.codeMirrorSetup();
+        this.listenForBookStackEditorEvents();
+
+        // Scroll to text if needed.
+        const queryParams = (new URL(window.location)).searchParams;
+        const scrollText = queryParams.get('content-text');
+        if (scrollText) {
+            this.scrollToText(scrollText);
+        }
     }
 
     // Update the input content and render the display.
     updateAndRender() {
-        let content = this.cm.getValue();
+        const content = this.cm.getValue();
         this.input.value = content;
-        let html = this.markdown.render(content);
+        const html = this.markdown.render(content);
         window.$events.emit('editor-html-change', html);
         window.$events.emit('editor-markdown-change', content);
-        this.display.innerHTML = html;
+
+        // Set body content
+        this.displayDoc.body.className = 'page-content';
+        this.displayDoc.body.innerHTML = html;
         this.htmlInput.value = html;
+
+        // Copy styles from page head and set custom styles for editor
+        this.loadStylesIntoDisplay();
+    }
+
+    loadStylesIntoDisplay() {
+        if (this.displayStylesLoaded) return;
+        this.displayDoc.documentElement.className = 'markdown-editor-display';
+
+        this.displayDoc.head.innerHTML = '';
+        const styles = document.head.querySelectorAll('style,link[rel=stylesheet]');
+        for (let style of styles) {
+            const copy = style.cloneNode(true);
+            this.displayDoc.head.appendChild(copy);
+        }
+
+        this.displayStylesLoaded = true;
     }
 
     onMarkdownScroll(lineCount) {
-        let elems = this.display.children;
+        const elems = this.displayDoc.body.children;
         if (elems.length <= lineCount) return;
 
-        let topElem = (lineCount === -1) ? elems[elems.length-1] : elems[lineCount];
-        // TODO - Replace jQuery
-        $(this.display).animate({
-            scrollTop: topElem.offsetTop
-        }, {queue: false, duration: 200, easing: 'linear'});
+        const topElem = (lineCount === -1) ? elems[elems.length-1] : elems[lineCount];
+        topElem.scrollIntoView({ block: 'start', inline: 'nearest', behavior: 'smooth'});
     }
 
     codeMirrorSetup() {
@@ -160,8 +191,7 @@ class MarkdownEditor {
             this.updateAndRender();
         });
 
-        // Handle scroll to sync display view
-        cm.on('scroll', instance => {
+        const onScrollDebounced = debounce((instance) => {
             // Thanks to http://liuhao.im/english/2015/11/10/the-sync-scroll-of-markdown-editor-in-javascript.html
             let scroll = instance.getScrollInfo();
             let atEnd = scroll.top + scroll.clientHeight === scroll.height;
@@ -176,6 +206,11 @@ class MarkdownEditor {
             let doc = parser.parseFromString(this.markdown.render(range), 'text/html');
             let totalLines = doc.documentElement.querySelectorAll('body > *');
             this.onMarkdownScroll(totalLines.length);
+        }, 100);
+
+        // Handle scroll to sync display view
+        cm.on('scroll', instance => {
+            onScrollDebounced(instance);
         });
 
         // Handle image paste
@@ -197,16 +232,30 @@ class MarkdownEditor {
             }
         });
 
-        // Handle images on drag-drop
+        // Handle image & content drag n drop
         cm.on('drop', (cm, event) => {
-            event.stopPropagation();
-            event.preventDefault();
-            let cursorPos = cm.coordsChar({left: event.pageX, top: event.pageY});
-            cm.setCursor(cursorPos);
-            if (!event.dataTransfer || !event.dataTransfer.files) return;
-            for (let i = 0; i < event.dataTransfer.files.length; i++) {
-                uploadImage(event.dataTransfer.files[i]);
+
+            const templateId = event.dataTransfer.getData('bookstack/template');
+            if (templateId) {
+                const cursorPos = cm.coordsChar({left: event.pageX, top: event.pageY});
+                cm.setCursor(cursorPos);
+                event.preventDefault();
+                window.$http.get(`/templates/${templateId}`).then(resp => {
+                    const content = resp.data.markdown || resp.data.html;
+                    cm.replaceSelection(content);
+                });
             }
+
+            if (event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files.length > 0) {
+                const cursorPos = cm.coordsChar({left: event.pageX, top: event.pageY});
+                cm.setCursor(cursorPos);
+                event.stopPropagation();
+                event.preventDefault();
+                for (let i = 0; i < event.dataTransfer.files.length; i++) {
+                    uploadImage(event.dataTransfer.files[i]);
+                }
+            }
+
         });
 
         // Helper to replace editor content
@@ -459,6 +508,37 @@ class MarkdownEditor {
         })
     }
 
+    listenForBookStackEditorEvents() {
+
+        function getContentToInsert({html, markdown}) {
+            return markdown || html;
+        }
+
+        // Replace editor content
+        window.$events.listen('editor::replace', (eventContent) => {
+            const markdown = getContentToInsert(eventContent);
+            this.cm.setValue(markdown);
+        });
+
+        // Append editor content
+        window.$events.listen('editor::append', (eventContent) => {
+            const cursorPos = this.cm.getCursor('from');
+            const markdown = getContentToInsert(eventContent);
+            const content = this.cm.getValue() + '\n' + markdown;
+            this.cm.setValue(content);
+            this.cm.setCursor(cursorPos.line, cursorPos.ch);
+        });
+
+        // Prepend editor content
+        window.$events.listen('editor::prepend', (eventContent) => {
+            const cursorPos = this.cm.getCursor('from');
+            const markdown = getContentToInsert(eventContent);
+            const content = markdown + '\n' + this.cm.getValue();
+            this.cm.setValue(content);
+            const prependLineCount = markdown.split('\n').length;
+            this.cm.setCursor(cursorPos.line + prependLineCount, cursorPos.ch);
+        });
+    }
 }
 
 export default MarkdownEditor ;
diff --git a/resources/js/components/new-user-password.js b/resources/js/components/new-user-password.js
new file mode 100644 (file)
index 0000000..9c4c21c
--- /dev/null
@@ -0,0 +1,28 @@
+
+class NewUserPassword {
+
+    constructor(elem) {
+        this.elem = elem;
+        this.inviteOption = elem.querySelector('input[name=send_invite]');
+
+        if (this.inviteOption) {
+            this.inviteOption.addEventListener('change', this.inviteOptionChange.bind(this));
+            this.inviteOptionChange();
+        }
+    }
+
+    inviteOptionChange() {
+        const inviting = (this.inviteOption.value === 'true');
+        const passwordBoxes = this.elem.querySelectorAll('input[type=password]');
+        for (const input of passwordBoxes) {
+            input.disabled = inviting;
+        }
+        const container = this.elem.querySelector('#password-input-container');
+        if (container) {
+            container.style.display = inviting ? 'none' : 'block';
+        }
+    }
+
+}
+
+export default NewUserPassword;
\ No newline at end of file
similarity index 73%
rename from resources/assets/js/components/overlay.js
rename to resources/js/components/overlay.js
index 1ba5efceadf553e3800f79ed50746d6984014f9f..ad6a01061ec033c0beaebc33fa003a665b57b0d7 100644 (file)
@@ -6,12 +6,22 @@ class Overlay {
         elem.addEventListener('click', event => {
              if (event.target === elem) return this.hide();
         });
+
+        window.addEventListener('keyup', event => {
+            if (event.key === 'Escape') {
+                this.hide();
+            }
+        });
+
         let closeButtons = elem.querySelectorAll('.popup-header-close');
         for (let i=0; i < closeButtons.length; i++) {
             closeButtons[i].addEventListener('click', this.hide.bind(this));
         }
     }
 
+    hide() { this.toggle(false); }
+    show() { this.toggle(true); }
+
     toggle(show = true) {
         let start = Date.now();
         let duration = 240;
@@ -22,6 +32,9 @@ class Overlay {
             this.container.style.opacity = targetOpacity;
             if (elapsedTime > duration) {
                 this.container.style.display = show ? 'flex' : 'none';
+                if (show) {
+                    this.focusOnBody();
+                }
                 this.container.style.opacity = '';
             } else {
                 requestAnimationFrame(setOpacity.bind(this));
@@ -31,8 +44,12 @@ class Overlay {
         requestAnimationFrame(setOpacity.bind(this));
     }
 
-    hide() { this.toggle(false); }
-    show() { this.toggle(true); }
+    focusOnBody() {
+        const body = this.container.querySelector('.popup-body');
+        if (body) {
+            body.focus();
+        }
+    }
 
 }
 
similarity index 96%
rename from resources/assets/js/components/page-comments.js
rename to resources/js/components/page-comments.js
index 975ff5a824b080bb5924ea46d557b1167103d364..5d8d169589804a5c92fd355281353bdcdaf6ed88 100644 (file)
@@ -1,4 +1,6 @@
 import MarkdownIt from "markdown-it";
+import {scrollAndHighlightElement} from "../services/util";
+
 const md = new MarkdownIt({ html: false });
 
 class PageComments {
@@ -24,10 +26,12 @@ class PageComments {
 
     handleAction(event) {
         let actionElem = event.target.closest('[action]');
+
         if (event.target.matches('a[href^="#"]')) {
-            let id = event.target.href.split('#')[1];
-            window.scrollAndHighlight(document.querySelector('#' + id));
+            const id = event.target.href.split('#')[1];
+            scrollAndHighlightElement(document.querySelector('#' + id));
         }
+
         if (actionElem === null) return;
         event.preventDefault();
 
@@ -132,7 +136,7 @@ class PageComments {
         this.formContainer.parentNode.style.display = 'block';
         this.elem.querySelector('[comment-add-button-container]').style.display = 'none';
         this.formInput.focus();
-        window.scrollToElement(this.formInput);
+        this.formInput.scrollIntoView({behavior: "smooth"});
     }
 
     hideForm() {
diff --git a/resources/js/components/page-display.js b/resources/js/components/page-display.js
new file mode 100644 (file)
index 0000000..2be1c1c
--- /dev/null
@@ -0,0 +1,201 @@
+import Clipboard from "clipboard/dist/clipboard.min";
+import Code from "../services/code";
+import * as DOM from "../services/dom";
+import {scrollAndHighlightElement} from "../services/util";
+
+class PageDisplay {
+
+    constructor(elem) {
+        this.elem = elem;
+        this.pageId = elem.getAttribute('page-display');
+
+        Code.highlight();
+        this.setupPointer();
+        this.setupNavHighlighting();
+
+        // Check the hash on load
+        if (window.location.hash) {
+            let text = window.location.hash.replace(/\%20/g, ' ').substr(1);
+            this.goToText(text);
+        }
+
+        // Sidebar page nav click event
+        const sidebarPageNav = document.querySelector('.sidebar-page-nav');
+        if (sidebarPageNav) {
+            DOM.onChildEvent(sidebarPageNav, 'a', 'click', (event, child) => {
+                event.preventDefault();
+                window.components['tri-layout'][0].showContent();
+                const contentId = child.getAttribute('href').substr(1);
+                this.goToText(contentId);
+                window.history.pushState(null, null, '#' + contentId);
+            });
+        }
+    }
+
+    goToText(text) {
+        const idElem = document.getElementById(text);
+
+        DOM.forEach('.page-content [data-highlighted]', elem => {
+            elem.removeAttribute('data-highlighted');
+            elem.style.backgroundColor = null;
+        });
+
+        if (idElem !== null) {
+            scrollAndHighlightElement(idElem);
+        } else {
+            const textElem = DOM.findText('.page-content > div > *', text);
+            if (textElem) {
+                scrollAndHighlightElement(textElem);
+            }
+        }
+    }
+
+    setupPointer() {
+        let pointer = document.getElementById('pointer');
+        if (!pointer) {
+            return;
+        }
+
+        // Set up pointer
+        pointer = pointer.parentNode.removeChild(pointer);
+        const pointerInner = pointer.querySelector('div.pointer');
+
+        // Instance variables
+        let pointerShowing = false;
+        let isSelection = false;
+        let pointerModeLink = true;
+        let pointerSectionId = '';
+
+        // Select all contents on input click
+        DOM.onChildEvent(pointer, 'input', 'click', (event, input) => {
+            input.select();
+            event.stopPropagation();
+        });
+
+        // Prevent closing pointer when clicked or focused
+        DOM.onEvents(pointer, ['click', 'focus'], event => {
+            event.stopPropagation();
+        });
+
+        // Pointer mode toggle
+        DOM.onChildEvent(pointer, 'span.icon', 'click', (event, icon) => {
+            event.stopPropagation();
+            pointerModeLink = !pointerModeLink;
+            icon.querySelector('[data-icon="include"]').style.display = (!pointerModeLink) ? 'inline' : 'none';
+            icon.querySelector('[data-icon="link"]').style.display = (pointerModeLink) ? 'inline' : 'none';
+            updatePointerContent();
+        });
+
+        // Set up clipboard
+        new Clipboard(pointer.querySelector('button'));
+
+        // Hide pointer when clicking away
+        DOM.onEvents(document.body, ['click', 'focus'], event => {
+            if (!pointerShowing || isSelection) return;
+            pointer = pointer.parentElement.removeChild(pointer);
+            pointerShowing = false;
+        });
+
+        let updatePointerContent = (element) => {
+            let inputText = pointerModeLink ? window.baseUrl(`/link/${this.pageId}#${pointerSectionId}`) : `{{@${this.pageId}#${pointerSectionId}}}`;
+            if (pointerModeLink && !inputText.startsWith('http')) {
+                inputText = window.location.protocol + "//" + window.location.host + inputText;
+            }
+
+            pointer.querySelector('input').value = inputText;
+
+            // Update anchor if present
+            const editAnchor = pointer.querySelector('#pointer-edit');
+            if (editAnchor && element) {
+                const editHref = editAnchor.dataset.editHref;
+                const elementId = element.id;
+
+                // get the first 50 characters.
+                const queryContent = element.textContent && element.textContent.substring(0, 50);
+                editAnchor.href = `${editHref}?content-id=${elementId}&content-text=${encodeURIComponent(queryContent)}`;
+            }
+        };
+
+        // Show pointer when selecting a single block of tagged content
+        DOM.forEach('.page-content [id^="bkmrk"]', bookMarkElem => {
+            DOM.onEvents(bookMarkElem, ['mouseup', 'keyup'], event => {
+                event.stopPropagation();
+                let selection = window.getSelection();
+                if (selection.toString().length === 0) return;
+
+                // Show pointer and set link
+                pointerSectionId = bookMarkElem.id;
+                updatePointerContent(bookMarkElem);
+
+                bookMarkElem.parentNode.insertBefore(pointer, bookMarkElem);
+                pointer.style.display = 'block';
+                pointerShowing = true;
+                isSelection = true;
+
+                // Set pointer to sit near mouse-up position
+                requestAnimationFrame(() => {
+                    const bookMarkBounds = bookMarkElem.getBoundingClientRect();
+                    let pointerLeftOffset = (event.pageX - bookMarkBounds.left - 164);
+                    if (pointerLeftOffset < 0) {
+                        pointerLeftOffset = 0
+                    }
+                    const pointerLeftOffsetPercent = (pointerLeftOffset / bookMarkBounds.width) * 100;
+
+                    pointerInner.style.left = pointerLeftOffsetPercent + '%';
+
+                    setTimeout(() => {
+                        isSelection = false;
+                    }, 100);
+                });
+
+            });
+        });
+    }
+
+    setupNavHighlighting() {
+        // Check if support is present for IntersectionObserver
+        if (!('IntersectionObserver' in window) ||
+            !('IntersectionObserverEntry' in window) ||
+            !('intersectionRatio' in window.IntersectionObserverEntry.prototype)) {
+            return;
+        }
+
+        let pageNav = document.querySelector('.sidebar-page-nav');
+
+        // fetch all the headings.
+        let headings = document.querySelector('.page-content').querySelectorAll('h1, h2, h3, h4, h5, h6');
+        // if headings are present, add observers.
+        if (headings.length > 0 && pageNav !== null) {
+            addNavObserver(headings);
+        }
+
+        function addNavObserver(headings) {
+            // Setup the intersection observer.
+            let intersectOpts = {
+                rootMargin: '0px 0px 0px 0px',
+                threshold: 1.0
+            };
+            let pageNavObserver = new IntersectionObserver(headingVisibilityChange, intersectOpts);
+
+            // observe each heading
+            for (let heading of headings) {
+                pageNavObserver.observe(heading);
+            }
+        }
+
+        function headingVisibilityChange(entries, observer) {
+            for (let entry of entries) {
+                let isVisible = (entry.intersectionRatio === 1);
+                toggleAnchorHighlighting(entry.target.id, isVisible);
+            }
+        }
+
+        function toggleAnchorHighlighting(elementId, shouldHighlight) {
+            DOM.forEach('a[href="#' + elementId + '"]', anchor => {
+                anchor.closest('li').classList.toggle('current-heading', shouldHighlight);
+            });
+        }
+    }
+}
+
+export default PageDisplay;
diff --git a/resources/js/components/setting-app-color-picker.js b/resources/js/components/setting-app-color-picker.js
new file mode 100644 (file)
index 0000000..6c0c0b3
--- /dev/null
@@ -0,0 +1,56 @@
+
+class SettingAppColorPicker {
+
+    constructor(elem) {
+        this.elem = elem;
+        this.colorInput = elem.querySelector('input[type=color]');
+        this.lightColorInput = elem.querySelector('input[name="setting-app-color-light"]');
+        this.resetButton = elem.querySelector('[setting-app-color-picker-reset]');
+
+        this.colorInput.addEventListener('change', this.updateColor.bind(this));
+        this.colorInput.addEventListener('input', this.updateColor.bind(this));
+        this.resetButton.addEventListener('click', event => {
+            this.colorInput.value = '#206ea7';
+            this.updateColor();
+        });
+    }
+
+    /**
+     * Update the app colors as a preview, and create a light version of the color.
+     */
+    updateColor() {
+        const hexVal = this.colorInput.value;
+        const rgb = this.hexToRgb(hexVal);
+        const rgbLightVal = 'rgba('+ [rgb.r, rgb.g, rgb.b, '0.15'].join(',') +')';
+
+        this.lightColorInput.value = rgbLightVal;
+
+        const customStyles = document.getElementById('custom-styles');
+        const oldColor = customStyles.getAttribute('data-color');
+        const oldColorLight = customStyles.getAttribute('data-color-light');
+
+        customStyles.innerHTML = customStyles.innerHTML.split(oldColor).join(hexVal);
+        customStyles.innerHTML = customStyles.innerHTML.split(oldColorLight).join(rgbLightVal);
+
+        customStyles.setAttribute('data-color', hexVal);
+        customStyles.setAttribute('data-color-light', rgbLightVal);
+    }
+
+    /**
+     * Covert a hex color code to rgb components.
+     * @attribution https://stackoverflow.com/a/5624139
+     * @param hex
+     * @returns {*}
+     */
+    hexToRgb(hex) {
+        const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
+        return {
+            r: result ? parseInt(result[1], 16) : 0,
+            g: result ? parseInt(result[2], 16) : 0,
+            b: result ? parseInt(result[3], 16) : 0
+        };
+    }
+
+}
+
+export default SettingAppColorPicker;
\ No newline at end of file
diff --git a/resources/js/components/shelf-sort.js b/resources/js/components/shelf-sort.js
new file mode 100644 (file)
index 0000000..38e8ae8
--- /dev/null
@@ -0,0 +1,56 @@
+import Sortable from "sortablejs";
+
+class ShelfSort {
+
+    constructor(elem) {
+        this.elem = elem;
+        this.input = document.getElementById('books-input');
+        this.shelfBooksList = elem.querySelector('[shelf-sort-assigned-books]');
+
+        this.initSortable();
+        this.setupListeners();
+    }
+
+    initSortable() {
+        const scrollBoxes = this.elem.querySelectorAll('.scroll-box');
+        for (let scrollBox of scrollBoxes) {
+            new Sortable(scrollBox, {
+                group: 'shelf-books',
+                ghostClass: 'primary-background-light',
+                animation: 150,
+                onSort: this.onChange.bind(this),
+            });
+        }
+    }
+
+    setupListeners() {
+        this.elem.addEventListener('click', event => {
+            const sortItem = event.target.closest('.scroll-box-item:not(.instruction)');
+            if (sortItem) {
+                event.preventDefault();
+                this.sortItemClick(sortItem);
+            }
+        });
+    }
+
+    /**
+     * Called when a sort item is clicked.
+     * @param {Element} sortItem
+     */
+    sortItemClick(sortItem) {
+        const lists = this.elem.querySelectorAll('.scroll-box');
+        const newList = Array.from(lists).filter(list => sortItem.parentElement !== list);
+        if (newList.length > 0) {
+            newList[0].appendChild(sortItem);
+        }
+        this.onChange();
+    }
+
+    onChange() {
+        const shelfBookElems = Array.from(this.shelfBooksList.querySelectorAll('[data-id]'));
+        this.input.value = shelfBookElems.map(elem => elem.getAttribute('data-id')).join(',');
+    }
+
+}
+
+export default ShelfSort;
\ No newline at end of file
diff --git a/resources/js/components/template-manager.js b/resources/js/components/template-manager.js
new file mode 100644 (file)
index 0000000..d004a43
--- /dev/null
@@ -0,0 +1,94 @@
+import * as DOM from "../services/dom";
+
+class TemplateManager {
+
+    constructor(elem) {
+        this.elem = elem;
+        this.list = elem.querySelector('[template-manager-list]');
+        this.searching = false;
+
+        // Template insert action buttons
+        DOM.onChildEvent(this.elem, '[template-action]', 'click', this.handleTemplateActionClick.bind(this));
+
+        // Template list pagination click
+        DOM.onChildEvent(this.elem, '.pagination a', 'click', this.handlePaginationClick.bind(this));
+
+        // Template list item content click
+        DOM.onChildEvent(this.elem, '.template-item-content', 'click', this.handleTemplateItemClick.bind(this));
+
+        // Template list item drag start
+        DOM.onChildEvent(this.elem, '.template-item', 'dragstart', this.handleTemplateItemDragStart.bind(this));
+
+        this.setupSearchBox();
+    }
+
+    handleTemplateItemClick(event, templateItem) {
+        const templateId = templateItem.closest('[template-id]').getAttribute('template-id');
+        this.insertTemplate(templateId, 'replace');
+    }
+
+    handleTemplateItemDragStart(event, templateItem) {
+        const templateId = templateItem.closest('[template-id]').getAttribute('template-id');
+        event.dataTransfer.setData('bookstack/template', templateId);
+        event.dataTransfer.setData('text/plain', templateId);
+    }
+
+    handleTemplateActionClick(event, actionButton) {
+        event.stopPropagation();
+
+        const action = actionButton.getAttribute('template-action');
+        const templateId = actionButton.closest('[template-id]').getAttribute('template-id');
+        this.insertTemplate(templateId, action);
+    }
+
+    async insertTemplate(templateId, action = 'replace') {
+        const resp = await window.$http.get(`/templates/${templateId}`);
+        const eventName = 'editor::' + action;
+        window.$events.emit(eventName, resp.data);
+    }
+
+    async handlePaginationClick(event, paginationLink) {
+        event.preventDefault();
+        const paginationUrl = paginationLink.getAttribute('href');
+        const resp = await window.$http.get(paginationUrl);
+        this.list.innerHTML = resp.data;
+    }
+
+    setupSearchBox() {
+        const searchBox = this.elem.querySelector('.search-box');
+        const input = searchBox.querySelector('input');
+        const submitButton = searchBox.querySelector('button');
+        const cancelButton = searchBox.querySelector('button.search-box-cancel');
+
+        async function performSearch() {
+            const searchTerm = input.value;
+            const resp = await window.$http.get(`/templates`, {
+                search: searchTerm
+            });
+            cancelButton.style.display = searchTerm ? 'block' : 'none';
+            this.list.innerHTML = resp.data;
+        }
+        performSearch = performSearch.bind(this);
+
+        // Searchbox enter press
+        searchBox.addEventListener('keypress', event => {
+            if (event.key === 'Enter') {
+                event.preventDefault();
+                performSearch();
+            }
+        });
+
+        // Submit button press
+        submitButton.addEventListener('click', event => {
+            performSearch();
+        });
+
+        // Cancel button press
+        cancelButton.addEventListener('click', event => {
+            input.value = '';
+            performSearch();
+        });
+    }
+}
+
+export default TemplateManager;
\ No newline at end of file
diff --git a/resources/js/components/toggle-switch.js b/resources/js/components/toggle-switch.js
new file mode 100644 (file)
index 0000000..b9b96af
--- /dev/null
@@ -0,0 +1,23 @@
+
+class ToggleSwitch {
+
+    constructor(elem) {
+        this.elem = elem;
+        this.input = elem.querySelector('input[type=hidden]');
+        this.checkbox = elem.querySelector('input[type=checkbox]');
+
+        this.checkbox.addEventListener('change', this.stateChange.bind(this));
+    }
+
+    stateChange() {
+        this.input.value = (this.checkbox.checked ? 'true' : 'false');
+
+        // Dispatch change event from hidden input so they can be listened to
+        // like a normal checkbox.
+        const changeEvent = new Event('change');
+        this.input.dispatchEvent(changeEvent);
+    }
+
+}
+
+export default ToggleSwitch;
\ No newline at end of file
similarity index 67%
rename from resources/assets/js/components/tri-layout.js
rename to resources/js/components/tri-layout.js
index 0ae7df976b37e3ad97835a3da3f61e1a20659bd0..905ca03b1020d566859366d6e2ecc8e851edc784 100644 (file)
@@ -66,28 +66,46 @@ class TriLayout {
      */
     mobileTabClick(event) {
         const tab = event.target.getAttribute('tri-layout-mobile-tab');
+        this.showTab(tab);
+    }
+
+    /**
+     * Show the content tab.
+     * Used by the page-display component.
+     */
+    showContent() {
+        this.showTab('content', false);
+    }
+
+    /**
+     * Show the given tab
+     * @param tabName
+     */
+    showTab(tabName, scroll = true) {
         this.scrollCache[this.lastTabShown] = document.documentElement.scrollTop;
 
         // Set tab status
-        const activeTabs = document.querySelectorAll('.tri-layout-mobile-tab.active');
-        for (let tab of activeTabs) {
-            tab.classList.remove('active');
+        const tabs = document.querySelectorAll('.tri-layout-mobile-tab');
+        for (let tab of tabs) {
+            const isActive = (tab.getAttribute('tri-layout-mobile-tab') === tabName);
+            tab.classList.toggle('active', isActive);
         }
-        event.target.classList.add('active');
 
         // Toggle section
-        const showInfo = (tab === 'info');
+        const showInfo = (tabName === 'info');
         this.elem.classList.toggle('show-info', showInfo);
 
         // Set the scroll position from cache
-        const pageHeader = document.querySelector('header');
-        const defaultScrollTop = pageHeader.getBoundingClientRect().bottom;
-        document.documentElement.scrollTop = this.scrollCache[tab] || defaultScrollTop;
-        setTimeout(() => {
-            document.documentElement.scrollTop = this.scrollCache[tab] || defaultScrollTop;
-        }, 50);
-
-        this.lastTabShown = tab;
+        if (scroll) {
+            const pageHeader = document.querySelector('header');
+            const defaultScrollTop = pageHeader.getBoundingClientRect().bottom;
+            document.documentElement.scrollTop = this.scrollCache[tabName] || defaultScrollTop;
+            setTimeout(() => {
+                document.documentElement.scrollTop = this.scrollCache[tabName] || defaultScrollTop;
+            }, 50);
+        }
+
+        this.lastTabShown = tabName;
     }
 
 }
similarity index 89%
rename from resources/assets/js/components/wysiwyg-editor.js
rename to resources/js/components/wysiwyg-editor.js
index 614cfb80f149fdba130ee19645973f6cb426b2fe..60a6743ea7679bdebb7100bd2c047e1b26101166 100644 (file)
@@ -168,23 +168,24 @@ function codePlugin() {
         });
     }
 
-    function codeMirrorContainerToPre($codeMirrorContainer) {
-        let textArea = $codeMirrorContainer[0].querySelector('textarea');
-        let code = textArea.textContent;
-        let lang = $codeMirrorContainer[0].getAttribute('data-lang');
-
-        $codeMirrorContainer.removeAttr('contentEditable');
-        let $pre = $('<pre></pre>');
-        $pre.append($('<code></code>').each((index, elem) => {
-            // Needs to be textContent since innerText produces BR:s
-            elem.textContent = code;
-        }).attr('class', `language-${lang}`));
-        $codeMirrorContainer.replaceWith($pre);
+    function codeMirrorContainerToPre(codeMirrorContainer) {
+        const textArea = codeMirrorContainer.querySelector('textarea');
+        const code = textArea.textContent;
+        const lang = codeMirrorContainer.getAttribute('data-lang');
+
+        codeMirrorContainer.removeAttribute('contentEditable');
+        const pre = document.createElement('pre');
+        const codeElem = document.createElement('code');
+        codeElem.classList.add(`language-${lang}`);
+        codeElem.textContent = code;
+        pre.appendChild(codeElem);
+
+        codeMirrorContainer.parentElement.replaceChild(pre, codeMirrorContainer);
     }
 
     window.tinymce.PluginManager.add('codeeditor', function(editor, url) {
 
-        let $ = editor.$;
+        const $ = editor.$;
 
         editor.addButton('codeeditor', {
             text: 'Code block',
@@ -198,10 +199,8 @@ function codePlugin() {
 
         // Convert
         editor.on('PreProcess', function (e) {
-            $('div.CodeMirrorContainer', e.node).
-            each((index, elem) => {
-                let $elem = $(elem);
-                codeMirrorContainerToPre($elem);
+            $('div.CodeMirrorContainer', e.node).each((index, elem) => {
+                codeMirrorContainerToPre(elem);
             });
         });
 
@@ -217,10 +216,10 @@ function codePlugin() {
             $('.CodeMirrorContainer').filter((index ,elem) => {
                 return typeof elem.querySelector('.CodeMirror').CodeMirror === 'undefined';
             }).each((index, elem) => {
-                codeMirrorContainerToPre($(elem));
+                codeMirrorContainerToPre(elem);
             });
 
-            let codeSamples = $('body > pre').filter((index, elem) => {
+            const codeSamples = $('body > pre').filter((index, elem) => {
                 return elem.contentEditable !== "false";
             });
 
@@ -341,7 +340,7 @@ function drawIoPlugin() {
         });
 
         editor.on('SetContent', function () {
-            let drawings = editor.$('body > div[drawio-diagram]');
+            const drawings = editor.$('body > div[drawio-diagram]');
             if (!drawings.length) return;
 
             editor.undoManager.transact(function () {
@@ -379,6 +378,27 @@ function customHrPlugin() {
 }
 
 
+function listenForBookStackEditorEvents(editor) {
+
+    // Replace editor content
+    window.$events.listen('editor::replace', ({html}) => {
+        editor.setContent(html);
+    });
+
+    // Append editor content
+    window.$events.listen('editor::append', ({html}) => {
+        const content = editor.getContent() + html;
+        editor.setContent(content);
+    });
+
+    // Prepend editor content
+    window.$events.listen('editor::prepend', ({html}) => {
+        const content = html + editor.getContent();
+        editor.setContent(content);
+    });
+
+}
+
 class WysiwygEditor {
 
     constructor(elem) {
@@ -392,6 +412,7 @@ class WysiwygEditor {
         this.loadPlugins();
 
         this.tinyMceConfig = this.getTinyMceConfig();
+        window.$events.emitPublic(elem, 'editor-tinymce::pre-init', {config: this.tinyMceConfig});
         window.tinymce.init(this.tinyMceConfig);
     }
 
@@ -472,9 +493,10 @@ class WysiwygEditor {
 
                 if (type === 'file') {
                     window.EntitySelectorPopup.show(function(entity) {
-                        let originalField = win.document.getElementById(field_name);
+                        const originalField = win.document.getElementById(field_name);
                         originalField.value = entity.link;
-                        $(originalField).closest('.mce-form').find('input').eq(2).val(entity.name);
+                        const mceForm = originalField.closest('.mce-form');
+                        mceForm.querySelectorAll('input')[2].value = entity.name;
                     });
                 }
 
@@ -553,6 +575,10 @@ class WysiwygEditor {
                     editor.focus();
                 }
 
+                listenForBookStackEditorEvents(editor);
+
+                // TODO - Update to standardise across both editors
+                // Use events within listenForBookStackEditorEvents instead (Different event signature)
                 window.$events.listen('editor-html-update', html => {
                     editor.setContent(html);
                     editor.selection.select(editor.getBody(), true);
@@ -583,6 +609,18 @@ class WysiwygEditor {
                     let dom = editor.dom,
                         rng = tinymce.dom.RangeUtils.getCaretRangeFromPoint(event.clientX, event.clientY, editor.getDoc());
 
+                    // Template insertion
+                    const templateId = event.dataTransfer.getData('bookstack/template');
+                    if (templateId) {
+                        event.preventDefault();
+                        window.$http.get(`/templates/${templateId}`).then(resp => {
+                            editor.selection.setRng(rng);
+                            editor.undoManager.transact(function () {
+                                editor.execCommand('mceInsertContent', false, resp.data.html);
+                            });
+                        });
+                    }
+
                     // Don't allow anything to be dropped in a captioned image.
                     if (dom.getParent(rng.startContainer, '.mceTemp')) {
                         event.preventDefault();
@@ -617,6 +655,8 @@ class WysiwygEditor {
                 // Paste image-uploads
                 editor.on('paste', event => editorPaste(event, editor, context));
 
+                // Custom handler hook
+                window.$events.emitPublic(context.elem, 'editor-tinymce::setup', {editor});
             }
         };
     }
similarity index 68%
rename from resources/assets/js/index.js
rename to resources/js/index.js
index f202c322e52a813576195410e820a14e35227d7e..e0c7b34e5a85d5c22e57236c04346c95b7041a38 100644 (file)
@@ -1,6 +1,3 @@
-// Global Polyfills
-import "./services/dom-polyfills"
-
 // Url retrieval function
 window.baseUrl = function(path) {
     let basePath = document.querySelector('meta[name="base-url"]').getAttribute('content');
@@ -11,27 +8,24 @@ window.baseUrl = function(path) {
 
 // Set events and http services on window
 import Events from "./services/events"
-import Http from "./services/http"
-let httpInstance = Http();
+import httpInstance from "./services/http"
+const eventManager = new Events();
 window.$http = httpInstance;
-window.$events = new Events();
+window.$events = eventManager;
 
 // Translation setup
 // Creates a global function with name 'trans' to be used in the same way as Laravel's translation system
 import Translations from "./services/translations"
-let translator = new Translations(window.translations);
+const translator = new Translations();
 window.trans = translator.get.bind(translator);
 window.trans_choice = translator.getPlural.bind(translator);
 
-// Load in global UI helpers and libraries including jQuery
-import "./services/global-ui"
-
-// Set services on Vue
+// Make services available to Vue instances
 import Vue from "vue"
 Vue.prototype.$http = httpInstance;
-Vue.prototype.$events = window.$events;
+Vue.prototype.$events = eventManager;
 
-// Load vues and components
+// Load Vues and components
 import vues from "./vues/vues"
 import components from "./components"
 vues();
diff --git a/resources/js/services/animations.js b/resources/js/services/animations.js
new file mode 100644 (file)
index 0000000..b6158ea
--- /dev/null
@@ -0,0 +1,114 @@
+/**
+ * Used in the function below to store references of clean-up functions.
+ * Used to ensure only one transitionend function exists at any time.
+ * @type {WeakMap<object, any>}
+ */
+const animateStylesCleanupMap = new WeakMap();
+
+/**
+ * Fade out the given element.
+ * @param {Element} element
+ * @param {Number} animTime
+ * @param {Function|null} onComplete
+ */
+export function fadeOut(element, animTime = 400, onComplete = null) {
+    cleanupExistingElementAnimation(element);
+    animateStyles(element, {
+        opacity: ['1', '0']
+    }, animTime, () => {
+        element.style.display = 'none';
+        if (onComplete) onComplete();
+    });
+}
+
+/**
+ * Hide the element by sliding the contents upwards.
+ * @param {Element} element
+ * @param {Number} animTime
+ */
+export function slideUp(element, animTime = 400) {
+    cleanupExistingElementAnimation(element);
+    const currentHeight = element.getBoundingClientRect().height;
+    const computedStyles = getComputedStyle(element);
+    const currentPaddingTop = computedStyles.getPropertyValue('padding-top');
+    const currentPaddingBottom = computedStyles.getPropertyValue('padding-bottom');
+    const animStyles = {
+        height: [`${currentHeight}px`, '0px'],
+        overflow: ['hidden', 'hidden'],
+        paddingTop: [currentPaddingTop, '0px'],
+        paddingBottom: [currentPaddingBottom, '0px'],
+    };
+
+    animateStyles(element, animStyles, animTime, () => {
+        element.style.display = 'none';
+    });
+}
+
+/**
+ * Show the given element by expanding the contents.
+ * @param {Element} element - Element to animate
+ * @param {Number} animTime - Animation time in ms
+ */
+export function slideDown(element, animTime = 400) {
+    cleanupExistingElementAnimation(element);
+    element.style.display = 'block';
+    const targetHeight = element.getBoundingClientRect().height;
+    const computedStyles = getComputedStyle(element);
+    const targetPaddingTop = computedStyles.getPropertyValue('padding-top');
+    const targetPaddingBottom = computedStyles.getPropertyValue('padding-bottom');
+    const animStyles = {
+        height: ['0px', `${targetHeight}px`],
+        overflow: ['hidden', 'hidden'],
+        paddingTop: ['0px', targetPaddingTop],
+        paddingBottom: ['0px', targetPaddingBottom],
+    };
+
+    animateStyles(element, animStyles, animTime);
+}
+
+/**
+ * Animate the css styles of an element using FLIP animation techniques.
+ * Styles must be an object where the keys are style properties, camelcase, and the values
+ * are an array of two items in the format [initialValue, finalValue]
+ * @param {Element} element
+ * @param {Object} styles
+ * @param {Number} animTime
+ * @param {Function} onComplete
+ */
+function animateStyles(element, styles, animTime = 400, onComplete = null) {
+    const styleNames = Object.keys(styles);
+    for (let style of styleNames) {
+        element.style[style] = styles[style][0];
+    }
+
+    const cleanup = () => {
+        for (let style of styleNames) {
+            element.style[style] = null;
+        }
+        element.style.transition = null;
+        element.removeEventListener('transitionend', cleanup);
+        animateStylesCleanupMap.delete(element);
+        if (onComplete) onComplete();
+    };
+
+    setTimeout(() => {
+        element.style.transition = `all ease-in-out ${animTime}ms`;
+        for (let style of styleNames) {
+            element.style[style] = styles[style][1];
+        }
+
+        element.addEventListener('transitionend', cleanup);
+        animateStylesCleanupMap.set(element, cleanup);
+    }, 15);
+}
+
+/**
+ * Run the active cleanup action for the given element.
+ * @param {Element} element
+ */
+function cleanupExistingElementAnimation(element) {
+    if (animateStylesCleanupMap.has(element)) {
+        const oldCleanup = animateStylesCleanupMap.get(element);
+        oldCleanup();
+    }
+}
\ No newline at end of file
similarity index 95%
rename from resources/assets/js/services/code.js
rename to resources/js/services/code.js
index 1e0e48289d595132a5ef0e08d006d3c249352d3f..eedbda4bb75b147add1f70a366735bddaa5cdc0d 100644 (file)
@@ -102,6 +102,7 @@ function highlightElem(elem) {
         value: content,
         mode:  mode,
         lineNumbers: true,
+        lineWrapping: false,
         theme: getTheme(),
         readOnly: true
     });
@@ -188,6 +189,7 @@ function wysiwygView(elem) {
         value: content,
         mode:  getMode(lang),
         lineNumbers: true,
+        lineWrapping: false,
         theme: getTheme(),
         readOnly: true
     });
@@ -213,8 +215,8 @@ function popupEditor(elem, modeSuggestion) {
         value: content,
         mode:  getMode(modeSuggestion),
         lineNumbers: true,
-        theme: getTheme(),
-        lineWrapping: true
+        lineWrapping: false,
+        theme: getTheme()
     });
 }
 
@@ -240,24 +242,27 @@ function setContent(cmInstance, codeContent) {
 }
 
 /**
- * Get a CodeMirror instace to use for the markdown editor.
+ * Get a CodeMirror instance to use for the markdown editor.
  * @param {HTMLElement} elem
  * @returns {*}
  */
 function markdownEditor(elem) {
-    let content = elem.textContent;
-
-    return CodeMirror(function (elt) {
-        elem.parentNode.insertBefore(elt, elem);
-        elem.style.display = 'none';
-    }, {
+    const content = elem.textContent;
+    const config = {
         value: content,
         mode: "markdown",
         lineNumbers: true,
-        theme: getTheme(),
         lineWrapping: true,
+        theme: getTheme(),
         scrollPastEnd: true,
-    });
+    };
+
+    window.$events.emitPublic(elem, 'editor-markdown-cm::pre-init', {config});
+
+    return CodeMirror(function (elt) {
+        elem.parentNode.insertBefore(elt, elem);
+        elem.style.display = 'none';
+    }, config);
 }
 
 /**
diff --git a/resources/js/services/dom.js b/resources/js/services/dom.js
new file mode 100644 (file)
index 0000000..966a454
--- /dev/null
@@ -0,0 +1,75 @@
+/**
+ * Run the given callback against each element that matches the given selector.
+ * @param {String} selector
+ * @param {Function<Element>} callback
+ */
+export function forEach(selector, callback) {
+    const elements = document.querySelectorAll(selector);
+    for (let element of elements) {
+        callback(element);
+    }
+}
+
+/**
+ * Helper to listen to multiple DOM events
+ * @param {Element} listenerElement
+ * @param {Array<String>} events
+ * @param {Function<Event>} callback
+ */
+export function onEvents(listenerElement, events, callback) {
+    for (let eventName of events) {
+        listenerElement.addEventListener(eventName, callback);
+    }
+}
+
+/**
+ * Helper to run an action when an element is selected.
+ * A "select" is made to be accessible, So can be a click, space-press or enter-press.
+ * @param listenerElement
+ * @param callback
+ */
+export function onSelect(listenerElement, callback) {
+    listenerElement.addEventListener('click', callback);
+    listenerElement.addEventListener('keydown', (event) => {
+        if (event.key === 'Enter' || event.key === ' ') {
+            event.preventDefault();
+            callback(event);
+        }
+    });
+}
+
+/**
+ * Set a listener on an element for an event emitted by a child
+ * matching the given childSelector param.
+ * Used in a similar fashion to jQuery's $('listener').on('eventName', 'childSelector', callback)
+ * @param {Element} listenerElement
+ * @param {String} childSelector
+ * @param {String} eventName
+ * @param {Function} callback
+ */
+export function onChildEvent(listenerElement, childSelector, eventName, callback) {
+    listenerElement.addEventListener(eventName, function(event) {
+        const matchingChild = event.target.closest(childSelector);
+        if (matchingChild) {
+            callback.call(matchingChild, event, matchingChild);
+        }
+    });
+}
+
+/**
+ * Look for elements that match the given selector and contain the given text.
+ * Is case insensitive and returns the first result or null if nothing is found.
+ * @param {String} selector
+ * @param {String} text
+ * @returns {Element}
+ */
+export function findText(selector, text) {
+    const elements = document.querySelectorAll(selector);
+    text = text.toLowerCase();
+    for (let element of elements) {
+        if (element.textContent.toLowerCase().includes(text)) {
+            return element;
+        }
+    }
+    return null;
+}
\ No newline at end of file
diff --git a/resources/js/services/events.js b/resources/js/services/events.js
new file mode 100644 (file)
index 0000000..fa3ed7f
--- /dev/null
@@ -0,0 +1,55 @@
+/**
+ * Simple global events manager
+ */
+class Events {
+    constructor() {
+        this.listeners = {};
+        this.stack = [];
+    }
+
+    /**
+     * Emit a custom event for any handlers to pick-up.
+     * @param {String} eventName
+     * @param {*} eventData
+     * @returns {Events}
+     */
+    emit(eventName, eventData) {
+        this.stack.push({name: eventName, data: eventData});
+        if (typeof this.listeners[eventName] === 'undefined') return this;
+        let eventsToStart = this.listeners[eventName];
+        for (let i = 0; i < eventsToStart.length; i++) {
+            let event = eventsToStart[i];
+            event(eventData);
+        }
+        return this;
+    }
+
+    /**
+     * Listen to a custom event and run the given callback when that event occurs.
+     * @param {String} eventName
+     * @param {Function} callback
+     * @returns {Events}
+     */
+    listen(eventName, callback) {
+        if (typeof this.listeners[eventName] === 'undefined') this.listeners[eventName] = [];
+        this.listeners[eventName].push(callback);
+        return this;
+    }
+
+    /**
+     * Emit an event for public use.
+     * Sends the event via the native DOM event handling system.
+     * @param {Element} targetElement
+     * @param {String} eventName
+     * @param {Object} eventData
+     */
+    emitPublic(targetElement, eventName, eventData) {
+        const event = new CustomEvent(eventName, {
+            detail: eventData,
+            bubbles: true
+        });
+        targetElement.dispatchEvent(event);
+    }
+}
+
+export default Events;
\ No newline at end of file
diff --git a/resources/js/services/http.js b/resources/js/services/http.js
new file mode 100644 (file)
index 0000000..06dac98
--- /dev/null
@@ -0,0 +1,146 @@
+
+/**
+ * Perform a HTTP GET request.
+ * Can easily pass query parameters as the second parameter.
+ * @param {String} url
+ * @param {Object} params
+ * @returns {Promise<{headers: Headers, original: Response, data: (Object|String), redirected: boolean, statusText: string, url: string, status: number}>}
+ */
+async function get(url, params = {}) {
+    return request(url, {
+        method: 'GET',
+        params,
+    });
+}
+
+/**
+ * Perform a HTTP POST request.
+ * @param {String} url
+ * @param {Object} data
+ * @returns {Promise<{headers: Headers, original: Response, data: (Object|String), redirected: boolean, statusText: string, url: string, status: number}>}
+ */
+async function post(url, data = null) {
+    return dataRequest('POST', url, data);
+}
+
+/**
+ * Perform a HTTP PUT request.
+ * @param {String} url
+ * @param {Object} data
+ * @returns {Promise<{headers: Headers, original: Response, data: (Object|String), redirected: boolean, statusText: string, url: string, status: number}>}
+ */
+async function put(url, data = null) {
+    return dataRequest('PUT', url, data);
+}
+
+/**
+ * Perform a HTTP PATCH request.
+ * @param {String} url
+ * @param {Object} data
+ * @returns {Promise<{headers: Headers, original: Response, data: (Object|String), redirected: boolean, statusText: string, url: string, status: number}>}
+ */
+async function patch(url, data = null) {
+    return dataRequest('PATCH', url, data);
+}
+
+/**
+ * Perform a HTTP DELETE request.
+ * @param {String} url
+ * @param {Object} data
+ * @returns {Promise<{headers: Headers, original: Response, data: (Object|String), redirected: boolean, statusText: string, url: string, status: number}>}
+ */
+async function performDelete(url, data = null) {
+    return dataRequest('DELETE', url, data);
+}
+
+/**
+ * Perform a HTTP request to the back-end that includes data in the body.
+ * Parses the body to JSON if an object, setting the correct headers.
+ * @param {String} method
+ * @param {String} url
+ * @param {Object} data
+ * @returns {Promise<{headers: Headers, original: Response, data: (Object|String), redirected: boolean, statusText: string, url: string, status: number}>}
+ */
+async function dataRequest(method, url, data = null) {
+    const options = {
+        method: method,
+        body: data,
+    };
+
+    if (typeof data === 'object' && !(data instanceof FormData)) {
+        options.headers = {'Content-Type': 'application/json'};
+        options.body = JSON.stringify(data);
+    }
+
+    return request(url, options)
+}
+
+/**
+ * Create a new HTTP request, setting the required CSRF information
+ * to communicate with the back-end. Parses & formats the response.
+ * @param {String} url
+ * @param {Object} options
+ * @returns {Promise<{headers: Headers, original: Response, data: (Object|String), redirected: boolean, statusText: string, url: string, status: number}>}
+ */
+async function request(url, options = {}) {
+    if (!url.startsWith('http')) {
+        url = window.baseUrl(url);
+    }
+
+    if (options.params) {
+        const urlObj = new URL(url);
+        for (let paramName of Object.keys(options.params)) {
+            const value = options.params[paramName];
+            if (typeof value !== 'undefined' && value !== null) {
+                urlObj.searchParams.set(paramName, value);
+            }
+        }
+        url = urlObj.toString();
+    }
+
+    const csrfToken = document.querySelector('meta[name=token]').getAttribute('content');
+    options = Object.assign({}, options, {
+        'credentials': 'same-origin',
+    });
+    options.headers = Object.assign({}, options.headers || {}, {
+        'baseURL': window.baseUrl(''),
+        'X-CSRF-TOKEN': csrfToken,
+    });
+
+    const response = await fetch(url, options);
+    const content = await getResponseContent(response);
+    return {
+        data: content,
+        headers: response.headers,
+        redirected: response.redirected,
+        status: response.status,
+        statusText: response.statusText,
+        url: response.url,
+        original: response,
+    }
+}
+
+/**
+ * Get the content from a fetch response.
+ * Checks the content-type header to determine the format.
+ * @param response
+ * @returns {Promise<Object|String>}
+ */
+async function getResponseContent(response) {
+    const responseContentType = response.headers.get('Content-Type');
+    const subType = responseContentType.split('/').pop();
+
+    if (subType === 'javascript' || subType === 'json') {
+        return await response.json();
+    }
+
+    return await response.text();
+}
+
+export default {
+    get: get,
+    post: post,
+    put: put,
+    patch: patch,
+    delete: performDelete,
+};
\ No newline at end of file
similarity index 65%
rename from resources/assets/js/services/translations.js
rename to resources/js/services/translations.js
index 06b44a58010ffe264174669f79e239a3a5982e19..b595a05e6f95b8e9a2d7862adbc5b5d003f6e163 100644 (file)
@@ -10,7 +10,20 @@ class Translator {
      * @param translations
      */
     constructor(translations) {
-        this.store = translations;
+        this.store = new Map();
+        this.parseTranslations();
+    }
+
+    /**
+     * Parse translations out of the page and place into the store.
+     */
+    parseTranslations() {
+        const translationMetaTags = document.querySelectorAll('meta[name="translation"]');
+        for (let tag of translationMetaTags) {
+            const key = tag.getAttribute('key');
+            const value = tag.getAttribute('value');
+            this.store.set(key, value);
+        }
     }
 
     /**
@@ -20,7 +33,7 @@ class Translator {
      * @returns {*}
      */
     get(key, replacements) {
-        let text = this.getTransText(key);
+        const text = this.getTransText(key);
         return this.performReplacements(text, replacements);
     }
 
@@ -33,26 +46,24 @@ class Translator {
      * @returns {*}
      */
     getPlural(key, count, replacements) {
-        let text = this.getTransText(key);
-        let splitText = text.split('|');
+        const text = this.getTransText(key);
+        const splitText = text.split('|');
+        const exactCountRegex = /^{([0-9]+)}/;
+        const rangeRegex = /^\[([0-9]+),([0-9*]+)]/;
         let result = null;
-        let exactCountRegex = /^{([0-9]+)}/;
-        let rangeRegex = /^\[([0-9]+),([0-9*]+)]/;
-
-        for (let i = 0, len = splitText.length; i < len; i++) {
-            let t = splitText[i];
 
+        for (let t of splitText) {
             // Parse exact matches
-            let exactMatches = t.match(exactCountRegex);
+            const exactMatches = t.match(exactCountRegex);
             if (exactMatches !== null && Number(exactMatches[1]) === count) {
                 result = t.replace(exactCountRegex, '').trim();
                 break;
             }
 
             // Parse range matches
-            let rangeMatches = t.match(rangeRegex);
+            const rangeMatches = t.match(rangeRegex);
             if (rangeMatches !== null) {
-                let rangeStart = Number(rangeMatches[1]);
+                const rangeStart = Number(rangeMatches[1]);
                 if (rangeStart <= count && (rangeMatches[2] === '*' || Number(rangeMatches[2]) >= count)) {
                     result = t.replace(rangeRegex, '').trim();
                     break;
@@ -64,7 +75,10 @@ class Translator {
             result = (count === 1) ? splitText[0] : splitText[1];
         }
 
-        if (result === null) result = splitText[0];
+        if (result === null) {
+            result = splitText[0];
+        }
+
         return this.performReplacements(result, replacements);
     }
 
@@ -74,14 +88,10 @@ class Translator {
      * @returns {String|Object}
      */
     getTransText(key) {
-        let splitKey = key.split('.');
-        let value = splitKey.reduce((a, b) => {
-            return a !== undefined ? a[b] : a;
-        }, this.store);
+        const value = this.store.get(key);
 
         if (value === undefined) {
-            console.log(`Translation with key "${key}" does not exist`);
-            value = key;
+            console.warn(`Translation with key "${key}" does not exist`);
         }
 
         return value;
@@ -95,10 +105,10 @@ class Translator {
      */
     performReplacements(string, replacements) {
         if (!replacements) return string;
-        let replaceMatches = string.match(/:([\S]+)/g);
+        const replaceMatches = string.match(/:([\S]+)/g);
         if (replaceMatches === null) return string;
         replaceMatches.forEach(match => {
-            let key = match.substring(1);
+            const key = match.substring(1);
             if (typeof replacements[key] === 'undefined') return;
             string = string.replace(match, replacements[key]);
         });
diff --git a/resources/js/services/util.js b/resources/js/services/util.js
new file mode 100644 (file)
index 0000000..b2f2918
--- /dev/null
@@ -0,0 +1,48 @@
+
+
+/**
+ * Returns a function, that, as long as it continues to be invoked, will not
+ * be triggered. The function will be called after it stops being called for
+ * N milliseconds. If `immediate` is passed, trigger the function on the
+ * leading edge, instead of the trailing.
+ * @attribution https://davidwalsh.name/javascript-debounce-function
+ * @param func
+ * @param wait
+ * @param immediate
+ * @returns {Function}
+ */
+export function debounce(func, wait, immediate) {
+    let timeout;
+    return function() {
+        const context = this, args = arguments;
+        const later = function() {
+            timeout = null;
+            if (!immediate) func.apply(context, args);
+        };
+        const callNow = immediate && !timeout;
+        clearTimeout(timeout);
+        timeout = setTimeout(later, wait);
+        if (callNow) func.apply(context, args);
+    };
+};
+
+/**
+ * Scroll and highlight an element.
+ * @param {HTMLElement} element
+ */
+export function scrollAndHighlightElement(element) {
+    if (!element) return;
+    element.scrollIntoView({behavior: 'smooth'});
+
+    const color = document.getElementById('custom-styles').getAttribute('data-color-light');
+    const initColor = window.getComputedStyle(element).getPropertyValue('background-color');
+    element.style.backgroundColor = color;
+    setTimeout(() => {
+        element.classList.add('selectFade');
+        element.style.backgroundColor = initColor;
+    }, 10);
+    setTimeout(() => {
+        element.classList.remove('selectFade');
+        element.style.backgroundColor = '';
+    }, 3000);
+}
\ No newline at end of file
similarity index 89%
rename from resources/assets/js/vues/code-editor.js
rename to resources/js/vues/code-editor.js
index d6f9965a879f0547497d3f046b8056ba5c83ab06..c6df6b1a5de43aafc902fe45fc4a454f2a3ee2dd 100644 (file)
@@ -3,10 +3,10 @@ import codeLib from "../services/code";
 const methods = {
     show() {
         if (!this.editor) this.editor = codeLib.popupEditor(this.$refs.editor, this.language);
-        this.$refs.overlay.style.display = 'flex';
+        this.$refs.overlay.components.overlay.show();
     },
     hide() {
-        this.$refs.overlay.style.display = 'none';
+        this.$refs.overlay.components.overlay.hide();
     },
     updateEditorMode(language) {
         codeLib.setMode(this.editor, language);
similarity index 84%
rename from resources/assets/js/vues/components/autosuggest.js
rename to resources/js/vues/components/autosuggest.js
index 4fe183f0295cbcddaa70416fb9cf90b9880d2bb9..b809313cb641cc0b723e334f510542fb045975a2 100644 (file)
@@ -6,6 +6,7 @@ const template = `
             @input="inputUpdate($event.target.value)" @focus="inputUpdate($event.target.value)"
             @blur="inputBlur"
             @keydown="inputKeydown"
+            :aria-label="placeholder"
         />
         <ul class="suggestion-box" v-if="showSuggestions">
             <li v-for="(suggestion, i) in suggestions"
@@ -66,23 +67,23 @@ const methods = {
     },
 
     inputKeydown(event) {
-        if (event.keyCode === 13) event.preventDefault();
+        if (event.key === 'Enter') event.preventDefault();
         if (!this.showSuggestions) return;
 
         // Down arrow
-        if (event.keyCode === 40) {
+        if (event.key === 'ArrowDown') {
             this.active = (this.active === this.suggestions.length - 1) ? 0 : this.active+1;
         }
         // Up Arrow
-        else if (event.keyCode === 38) {
+        else if (event.key === 'ArrowUp') {
             this.active = (this.active === 0) ? this.suggestions.length - 1 : this.active-1;
         }
-        // Enter or tab keys
-        else if ((event.keyCode === 13 || event.keyCode === 9) && !event.shiftKey) {
+        // Enter key
+        else if ((event.key === 'Enter') && !event.shiftKey) {
             this.selectSuggestion(this.suggestions[this.active]);
         }
         // Escape key
-        else if (event.keyCode === 27) {
+        else if (event.key === 'Escape') {
             this.showSuggestions = false;
         }
     },
@@ -113,11 +114,13 @@ const methods = {
      */
     getSuggestions(input, params) {
         params.search = input;
-        let cacheKey = `${this.url}:${JSON.stringify(params)}`;
+        const cacheKey = `${this.url}:${JSON.stringify(params)}`;
 
-        if (typeof ajaxCache[cacheKey] !== "undefined") return Promise.resolve(ajaxCache[cacheKey]);
+        if (typeof ajaxCache[cacheKey] !== "undefined") {
+            return Promise.resolve(ajaxCache[cacheKey]);
+        }
 
-        return this.$http.get(this.url, {params}).then(resp => {
+        return this.$http.get(this.url, params).then(resp => {
             ajaxCache[cacheKey] = resp.data;
             return resp.data;
         });
similarity index 83%
rename from resources/assets/js/vues/components/dropzone.js
rename to resources/js/vues/components/dropzone.js
index 9d3d22b4dd2fdf7668299a1c65a1799db0e46366..1c045727f842686ebbcb690ced96307c56b1017f 100644 (file)
@@ -1,14 +1,14 @@
 import DropZone from "dropzone";
+import { fadeOut } from "../../services/animations";
 
 const template = `
-    <div class="dropzone-container">
-        <div class="dz-message">{{placeholder}}</div>
+    <div class="dropzone-container text-center">
+        <button type="button" class="dz-message">{{placeholder}}</button>
     </div>
 `;
 
 const props = ['placeholder', 'uploadUrl', 'uploadedTo'];
 
-// TODO - Remove jQuery usage
 function mounted() {
    const container = this.$el;
    const _this = this;
@@ -37,7 +37,7 @@ function mounted() {
 
             dz.on('success', function (file, data) {
                 _this.$emit('success', {file, data});
-                $(file.previewElement).fadeOut(400, function () {
+                fadeOut(file.previewElement, 800, () => {
                     dz.removeFile(file);
                 });
             });
@@ -46,7 +46,8 @@ function mounted() {
                 _this.$emit('error', {file, errorMessage, xhr});
 
                 function setMessage(message) {
-                    $(file.previewElement).find('[data-dz-errormessage]').text(message);
+                    const messsageEl = file.previewElement.querySelector('[data-dz-errormessage]');
+                    messsageEl.textContent = message;
                 }
 
                 if (xhr && xhr.status === 413) {
similarity index 98%
rename from resources/assets/js/vues/image-manager.js
rename to resources/js/vues/image-manager.js
index dd1d9d17adcd4be1aebb2addf9095d1cb1f0f861..6df12d16d3a8c128a0d4205c7b8ee14eee786e1b 100644 (file)
@@ -57,14 +57,14 @@ const methods = {
     },
 
     async fetchData() {
-        let query = {
+        const params = {
             page,
             search: this.searching ? this.searchTerm : null,
             uploaded_to: this.uploadedTo || null,
             filter_type: this.filter,
         };
 
-        const {data} = await this.$http.get(baseUrl, {params: query});
+        const {data} = await this.$http.get(baseUrl, params);
         this.images = this.images.concat(data.images);
         this.hasMore = data.has_more;
         page++;
similarity index 92%
rename from resources/assets/js/vues/page-editor.js
rename to resources/js/vues/page-editor.js
index 864a3a9064912b203658959391b7ca16534c6efb..fbf2857a428fe42058c304737e4a7327c17f56ea 100644 (file)
@@ -69,8 +69,8 @@ let methods = {
         autoSave = window.setInterval(() => {
             // Return if manually saved recently to prevent bombarding the server
             if (Date.now() - lastSave < (1000 * autoSaveFrequency)/2) return;
-            let newTitle = document.getElementById('name').value.trim();
-            let newHtml = this.editorHTML;
+            const newTitle = document.getElementById('name').value.trim();
+            const newHtml = this.editorHTML;
 
             if (newTitle !== currentContent.title || newHtml !== currentContent.html) {
                 currentContent.html = newHtml;
@@ -84,18 +84,18 @@ let methods = {
     saveDraft() {
         if (!this.draftsEnabled) return;
 
-        let data = {
+        const data = {
             name: document.getElementById('name').value.trim(),
             html: this.editorHTML
         };
 
         if (this.editorType === 'markdown') data.markdown = this.editorMarkdown;
 
-        let url = window.baseUrl(`/ajax/page/${this.pageId}/save-draft`);
+        const url = window.baseUrl(`/ajax/page/${this.pageId}/save-draft`);
         window.$http.put(url, data).then(response => {
             draftErroring = false;
             if (!this.isNewDraft) this.isUpdateDraft = true;
-            this.draftNotifyChange(`${response.data.message } ${Dates.utcTimeStampToLocalTime(response.data.timestamp)}`);
+            this.draftNotifyChange(`${response.data.message} ${Dates.utcTimeStampToLocalTime(response.data.timestamp)}`);
             lastSave = Date.now();
         }, errorRes => {
             if (draftErroring) return;
similarity index 98%
rename from resources/assets/js/vues/tag-manager.js
rename to resources/js/vues/tag-manager.js
index e0dab595ae79502d17655634691e210a799cca68..65233cbb676636bd7756ade1cd48b026d0572835 100644 (file)
@@ -1,7 +1,7 @@
 import draggable from 'vuedraggable';
 import autosuggest from './components/autosuggest';
 
-let data = {
+const data = {
     entityId: false,
     entityType: null,
     tags: [],
@@ -10,7 +10,7 @@ let data = {
 const components = {draggable, autosuggest};
 const directives = {};
 
-let methods = {
+const methods = {
 
     addEmptyTag() {
         this.tags.push({name: '', value: '', key: Math.random().toString(36).substring(7)});
index bad0910a28f20592d5fb836ab83f4f67cf4852f1..8c822a5a79fa9dc212c792837f37b99050941b0d 100644 (file)
@@ -27,7 +27,7 @@ return [
     'email' => 'البريد الإلكتروني',
     'password' => 'كلمة المرور',
     'password_confirm' => 'تأكيد كلمة المرور',
-    'password_hint' => 'يجب أن تكون أكثر من 5 حروف',
+    'password_hint' => 'يجب أن تكون أكثر من 7 حروف',
     'forgot_password' => 'نسيت كلمة المرور؟',
     'remember_me' => 'تذكرني',
     'ldap_email_hint' => 'الرجاء إدخال عنوان بريد إلكتروني لاستخدامه مع الحساب.',
index 69d6f0b97946dd4c397d98bdabf25a6b9a4320b4..27cb33880de76d4997ed620272c4d4223738dbea 100644 (file)
@@ -21,7 +21,7 @@ return [
     'email' => 'Email',
     'password' => 'Heslo',
     'password_confirm' => 'Potvrdit heslo',
-    'password_hint' => 'Musí mít víc než 5 znaků',
+    'password_hint' => 'Musí mít víc než 7 znaků',
     'forgot_password' => 'Zapomněli jste heslo?',
     'remember_me' => 'Neodhlašovat',
     'ldap_email_hint' => 'Zadejte email, který chcete přiřadit k tomuto účtu.',
index be2eb54b82a8285177dfa6130e25dc1086fe08b7..35b2c9f8a50a3fd5a04e4384e8977bfa97d7340e 100644 (file)
@@ -17,7 +17,6 @@ return [
     'page_restore'                => 'stellt Seite wieder her',
     'page_restore_notification'   => 'Die Seite wurde erfolgreich wiederhergestellt.',
     'page_move'                   => 'verschiebt Seite',
-    'page_move_notification'      => 'Die Seite wurde erfolgreich verschoben.',
 
     // Chapters
     'chapter_create'              => 'erstellt Kapitel',
index 7b1ebec6e78f65f148e53bfa6336d64525681106..b367fc63b3bf0c07cb6e0da85578f363c1044112 100644 (file)
@@ -25,11 +25,13 @@ return [
     'email' => 'E-Mail',
     'password' => 'Passwort',
     'password_confirm' => 'Passwort best&auml;tigen',
-    'password_hint' => 'Mindestlänge: 5 Zeichen',
+    'password_hint' => 'Mindestlänge: 7 Zeichen',
     'forgot_password' => 'Passwort vergessen?',
     'remember_me' => 'Angemeldet bleiben',
     'ldap_email_hint' => 'Bitte geben Sie eine E-Mail-Adresse ein, um diese mit dem Account zu nutzen.',
     'create_account' => 'Account registrieren',
+    'already_have_account' => 'Bereits ein Konto erstellt?',
+    'dont_have_account' => 'Noch kein Konto erstellt?',
     'social_login' => 'Mit Sozialem Netzwerk anmelden',
     'social_registration' => 'Mit Sozialem Netzwerk registrieren',
     'social_registration_text' => 'Mit einer dieser Dienste registrieren oder anmelden',
index 5579a488aea2fd7371ec69865aceb6d793241c8c..97b48ce4d623849c75b75814af36a20b358596e8 100644 (file)
@@ -10,6 +10,7 @@ return [
     'save' => 'Speichern',
     'continue' => 'Weiter',
     'select' => 'Auswählen',
+    'toggle_all' => 'Alle umschalten',
     'more' => 'Mehr',
 
     /**
@@ -26,6 +27,7 @@ return [
      */
     'actions' => 'Aktionen',
     'view' => 'Anzeigen',
+    'view_all' => 'Alle anzeigen',
     'create' => 'Anlegen',
     'update' => 'Aktualisieren',
     'edit' => 'Bearbeiten',
@@ -40,6 +42,11 @@ return [
     'remove' => 'Entfernen',
     'add' => 'Hinzufügen',
 
+    // Sort Options
+    'sort_name' => 'Name',
+    'sort_created_at' => 'Erstellungsdatum',
+    'sort_updated_at' => 'Aktualisierungsdatum',
+
     /**
      * Misc
      */
@@ -52,6 +59,7 @@ return [
     'details' => 'Details',
     'grid_view' => 'Gitteransicht',
     'list_view' => 'Listenansicht',
+    'default' => 'Voreinstellung',
 
     /**
      * Header
@@ -59,6 +67,10 @@ return [
     'view_profile' => 'Profil ansehen',
     'edit_profile' => 'Profil bearbeiten',
 
+    // Layout tabs
+    'tab_info' => 'Info',
+    'tab_content' => 'Inhalt',
+
     /**
      * Email Content
      */
index 07a92e2c7aac8acc1e6bfe12a9ab66ecc676b6b5..d674195434a3f1143facc2e909db74bd73d112de 100644 (file)
@@ -8,6 +8,7 @@ return [
     'recently_updated_pages' => 'Kürzlich aktualisierte Seiten',
     'recently_created_chapters' => 'Kürzlich angelegte Kapitel',
     'recently_created_books' => 'Kürzlich angelegte Bücher',
+    'recently_created_shelves' => 'Kürzlich angelegte Regale',
     'recently_update' => 'Kürzlich aktualisiert',
     'recently_viewed' => 'Kürzlich angesehen',
     'recent_activity' => 'Kürzliche Aktivität',
@@ -49,28 +50,32 @@ return [
     'search_content_type' => 'Inhaltstyp',
     'search_exact_matches' => 'Exakte Treffer',
     'search_tags' => 'Nach Schlagwort suchen',
+    'search_options' => 'Optionen',
     'search_viewed_by_me' => 'Schon von mir angesehen',
     'search_not_viewed_by_me' => 'Noch nicht von mir angesehen',
     'search_permissions_set' => 'Berechtigungen gesetzt',
     'search_created_by_me' => 'Von mir erstellt',
     'search_updated_by_me' => 'Von mir aktualisiert',
+    'search_date_options' => 'Datums Optionen',
     'search_updated_before' => 'Aktualisiert vor',
     'search_updated_after' => 'Aktualisiert nach',
     'search_created_before' => 'Erstellt vor',
     'search_created_after' => 'Erstellt nach',
     'search_set_date' => 'Datum auswählen',
     'search_update' => 'Suche aktualisieren',
-    
+
     /*
      * Shelves
      */
     'shelf' => 'Regal',
     'shelves' => 'Regale',
+    'x_shelves' => ':count Regal|:count Regale',
     'shelves_long' => 'Bücherregal',
     'shelves_empty' => 'Es wurden noch keine Regale angelegt',
     'shelves_create' => 'Erzeuge ein Regal',
     'shelves_popular' => 'Beliebte Regale',
     'shelves_new' => 'Kürzlich erstellte Regale',
+    'shelves_new_action' => 'Neues Regal',
     'shelves_popular_empty' => 'Die beliebtesten Regale werden hier angezeigt.',
     'shelves_new_empty' => 'Die neusten Regale werden hier angezeigt.',
     'shelves_save' => 'Regal speichern',
@@ -92,7 +97,7 @@ return [
     '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.',
     'shelves_copy_permission_success' => 'Regal-Berechtigungen wurden zu :count Büchern kopiert',
-    
+
     /**
      * Books
      */
@@ -103,6 +108,7 @@ return [
     'books_popular' => 'Beliebte Bücher',
     'books_recent' => 'Kürzlich angesehene Bücher',
     'books_new' => 'Neue Bücher',
+    'books_new_action' => 'Neues Buch',
     'books_popular_empty' => 'Die beliebtesten Bücher werden hier angezeigt.',
     'books_new_empty' => 'Die neusten Bücher werden hier angezeigt.',
     'books_create' => 'Neues Buch erstellen',
@@ -118,7 +124,6 @@ return [
     'books_permissions_updated' => 'Buch-Berechtigungen aktualisiert',
     'books_empty_contents' => 'Es sind noch keine Seiten oder Kapitel zu diesem Buch hinzugefügt worden.',
     'books_empty_create_page' => 'Neue Seite anlegen',
-    'books_empty_or' => 'oder',
     'books_empty_sort_current_book' => 'Aktuelles Buch sortieren',
     'books_empty_add_chapter' => 'Neues Kapitel hinzufügen',
     'books_permissions_active' => 'Buch-Berechtigungen aktiv',
@@ -126,6 +131,11 @@ return [
     'books_navigation' => 'Buchnavigation',
     'books_sort' => 'Buchinhalte sortieren',
     'books_sort_named' => 'Buch ":bookName" sortieren',
+    'books_sort_name' => 'Sortieren nach Namen',
+    'books_sort_created' => 'Sortieren nach Erstellungsdatum',
+    'books_sort_updated' => 'Sortieren nach Aktualisierungsdatum',
+    'books_sort_chapters_first' => 'Kapitel zuerst',
+    'books_sort_chapters_last' => 'Kapitel zuletzt',
     'books_sort_show_other' => 'Andere Bücher anzeigen',
     'books_sort_save' => 'Neue Reihenfolge speichern',
     /**
@@ -232,6 +242,7 @@ return [
     'page_tags' => 'Seiten-Schlagwörter',
     'chapter_tags' => 'Kapitel-Schlagwörter',
     'book_tags' => 'Buch-Schlagwörter',
+    'shelf_tags' => 'Regal-Schlagwörter',
     'tag' => 'Schlagwort',
     'tags' =>  'Schlagwörter',
     'tag_value' => 'Inhalt (Optional)',
@@ -270,6 +281,7 @@ return [
     'profile_not_created_pages' => ':userName hat noch keine Seiten erstellt.',
     'profile_not_created_chapters' => ':userName hat noch keine Kapitel erstellt.',
     'profile_not_created_books' => ':userName hat noch keine Bücher erstellt.',
+    'profile_not_created_shelves' => ':userName hat noch keine Regale erstellt.',
     /**
      * Comments
      */
@@ -294,6 +306,7 @@ return [
      * Revision
      */
     'revision_delete_confirm' => 'Sind Sie sicher, dass Sie diese Revision löschen wollen?',
+    'revision_restore_confirm' => 'Sind Sie sicher, dass Sie diese Revision wiederherstellen wollen? Der aktuelle Seiteninhalt wird ersetzt.',
     'revision_delete_success' => 'Revision gelöscht',
     'revision_cannot_delete_latest' => 'Die letzte Version kann nicht gelöscht werden.'
 ];
index dc95d1d2bf896b14f8a1a3c65c014a45124769e9..362641bc88489194335036961acf97f522c69c93 100644 (file)
@@ -30,6 +30,7 @@ return [
     'cannot_get_image_from_url' => 'Bild konnte nicht von der URL :url geladen werden.',
     'cannot_create_thumbs' => 'Der Server kann keine Vorschau-Bilder erzeugen. Bitte prüfen Sie, ob die GD PHP-Erweiterung installiert ist.',
     'server_upload_limit' => 'Der Server verbietet das Hochladen von Dateien mit dieser Dateigröße. Bitte versuchen Sie es mit einer kleineren Datei.',
+    '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.',
@@ -43,6 +44,7 @@ return [
     // Entities
     'entity_not_found' => 'Eintrag nicht gefunden',
     'book_not_found' => 'Buch nicht gefunden',
+    'bookshelf_not_found' => 'Regal 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.',
@@ -55,6 +57,8 @@ return [
     '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.',
+
     // 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.',
index 0a8d50d051fe2a7a8a9c02af02f872dea3d31be7..11050924ec5159b4d1b33826ca0ef17e32cf1f90 100644 (file)
@@ -11,10 +11,16 @@ return [
     /**
      * App settings
      */
-    'app_settings' => 'Anwendungseinstellungen',
+    'app_customization' => 'Personalisierung',
+    'app_features_security' => 'Funktionen & Sicherheit',
     'app_name' => 'Anwendungsname',
     '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_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?',
     'app_secure_images' => 'Erhöhte Sicherheit für hochgeladene Bilder aktivieren?',
     'app_secure_images_desc' => 'Aus Leistungsgründen sind alle Bilder öffentlich sichtbar. Diese Option fügt zufällige, schwer zu eratene, Zeichenketten zu Bild-URLs hinzu. Stellen sie sicher, dass Verzeichnisindizes deaktiviert sind, um einen einfachen Zugriff zu verhindern.',
@@ -28,22 +34,25 @@ return [
     'app_primary_color_desc' => "Dies sollte ein HEX Wert sein.\nWenn Sie nicht eingeben, wird die Anwendung auf die Standardfarbe zurückgesetzt.",
     'app_homepage' => 'Startseite der Anwendung',
     '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_default' => 'Ausgewählte Startseite',
-    'app_homepage_books' => 'Oder wähle die Buch-Übersicht als Startseite. Das wird die Seiten-Auswahl überschreiben.',
+    'app_homepage_select' => 'Wählen Sie eine Seite aus',
     'app_disable_comments' => 'Kommentare deaktivieren',
+    'app_disable_comments_toggle' => 'Kommentare deaktivieren',
     'app_disable_comments_desc' => 'Deaktiviert Kommentare über alle Seiten in der Anwendung. Vorhandene Kommentare werden nicht angezeigt.',
     /**
      * Registration settings
      */
     'reg_settings' => 'Registrierungseinstellungen',
-    'reg_allow' => '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',
-    'reg_confirm_email' => 'Bestätigung per E-Mail erforderlich?',
+    'reg_email_confirmation' => 'Bestätigung per E-Mail',
+    'reg_email_confirmation_toggle' => 'Bestätigung per E-Mail erforderlich',
     'reg_confirm_email_desc' => 'Falls die Einschränkung für Domains genutzt wird, ist die Bestätigung per E-Mail zwingend erforderlich und der untenstehende Wert wird ignoriert.',
     'reg_confirm_restrict_domain' => 'Registrierung auf bestimmte Domains einschränken',
     'reg_confirm_restrict_domain_desc' => "Fügen sie eine durch Komma getrennte Liste von Domains hinzu, auf die die Registrierung eingeschränkt werden soll. Benutzern wird eine E-Mail gesendet, um ihre E-Mail Adresse zu bestätigen, bevor sie diese Anwendung nutzen können.\nHinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung ändern.",
     'reg_confirm_restrict_domain_placeholder' => 'Keine Einschränkung gesetzt',
-    
+
     /**
      * Maintenance settings
      */
@@ -74,6 +83,7 @@ return [
     'role_details' => 'Rollendetails',
     'role_name' => 'Rollenname',
     'role_desc' => 'Kurzbeschreibung der Rolle',
+    'role_external_auth_id' => 'Externe Authentifizierungs-IDs',
     'role_system' => 'System-Berechtigungen',
     'role_manage_users' => 'Benutzer verwalten',
     'role_manage_roles' => 'Rollen und Rollen-Berechtigungen verwalten',
@@ -82,6 +92,7 @@ return [
     'role_manage_settings' => 'Globaleinstellungen verwalten',
     'role_asset' => 'Berechtigungen',
     '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.',
+    'role_asset_admins' => 'Administratoren erhalten automatisch Zugriff auf alle Inhalte, aber diese Optionen können Oberflächenoptionen ein- oder ausblenden.',
     'role_all' => 'Alle',
     'role_own' => 'Eigene',
     'role_controlled_by_asset' => 'Berechtigungen werden vom Uploadziel bestimmt',
@@ -96,8 +107,15 @@ return [
     'user_profile' => 'Benutzerprofil',
     'users_add_new' => 'Benutzer hinzufügen',
     'users_search' => 'Benutzer suchen',
+    'users_details' => 'Benutzerdetails',
+    'users_details_desc' => 'Legen Sie für diesen Benutzer einen Anzeigenamen und eine E-Mail-Adresse fest. Die E-Mail-Adresse wird bei der Anmeldung verwendet.',
+    'users_details_desc_no_email' => 'Legen Sie für diesen Benutzer einen Anzeigenamen fest, damit andere ihn erkennen können.',
     'users_role' => 'Benutzerrollen',
+    'users_role_desc' => 'Wählen Sie aus, welchen Rollen dieser Benutzer zugeordnet werden soll. Wenn ein Benutzer mehreren Rollen zugeordnet ist, werden die Berechtigungen dieser Rollen gestapelt und er erhält alle Fähigkeiten der zugewiesenen Rollen.',
+    'users_password' => 'Benutzerpasswort',
+    'users_password_desc' => 'Legen Sie ein Passwort fest, mit dem Sie sich anmelden möchten. Diese muss mindestens 5 Zeichen lang sein.',
     'users_external_auth_id' => 'Externe Authentifizierungs-ID',
+    'users_external_auth_id_desc' => 'Dies ist die ID, die verwendet wird, um diesen Benutzer bei der Kommunikation mit Ihrem LDAP-System abzugleichen.',
     'users_password_warning' => 'Füllen Sie die folgenden Felder nur aus, wenn Sie Ihr Passwort ändern möchten:',
     'users_system_public' => 'Dieser Benutzer repräsentiert alle unangemeldeten Benutzer, die diese Seite betrachten. Er kann nicht zum Anmelden benutzt werden, sondern wird automatisch zugeordnet.',
     'users_delete' => 'Benutzer löschen',
@@ -111,6 +129,7 @@ return [
     'users_avatar' => 'Benutzer-Bild',
     'users_avatar_desc' => 'Das Bild sollte eine Auflösung von 256x256px haben.',
     'users_preferred_language' => 'Bevorzugte Sprache',
+    'users_preferred_language_desc' => 'Diese Option ändert die Sprache, die für die Benutzeroberfläche der Anwendung verwendet wird. Dies hat keinen Einfluss auf von Benutzern erstellte Inhalte.',
     'users_social_accounts' => 'Social-Media Konten',
     'users_social_accounts_info' => 'Hier können Sie andere Social-Media-Konten für eine schnellere und einfachere Anmeldung verknüpfen. Wenn Sie ein Social-Media Konto lösen, bleibt der Zugriff erhalten. Entfernen Sie in diesem Falle die Berechtigung in Ihren Profil-Einstellungen des verknüpften Social-Media-Kontos.',
     'users_social_connect' => 'Social-Media-Konto verknüpfen',
index 5ac4b1b2735061345bc12a3df4168ba42589b137..84faeebb770cca2ec98c1fba560f184bc17d726f 100644 (file)
@@ -38,6 +38,7 @@ return [
     'filled'               => ':attribute ist erforderlich.',
     'exists'               => ':attribute ist ungültig.',
     'image'                => ':attribute muss ein Bild sein.',
+    'image_extension'      => ':attribute muss eine gültige und unterstützte Bild-Dateiendung haben.',
     'in'                   => ':attribute ist ungültig.',
     'integer'              => ':attribute muss eine Zahl sein.',
     'ip'                   => ':attribute muss eine valide IP-Adresse sein.',
@@ -54,6 +55,7 @@ return [
         'string'  => ':attribute muss mindestens :min Zeichen lang sein.',
         'array'   => ':attribute muss mindesten :min Elemente enthalten.',
     ],
+    'no_double_extension'  => ':attribute darf nur eine gültige Dateiendung',
     'not_in'               => ':attribute ist ungültig.',
     'numeric'              => ':attribute muss eine Zahl sein.',
     'regex'                => ':attribute ist in einem ungültigen Format.',
@@ -74,6 +76,7 @@ return [
     'timezone'             => ':attribute muss eine valide zeitzone sein.',
     '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.',
 
     /*
     |--------------------------------------------------------------------------
@@ -90,6 +93,9 @@ return [
         'attribute-name' => [
             'rule-name' => 'custom-message',
         ],
+        'password-confirm' => [
+            'required_with' => 'Passwortbestätigung erforderlich',
+        ],
     ],
 
     /*
index 1065945c0b0a55b89b70103fa2cfe554847aa65c..6961e049b3dc2ad15d25d2a1369f4dc6988e2a6b 100644 (file)
@@ -21,7 +21,7 @@ return [
     'email' => 'Email',
     'password' => 'Password',
     'password_confirm' => 'Confirm Password',
-    'password_hint' => 'Must be over 5 characters',
+    'password_hint' => 'Must be over 7 characters',
     'forgot_password' => 'Forgot Password?',
     'remember_me' => 'Remember Me',
     'ldap_email_hint' => 'Please enter an email to use for this account.',
@@ -64,4 +64,14 @@ return [
     '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',
+
+    // 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!'
 ];
\ No newline at end of file
index ed880afcf9ab24d0bdd0f30dcf6ae8f5b3de7394..1807217a375d2f0a77f5fe10ea80113ee57ea490 100644 (file)
@@ -40,6 +40,10 @@ return [
     'add' => 'Add',
 
     // Sort Options
+    'sort_options' => 'Sort Options',
+    'sort_direction_toggle' => 'Sort Direction Toggle',
+    'sort_ascending' => 'Sort Ascending',
+    'sort_descending' => 'Sort Descending',
     'sort_name' => 'Name',
     'sort_created_at' => 'Created Date',
     'sort_updated_at' => 'Updated Date',
@@ -55,8 +59,10 @@ return [
     'grid_view' => 'Grid View',
     'list_view' => 'List View',
     'default' => 'Default',
+    'breadcrumb' => 'Breadcrumb',
 
     // Header
+    'profile_menu' => 'Profile Menu',
     'view_profile' => 'View Profile',
     'edit_profile' => 'Edit Profile',
 
index abcd2cf2327efb66683f16222ece0fa4b94a5cbe..6bbc723b0abfc1e6b1f69e270bbb9d2afdbe851b 100644 (file)
@@ -176,7 +176,7 @@ return [
     'pages_delete_confirm' => 'Are you sure you want to delete this page?',
     'pages_delete_draft_confirm' => 'Are you sure you want to delete this draft page?',
     'pages_editing_named' => 'Editing Page :pageName',
-    'pages_edit_toggle_header' => 'Toggle header',
+    'pages_edit_draft_options' => 'Draft Options',
     'pages_edit_save_draft' => 'Save Draft',
     'pages_edit_draft' => 'Edit Page Draft',
     'pages_editing_draft' => 'Editing Draft',
@@ -234,6 +234,7 @@ return [
     ],
     'pages_draft_discarded' => 'Draft discarded, The editor has been updated with the current page content',
     'pages_specific' => 'Specific Page',
+    'pages_is_template' => 'Page Template',
 
     // Editor Sidebar
     'page_tags' => 'Page Tags',
@@ -242,9 +243,11 @@ return [
     'shelf_tags' => 'Shelf Tags',
     'tag' => 'Tag',
     'tags' =>  'Tags',
+    'tag_name' =>  'Tag Name',
     'tag_value' => 'Tag Value (Optional)',
     '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' => 'Add another tag',
+    'tags_remove' => 'Remove this tag',
     'attachments' => 'Attachments',
     'attachments_explain' => 'Upload some files or attach some links to display on your page. These are visible in the page sidebar.',
     'attachments_explain_instant_save' => 'Changes here are saved instantly.',
@@ -270,6 +273,12 @@ return [
     'attachments_file_uploaded' => 'File successfully uploaded',
     'attachments_file_updated' => 'File successfully updated',
     'attachments_link_attached' => 'Link successfully attached to page',
+    'templates' => 'Templates',
+    'templates_set_as_template' => 'Page is a template',
+    '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',
+    'templates_prepend_content' => 'Prepend to page content',
 
     // Profile View
     'profile_user_for_x' => 'User for :time',
index b91a0c3e11c96cfdc6f85d4f9940c472476610b2..c3b47744d31f5fc63081ec6d50136511721941c1 100644 (file)
@@ -27,6 +27,7 @@ return [
     '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.',
+    'invite_token_expired' => 'This invitation link has expired. You can instead try to reset your account password.',
 
     // System
     'path_not_writable' => 'File path :filePath could not be uploaded to. Ensure it is writable to the server.',
index 9f7d9e3cbcaccaaa5631115f4d387785b196c18c..f41ca7868f66f2937e8ad49e7e2daee1eff3c569 100644 (file)
@@ -6,7 +6,7 @@
  */
 return [
 
-    'password' => 'Passwords must be at least six characters and match the confirmation.',
+    'password' => 'Passwords must be at least eight characters and match the confirmation.',
     'user' => "We can't find a user with that e-mail address.",
     'token' => 'This password reset token is invalid.',
     'sent' => 'We have e-mailed your password reset link!',
index e6c24f5a14f57a598d8edd48370981c5ad51b471..026308a3451603809d7487bbf2a3bd6734a5ff92 100755 (executable)
@@ -29,6 +29,7 @@ return [
     'app_editor_desc' => 'Select which editor will be used by all users to edit pages.',
     'app_custom_html' => 'Custom HTML Head Content',
     'app_custom_html_desc' => 'Any content added here will be inserted into the bottom of the <head> section of every page. This is handy for overriding styles or adding analytics code.',
+    'app_custom_html_disabled_notice' => 'Custom HTML head content is disabled on this settings page to ensure any breaking changes can be reverted.',
     'app_logo' => 'Application Logo',
     'app_logo_desc' => 'This image should be 43px in height. <br>Large images will be scaled down.',
     'app_primary_color' => 'Application Primary Color',
@@ -84,6 +85,7 @@ return [
     'role_manage_roles' => 'Manage roles & role permissions',
     'role_manage_entity_permissions' => 'Manage all book, chapter & page permissions',
     'role_manage_own_entity_permissions' => 'Manage permissions on own book, chapter & pages',
+    'role_manage_page_templates' => 'Manage page templates',
     'role_manage_settings' => 'Manage app settings',
     'role_asset' => 'Asset Permissions',
     'role_asset_desc' => 'These permissions control default access to the assets within the system. Permissions on Books, Chapters and Pages will override these permissions.',
@@ -107,7 +109,9 @@ return [
     'users_role' => 'User Roles',
     'users_role_desc' => 'Select which roles this user will be assigned to. If a user is assigned to multiple roles the permissions from those roles will stack and they will receive all abilities of the assigned roles.',
     'users_password' => 'User Password',
-    'users_password_desc' => 'Set a password used to log-in to the application. This must be at least 5 characters long.',
+    'users_password_desc' => 'Set a password used to log-in to the application. This must be at least 6 characters long.',
+    'users_send_invite_text' => 'You can choose to send this user an invitation email which allows them to set their own password otherwise you can set their password yourself.',
+    'users_send_invite_option' => 'Send user invite email',
     'users_external_auth_id' => 'External Authentication ID',
     'users_external_auth_id_desc' => 'This is the ID used to match this user when communicating with your LDAP system.',
     'users_password_warning' => 'Only fill the below if you would like to change your password.',
@@ -125,7 +129,7 @@ return [
     'users_preferred_language' => 'Preferred Language',
     'users_preferred_language_desc' => 'This option will change the language used for the user-interface of the application. This will not affect any user-created content.',
     'users_social_accounts' => 'Social Accounts',
-    'users_social_accounts_info' => 'Here you can connect your other accounts for quicker and easier login. Disconnecting an account here does not previously authorized access. Revoke access from your profile settings on the connected social account.',
+    'users_social_accounts_info' => 'Here you can connect your other accounts for quicker and easier login. Disconnecting an account here does not revoke previously authorized access. Revoke access from your profile settings on the connected social account.',
     'users_social_connect' => 'Connect Account',
     'users_social_disconnect' => 'Disconnect Account',
     'users_social_connected' => ':socialAccount account was successfully attached to your profile.',
@@ -155,7 +159,9 @@ return [
         'ru' => 'Русский',
         'uk' => 'Українська',
         'zh_CN' => '简体中文',
-        'zh_TW' => '繁體中文'
+        'zh_TW' => '繁體中文',
+        'hu' => 'Magyar',
+        'tr' => 'Türkçe',
     ]
     //!////////////////////////////////
 ];
index 210980ac2e172244dbcb9a52016afff7fcb5ef8d..76b57a2a3b58ddb8ef41e0562c5187359cc6e542 100644 (file)
@@ -12,7 +12,7 @@ return [
     'active_url'           => 'The :attribute is not a valid URL.',
     'after'                => 'The :attribute must be a date after :date.',
     'alpha'                => 'The :attribute may only contain letters.',
-    'alpha_dash'           => 'The :attribute may only contain letters, numbers, and dashes.',
+    'alpha_dash'           => 'The :attribute may only contain letters, numbers, dashes and underscores.',
     'alpha_num'            => 'The :attribute may only contain letters and numbers.',
     'array'                => 'The :attribute must be an array.',
     'before'               => 'The :attribute must be a date before :date.',
@@ -30,13 +30,41 @@ return [
     'digits'               => 'The :attribute must be :digits digits.',
     'digits_between'       => 'The :attribute must be between :min and :max digits.',
     'email'                => 'The :attribute must be a valid email address.',
+    'ends_with' => 'The :attribute must end with one of the following: :values',
     'filled'               => 'The :attribute field is required.',
+    'gt'                   => [
+        'numeric' => 'The :attribute must be greater than :value.',
+        'file'    => 'The :attribute must be greater than :value kilobytes.',
+        'string'  => 'The :attribute must be greater than :value characters.',
+        'array'   => 'The :attribute must have more than :value items.',
+    ],
+    'gte'                  => [
+        'numeric' => 'The :attribute must be greater than or equal :value.',
+        'file'    => 'The :attribute must be greater than or equal :value kilobytes.',
+        'string'  => 'The :attribute must be greater than or equal :value characters.',
+        'array'   => 'The :attribute must have :value items or more.',
+    ],
     'exists'               => 'The selected :attribute is invalid.',
     'image'                => 'The :attribute must be an image.',
     'image_extension'      => 'The :attribute must have a valid & supported image extension.',
     'in'                   => 'The selected :attribute is invalid.',
     'integer'              => 'The :attribute must be an integer.',
     'ip'                   => 'The :attribute must be a valid IP address.',
+    'ipv4'                 => 'The :attribute must be a valid IPv4 address.',
+    'ipv6'                 => 'The :attribute must be a valid IPv6 address.',
+    'json'                 => 'The :attribute must be a valid JSON string.',
+    'lt'                   => [
+        'numeric' => 'The :attribute must be less than :value.',
+        'file'    => 'The :attribute must be less than :value kilobytes.',
+        'string'  => 'The :attribute must be less than :value characters.',
+        'array'   => 'The :attribute must have less than :value items.',
+    ],
+    'lte'                  => [
+        'numeric' => 'The :attribute must be less than or equal :value.',
+        'file'    => 'The :attribute must be less than or equal :value kilobytes.',
+        'string'  => 'The :attribute must be less than or equal :value characters.',
+        'array'   => 'The :attribute must not have more than :value items.',
+    ],
     'max'                  => [
         'numeric' => 'The :attribute may not be greater than :max.',
         'file'    => 'The :attribute may not be greater than :max kilobytes.',
@@ -52,6 +80,7 @@ return [
     ],
     'no_double_extension'  => 'The :attribute must only have a single file extension.',
     'not_in'               => 'The selected :attribute is invalid.',
+    'not_regex'            => 'The :attribute format is invalid.',
     'numeric'              => 'The :attribute must be a number.',
     'regex'                => 'The :attribute format is invalid.',
     'required'             => 'The :attribute field is required.',
index c93751a10ab7bd9749606cfc82e40181a0b91e0a..3636507fee72669b19fcc12aa023e1eaad476e22 100644 (file)
@@ -21,7 +21,7 @@ return [
     'email' => 'Correo electrónico',
     'password' => 'Contraseña',
     'password_confirm' => 'Confirmar Contraseña',
-    'password_hint' => 'Debe contener más de 5 caracteres',
+    'password_hint' => 'Debe contener más de 7 caracteres',
     'forgot_password' => '¿Contraseña Olvidada?',
     'remember_me' => 'Recordarme',
     'ldap_email_hint' => 'Por favor introduzca un mail para utilizar con esta cuenta.',
@@ -64,4 +64,14 @@ return [
     'email_not_confirmed_click_link' => 'Por favor siga el enlace en el correo electrónico que ha sido enviado durante el proceso de registro.',
     'email_not_confirmed_resend' => 'Si no puede encontrar el correo electrónico, puede solicitar el renvío del correo electrónico de confirmación rellenando el formulario que se muestra a continuación.',
     'email_not_confirmed_resend_button' => 'Reenviar Correo Electrónico de confirmación',
+
+    // User Invite
+    'user_invite_email_subject' => 'As sido invitado a unirte a :appName!',
+    'user_invite_email_greeting' => 'Se ha creado una cuenta para usted en :appName.',
+    'user_invite_email_text' => 'Clica en el botón a continuación para ajustar una contraseña y poder acceder:',
+    'user_invite_email_action' => 'Ajustar la Contraseña de la Cuenta',
+    '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!'
 ];
\ No newline at end of file
index 594a8cf00bbdb2a6fdc65ad9f0ee1457b4188651..3c9cfa69b8f0969c90667ae2da7857c907c7f304 100644 (file)
@@ -40,6 +40,10 @@ return [
     'add' => 'Añadir',
 
     // Sort Options
+    'sort_options' => 'Opciones de ordenación',
+    'sort_direction_toggle' => 'Cambiar el Orden',
+    'sort_ascending' => 'Ordenar Ascendentemente',
+    'sort_descending' => 'Ordenar Descendentemente',
     'sort_name' => 'Nombre',
     'sort_created_at' => 'Fecha de Creación',
     'sort_updated_at' => 'Fecha de Modificación',
@@ -55,8 +59,10 @@ return [
     'grid_view' => 'Vista en Cuadrícula',
     'list_view' => 'Vista en Lista',
     'default' => 'Predeterminada',
+    'breadcrumb' => 'Rastro de migas de pan',
 
     // Header
+    'profile_menu' => 'Menú de Perfil',
     'view_profile' => 'Ver Perfil',
     'edit_profile' => 'Editar Perfil',
 
index 09b318eaa1cf591c62e623981f394165be47147a..db87efda7eeb7655ea90f9ad4d05f12fe784074a 100644 (file)
@@ -176,7 +176,7 @@ return [
     'pages_delete_confirm' => '¿Está seguro de borrar esta página?',
     'pages_delete_draft_confirm' => '¿Está seguro de que desea borrar este borrador de página?',
     'pages_editing_named' => 'Editando página :pageName',
-    'pages_edit_toggle_header' => 'Toggle Título',
+    'pages_edit_draft_options' => 'Opciones de Borrador',
     'pages_edit_save_draft' => 'Guardar borrador',
     'pages_edit_draft' => 'Editar borrador de página',
     'pages_editing_draft' => 'Editando borrador',
@@ -234,6 +234,7 @@ return [
     ],
     'pages_draft_discarded' => 'Borrador descartado, el editor ha sido actualizado con el contenido de la página actual',
     'pages_specific' => 'Página específica',
+    'pages_is_template' => 'Página es plantilla',
 
     // Editor Sidebar
     'page_tags' => 'Etiquetas de Página',
@@ -242,9 +243,11 @@ return [
     'shelf_tags' => 'Etiquetas de Estante',
     'tag' => 'Etiqueta',
     'tags' =>  'Etiquetas',
+    'tag_name' =>  'Nombre de la Etiqueta',
     'tag_value' => 'Valor de la etiqueta (Opcional)',
     'tags_explain' => "Agrege algunas etiquetas para mejorar la categorización de su contenido. \n Puede asignar un valor a una etiqueta para una organización a mayor detalle.",
     'tags_add' => 'Agregar otra etiqueta',
+    'tags_remove' => 'Eliminar esta etiqueta',
     'attachments' => 'Adjuntos',
     'attachments_explain' => 'Subir ficheros o agregar enlaces para mostrar en la página. Estos son visibles en la barra lateral de la página.',
     'attachments_explain_instant_save' => 'Los cambios son guardados de manera instantánea .',
@@ -269,7 +272,13 @@ return [
     'attachments_deleted' => 'Adjunto borrado',
     'attachments_file_uploaded' => 'Fichero subido éxitosamente',
     'attachments_file_updated' => 'Fichero actualizado éxitosamente',
-    'attachments_link_attached' => 'Enlace agregado éxitosamente a la ágina',
+    'attachments_link_attached' => 'Enlace agregado éxitosamente a la página',
+    'templates' => 'Plantillas',
+    'templates_set_as_template' => 'La página es una plantilla',
+    'templates_explain_set_as_template' => 'Puede ajustar esta página como una plantilla, así su contenido puede emplearse al crear una nueva página. Otros usuarios podrán utilizar esta plantilla si tienen permisos de lectura sobre esta página.',
+    'templates_replace_content' => 'Reemplazar el contenido de la página',
+    'templates_append_content' => 'Añadir después del contenido de la página',
+    'templates_prepend_content' => 'Añadir antes del contenido de la página',
 
     // Profile View
     'profile_user_for_x' => 'Usuario para :time',
index 51834f18dfa2abf02290ca037abaf20e531574a4..77d5520f4868208e282bb70cbe2a93e83c513b9e 100644 (file)
@@ -27,6 +27,7 @@ return [
     'social_account_register_instructions' => 'Si no dispone de una cuenta, puede registrar una cuenta usando la opción de :socialAccount .',
     'social_driver_not_found' => 'Driver social no encontrado',
     'social_driver_not_configured' => 'Su configuración :socialAccount no es correcta.',
+    'invite_token_expired' => 'Este enlace de invitación ha expirado. Puede resetear la contraseña de su cuenta como alternativa.',
 
     // System
     'path_not_writable' => 'El fichero no pudo ser subido a la ruta :filePath . Asegúrese de que es escribible por el servidor.',
index 83fe73562a2fd9f41cffed4a66672dad0f06a3f5..f063328f1758d2bcb7c6f6de5f50f1756c8dcb44 100644 (file)
@@ -29,6 +29,7 @@ return [
     'app_editor_desc' => 'Seleccione qué editor se usará por todos los usuarios para editar páginas.',
     'app_custom_html' => 'Contenido de cabecera HTML personalizado',
     'app_custom_html_desc' => 'Cualquier contenido agregado aquí será insertado al final de la sección <head> de cada página. Esto es útil para sobreescribir estilos o agregar código para analíticas web.',
+    'app_custom_html_disabled_notice' => 'El contenido personalizado para la cabecera está deshabilitado en esta página de ajustes para permitir que cualquier cambio que rompa la funcionalidad pueda ser revertido.',
     'app_logo' => 'Logo de la Aplicación',
     'app_logo_desc' => 'Esta imagen debería de ser 43px de altura. <br> Las imágenes grandes serán escaladas.',
     'app_primary_color' => 'Color Primario de la Aplicación',
@@ -84,6 +85,7 @@ return [
     'role_manage_roles' => 'Gestionar roles y permisos de roles',
     'role_manage_entity_permissions' => 'Gestionar todos los permisos de libros, capítulos y páginas',
     'role_manage_own_entity_permissions' => 'Gestionar permisos en libros, capítulos y páginas propias',
+    'role_manage_page_templates' => 'Administrar plantillas',
     'role_manage_settings' => 'Gestionar ajustes de la aplicación',
     'role_asset' => 'Permisos de contenido',
     '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.',
@@ -108,6 +110,8 @@ return [
     'users_role_desc' => 'Selecciona los roles a los que será asignado este usuario. Si se asignan varios roles los permisos se acumularán y recibirá todas las habilidades de los roles asignados.',
     'users_password' => 'Contraseña de Usuario',
     'users_password_desc' => 'Ajusta una contraseña que se utilizará para acceder a la aplicación. Debe ser al menos de 5 caracteres de longitud.',
+    'users_send_invite_text' => 'Puede enviar una invitación a este usuario por correo electrónico que le permitirá ajustar su propia contraseña, o puede usted ajustar su contraseña.',
+    'users_send_invite_option' => 'Enviar un correo electrónico de invitación',
     'users_external_auth_id' => 'ID externo de autenticación',
     'users_external_auth_id_desc' => 'Esta es la ID usada para asociar este usuario con LDAP.',
     'users_password_warning' => 'Solo debe rellenar este campo si desea cambiar su contraseña.',
index 50d178202244fb7c17a299cd24b99e9fed1c3657..fd5c8307b2dfb1bff8b3c0e87c3d6ad932042956 100644 (file)
@@ -1,12 +1,13 @@
 <?php
-
+/**
+ * Activity text strings.
+ * Is used for all the text within activity logs & notifications.
+ * 
+ * Cadenas de texto de Actividades
+ * Se usa para todos los textos dentro de los registros de actividad y notificaciones.
+ */
 return [
 
-    /**
-     * Activity text strings.
-     * Is used for all the text within activity logs & notifications.
-     */
-
     // Pages
     'page_create'                 => 'página creada',
     'page_create_notification'    => 'Página creada exitosamente',
index 4899b3d8998b39855552c8d21a6ec3d9b0743347..df1a33dde6aab49cedc075859d7b4770b40bbf9a 100644 (file)
@@ -1,37 +1,36 @@
 <?php
+/**
+ * Authentication Language Lines
+ * The following language lines are used during authentication for various
+ * messages that we need to display to the user.
+ * 
+ * Líneas de lenguaje para autenticación
+ * Las siguientes líneas de lenguaje se utilizan para mostrar mensajes al usuario durante la autenticación.
+ */
 return [
-    /*
-    |--------------------------------------------------------------------------
-    | Authentication Language Lines
-    |--------------------------------------------------------------------------
-    |
-    | The following language lines are used during authentication for various
-    | messages that we need to display to the user. You are free to modify
-    | these language lines according to your application's requirements.
-    |
-    */
+
     'failed' => 'Las credenciales no concuerdan con nuestros registros.',
     'throttle' => 'Demasiados intentos fallidos de conexión. Por favor intente nuevamente en :seconds segundos.',
 
-    /**
-     * Login & Register
-     */
+    // Login & Register - Ingreso y Registro
     'sign_up' => 'Registrarse',
     'log_in' => 'Acceder',
     'log_in_with' => 'Acceder con :socialDriver',
     'sign_up_with' => 'Registrarse con :socialDriver',
-    'logout' => 'Logout',
+    'logout' => 'Salir',
 
     'name' => 'Nombre',
     'username' => 'Nombre de usuario',
     'email' => 'Correo electrónico',
     'password' => 'Contraseña',
     'password_confirm' => 'Confirmar contraseña',
-    'password_hint' => 'Debe contener al menos 5 caracteres',
+    'password_hint' => 'Debe contener al menos 7 caracteres',
     'forgot_password' => '¿Olvidó la contraseña?',
     'remember_me' => 'Recordarme',
     'ldap_email_hint' => 'Por favor introduzca un correo electrónico para utilizar con esta cuenta.',
     'create_account' => 'Crear una cuenta',
+    'already_have_account' => '¿Ya tiene una cuenta?',
+    'dont_have_account' => '¿No tiene una cuenta?',
     'social_login' => 'Acceso con cuenta Social',
     'social_registration' => 'Registro con cuenta Social',
     'social_registration_text' => 'Registrar y entrar utilizando otro servicio.',
@@ -43,9 +42,7 @@ return [
     'register_success' => '¡Gracias por registrarse! Ahora se encuentra registrado y ha accedido a la aplicación.',
 
 
-    /**
-     * Password Reset
-     */
+    // Password Reset - Restablecer Contraseña
     'reset_password' => 'Restablecer la contraseña',
     'reset_password_send_instructions' => 'Introduzca su correo electrónico a continuación y se le enviará un correo electrónico con un enlace para la restauración',
     'reset_password_send_button' => 'Enviar enlace de restauración',
@@ -57,9 +54,7 @@ return [
     'email_reset_not_requested' => 'Si ud. no solicitó un cambio de contraseña, no se requiere ninguna acción.',
 
 
-    /**
-     * Email Confirmation
-     */
+    // Email Confirmation - Confirmación de correo electrónico
     'email_confirm_subject' => 'Confirme su correo electrónico en :appName',
     'email_confirm_greeting' => '¡Gracias por unirse a :appName!',
     'email_confirm_text' => 'Por favor confirme su dirección de correo electrónico presionando en el siguiente botón:',
@@ -73,4 +68,14 @@ return [
     'email_not_confirmed_click_link' => 'Por favor verifique el correo electrónico con el enlace de confirmación que fue enviado luego de registrarse.',
     'email_not_confirmed_resend' => 'Si no puede encontrar el correo electrónico, puede solicitar el renvío del correo electrónico de confirmación rellenando el formulario a continuación.',
     'email_not_confirmed_resend_button' => 'Reenviar correo electrónico de confirmación',
+
+    // User Invite
+    'user_invite_email_subject' => 'Lo invitaron a unirse a :appName!',
+    'user_invite_email_greeting' => 'Se creó una cuenta para usted en :appName.',
+    'user_invite_email_text' => 'Presione el botón de abajo para establecer una contraseña y tener acceso access:',
+    'user_invite_email_action' => 'Establecer la contraseña de la cuenta',
+    '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!'
 ];
index eb74315355ef5d83221544d3c3b3f8447c74663b..4738897ea696ec973ab542b41724a0facac371d9 100644 (file)
@@ -1,31 +1,32 @@
 <?php
+/**
+ * Common elements found throughout many areas of BookStack.
+ * 
+ * Elementos comunes encontrados en varias áreas de BookStack.
+ */
 return [
 
-    /**
-     * Buttons
-     */
+    // Buttons - Botones
     'cancel' => 'Cancelar',
     'confirm' => 'Confirmar',
     'back' => 'Atrás',
     'save' => 'Guardar',
     'continue' => 'Continuar',
     'select' => 'Seleccionar',
+    'toggle_all' => 'Alternar todo',
     'more' => 'Más',
     
-    /**
-     * Form Labels
-     */
+    // Form Labels - Etiquetas de Formularios
     'name' => 'Nombre',
     'description' => 'Descripción',
     'role' => 'Rol',
     'cover_image' => 'Imagen de cubierta',
     'cover_image_description' => 'Esta imagen debe ser de 440x250px aproximadamente.',
 
-    /**
-     * Actions
-     */
+    // Actions - Acciones
     'actions' => 'Acciones',
     'view' => 'Ver',
+    'view_all' => 'Ver todo',
     'create' => 'Crear',
     'update' => 'Actualizar',
     'edit' => 'Editar',
@@ -40,9 +41,16 @@ return [
     'remove' => 'Remover',
     'add' => 'Agregar',
 
-    /**
-     * Misc
-     */
+    // Sort Options - Opciones de filtro
+    'sort_options' => 'Opciones de Orden',
+    'sort_direction_toggle' => 'Cambiar Dirección de Orden',
+    'sort_ascending' => 'Orden Ascendente',
+    'sort_descending' => 'Orden Descendente',
+    'sort_name' => 'Nombre',
+    'sort_created_at' => 'Fecha de creación',
+    'sort_updated_at' => 'Fecha de actualización',
+
+    //Misc
     'deleted_user' => 'Usuario borrado',
     'no_activity' => 'Ninguna actividad para mostrar',
     'no_items' => 'No hay items disponibles',
@@ -53,16 +61,18 @@ return [
     "grid_view" => "Vista de grilla",
     "list_view" => "Vista de lista",
     'default' => 'Por defecto',
+    'breadcrumb' => 'Miga de Pan',
 
-    /**
-     * Header
-     */
+    // Header - Cabecera
+    'profile_menu' => 'Menu del Perfil',
     'view_profile' => 'Ver Perfil',
     'edit_profile' => 'Editar Perfil',
 
-    /**
-     * Email Content
-     */
+    // Layout tabs - Pestañas de visualización
+    'tab_info' => 'Información',
+    'tab_content' => 'Contenido',
+
+    // Email Content - Contenido de correo Electrónico
     'email_action_help' => 'Si está teniendo problemas haga click en el botón ":actionText", copie y pegue la siguiente URL en su navegador web:',
     'email_rights' => 'Todos los derechos reservados',
 ];
index e8d01743be6033b1aa701664b75dc6a08a698bc7..8cb840c06904b91b4b7a929663c04a94d9da10ad 100644 (file)
@@ -1,16 +1,22 @@
 <?php
+/**
+ * Text used for 'Entities' (Document Structure Elements) such as
+ * Books, Shelves, Chapters & Pages
+ * 
+ * Texto utilizado para 'Entities' (Elementos de Estructura de
+ * los Documentos) como Libros, Estantes, Capítulos y Páginas
+ */
 return [
 
-    /**
-     * Shared
-     */
-    'recently_created' => 'Recientemente creado',
-    'recently_created_pages' => 'Páginas recientemente creadas',
-    'recently_updated_pages' => 'Páginas recientemente actualizadas',
-    'recently_created_chapters' => 'Capítulos recientemente creados',
-    'recently_created_books' => 'Libros recientemente creados',
-    'recently_update' => 'Recientemente actualizado',
-    'recently_viewed' => 'Recientemente visto',
+    // Shared - Compartido
+    'recently_created' => 'Creado recientemente',
+    'recently_created_pages' => 'Páginas creadas recientemente',
+    'recently_updated_pages' => 'Páginas actualizadas recientemente',
+    'recently_created_chapters' => 'Capítulos creados recientemente',
+    'recently_created_books' => 'Libros creados recientemente',
+    'recently_created_shelves' => 'Estantes creados recientemente',
+    'recently_update' => 'Actaulizado recientemente',
+    'recently_viewed' => 'Visto recientemente',
     'recent_activity' => 'Actividad reciente',
     'create_now' => 'Crear uno ahora',
     'revisions' => 'Revisiones',
@@ -31,17 +37,13 @@ return [
     'export_pdf' => 'Archivo PDF',
     'export_text' => 'Archivo de texto plano',
 
-    /**
-     * Permissions and restrictions
-     */
+    // Permissions and restrictions - Permisos y Restricciones
     'permissions' => 'Permisos',
     'permissions_intro' => 'una vez habilitado, Estos permisos tendrán prioridad por encima de cualquier permiso establecido.',
     'permissions_enable' => 'Habilitar permisos custom',
     'permissions_save' => 'Guardar permisos',
 
-    /**
-     * Search
-     */
+    // Search - Búsqueda
     'search_results' => 'Buscar resultados',
     'search_total_results_found' => ':count resultados encontrados|:count total de resultados encontrados',
     'search_clear' => 'Limpiar resultados',
@@ -66,16 +68,16 @@ return [
     'search_set_date' => 'Esablecer fecha',
     'search_update' => 'Actualizar búsqueda',
 
-    /**
-     * Shelves
-     */
+    // Shelves - Estantes
     'shelf' => 'Estante',
     'shelves' => 'Estantes',
+    'x_shelves' => ':count Estante|:count Estantes',
     'shelves_long' => 'Estantes de libros',
     'shelves_empty' => 'No se crearon estantes',
     'shelves_create' => 'Crear un estante nuevo',
     'shelves_popular' => 'Estantes Populares',
     'shelves_new' => 'Estantes Nuevos',
+    'shelves_new_action' => 'Estante Nuevo',
     'shelves_popular_empty' => 'Los estantes más populares aparecerán aquí.',
     'shelves_new_empty' => 'Los estantes mas nuevos aparecerán aquí.',
     'shelves_save' => 'Guardar estantes',
@@ -98,9 +100,7 @@ return [
     '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.',
     'shelves_copy_permission_success' => 'Se copiaron los permisos del estante a :count libros',
 
-    /**
-     * Books
-     */
+    // Books - Libros
     'book' => 'Libro',
     'books' => 'Libros',
     'x_books' => ':count Libro|:count Libros',
@@ -108,6 +108,7 @@ return [
     'books_popular' => 'Libros populares',
     'books_recent' => 'Libros recientes',
     'books_new' => 'Libros nuevos',
+    'books_new_action' => 'Libro nuevo',
     'books_popular_empty' => 'Los libros más populares aparecerán aquí.',
     'books_new_empty' => 'Los libros creados más recientemente aparecerán aquí.',
     'books_create' => 'Crear nuevo libro',
@@ -123,7 +124,6 @@ return [
     'books_permissions_updated' => 'Permisos de libro actualizados',
     'books_empty_contents' => 'Ninguna página o capítulo ha sido creada para este libro.',
     'books_empty_create_page' => 'Crear una nueva página',
-    'books_empty_or' => 'ó',
     'books_empty_sort_current_book' => 'Organizar el libro actual',
     'books_empty_add_chapter' => 'Agregar un capítulo',
     'books_permissions_active' => 'Permisos de libro activados',
@@ -131,12 +131,15 @@ return [
     'books_navigation' => 'Navegación de libro',
     'books_sort' => 'Organizar contenido de libro',
     'books_sort_named' => 'Organizar libro :bookName',
+    'books_sort_name' => 'Organizar por nombre',
+    'books_sort_created' => 'Organizar por fecha de creación',
+    'books_sort_updated' => 'Organizar por fecha de actualización',
+    'books_sort_chapters_first' => 'Capítulos primero',
+    'books_sort_chapters_last' => 'Capítulos al final',
     'books_sort_show_other' => 'Mostrar otros libros',
     'books_sort_save' => 'Guardar nuevo orden',
 
-    /**
-     * Chapters
-     */
+    // Chapters - Capítulos
     'chapter' => 'Capítulo',
     'chapters' => 'Capítulos',
     'x_chapters' => ':count Capítulo|:count Capítulos',
@@ -159,9 +162,7 @@ return [
     'chapters_permissions_success' => 'Permisos de capítulo actualizados',
     'chapters_search_this' => 'Buscar en este capítulo',
 
-    /**
-     * Pages
-     */
+    // Pages - Páginas
     'page' => 'Página',
     'pages' => 'Páginas',
     'x_pages' => ':count Página|:count Páginas',
@@ -178,7 +179,7 @@ return [
     'pages_delete_confirm' => '¿Está seguro de borrar esta página?',
     'pages_delete_draft_confirm' => 'Está seguro de que desea borrar este borrador de página?',
     'pages_editing_named' => 'Editando página :pageName',
-    'pages_edit_toggle_header' => 'Alternar cabecera',
+    'pages_edit_draft_options' => 'Opciones de borrador',
     'pages_edit_save_draft' => 'Guardar borrador',
     'pages_edit_draft' => 'Editar borrador de página',
     'pages_editing_draft' => 'Editando borrador',
@@ -212,6 +213,8 @@ return [
     'pages_revisions_created_by' => 'Creado por',
     'pages_revisions_date' => 'Fecha de revisión',
     'pages_revisions_number' => '#',
+    'pages_revisions_numbered' => 'Revisión #:id',
+    'pages_revisions_numbered_changes' => 'Cambios de Revisión #:id',
     'pages_revisions_changelog' => 'Registro de cambios',
     'pages_revisions_changes' => 'Cambios',
     'pages_revisions_current' => 'Versión actual',
@@ -234,20 +237,20 @@ return [
     ],
     'pages_draft_discarded' => 'Borrador descartado, el editor ha sido actualizado con el contenido de la página actual',
     'pages_specific' => 'Página Específica',
+    'pages_is_template' => 'Plantilla de Página',
 
-
-    /**
-     * Editor sidebar
-     */
+    // Editor sidebar - Barra lateral del editor
     'page_tags' => 'Etiquetas de página',
     'chapter_tags' => 'Etiquetas de capítulo',
     'book_tags' => 'Etiquetas de libro',
     'shelf_tags' => 'Shelf Tags',
     'tag' => 'Etiqueta',
     'tags' =>  'Etiquetas',
+    'tag_name' =>  'Nombre de etiqueta',
     'tag_value' => 'Valor de la etiqueta (Opcional)',
     'tags_explain' => "Agregar algunas etiquetas para mejorar la categorización de su contenido. \n Se puede asignar un valor a una etiqueta para una organizacón con mayor detalle.",
     'tags_add' => 'Agregar otra etiqueta',
+    'tags_remove' => 'Eliminar esta etiqueta',
     'attachments' => 'Adjuntos',
     'attachments_explain' => 'Subir archivos o agregar enlaces para mostrar en la página. Estos son visibles en la barra lateral de la página.',
     'attachments_explain_instant_save' => 'Los cambios se guardan de manera instantánea.',
@@ -273,19 +276,23 @@ return [
     'attachments_file_uploaded' => 'Archivo subido exitosamente',
     'attachments_file_updated' => 'Archivo actualizado exitosamente',
     'attachments_link_attached' => 'Enlace agregado exitosamente a la página',
+    'templates' => 'Plantillas',
+    'templates_set_as_template' => 'La Página es una plantilla',
+    'templates_explain_set_as_template' => 'Puede establecer esta página como plantilla para que el contenido pueda utilizarse para al crear otras páginas. Otris usuarios podrán utilizar esta plantilla si tienen permisos para ver de esta página.',
+    'templates_replace_content' => 'Reemplazar el contenido de la página',
+    'templates_append_content' => 'Incorporar al fina del contenido de la página',
+    'templates_prepend_content' => 'Incorporar al principio del contenido de la página',
 
-    /**
-     * Profile View
-     */
+    // Profile View - Vista de Perfil
     'profile_user_for_x' => 'Usuario para :time',
     'profile_created_content' => 'Contenido creado',
-    'profile_not_created_pages' => ':userName no ha creado ninguna página',
-    'profile_not_created_chapters' => ':userName no ha creado ningún capítulo',
-    'profile_not_created_books' => ':userName no ha creado ningún libro',
+    'profile_not_created_pages' => ':userName no ha creado páginas',
+    'profile_not_created_chapters' => ':userName no ha creado capítulos',
+    'profile_not_created_books' => ':userName no ha creado libros',
+    'profile_not_created_shelves' => ':userName no ha creado estantes',
+
 
-    /**
-     * Comments
-     */
+    // Comments - Comentarios
     'comment' => 'Comentario',
     'comments' => 'Comentarios',
     'comment_add' => 'Agregar comentario',
@@ -303,10 +310,9 @@ return [
     'comment_delete_confirm' => '¿Está seguro que quiere borrar este comentario?',
     'comment_in_reply_to' => 'En respuesta a :commentId',
 
-     /**
-     * Revision
-     */
-    'revision_delete_confirm' => 'Are you sure you want to delete this revision?',
+     // Revision - Revisión
+    'revision_delete_confirm' => '¿Está seguro de que quiere eliminar esta revisión?',
+    'revision_restore_confirm' => '¿Está seguro de que quiere restaurar esta revisión? Se reemplazará el contenido de la página actual.',
     'revision_delete_success' => 'Revisión eliminada',
     'revision_cannot_delete_latest' => 'No se puede eliminar la última revisión.'
 ];
index d0caf942033e844053d4e8c39b1ed9b8d564d3bd..f8871854551d7b886d513cac3dd57116064c62b8 100644 (file)
@@ -29,6 +29,7 @@ return [
     'social_account_register_instructions' => 'Si no dispone de una cuenta, puede registrar una cuenta usando la opción de :socialAccount .',
     'social_driver_not_found' => 'Driver social no encontrado',
     'social_driver_not_configured' => 'Su configuración :socialAccount no es correcta.',
+    'invite_token_expired' => 'El enace de la esta invitación expiró. Puede intentar restablecer la contraseña de su cuenta',
 
     // System
     'path_not_writable' => 'La ruta :filePath no pudo ser cargada. Asegurese de que es escribible por el servidor.',
index 0b32b601155ae925a66f6f1f5a0fa124a8b2ab1f..11369d94d9a0c610fe235c6e9081f00ab8e4b26b 100644 (file)
@@ -1,32 +1,39 @@
 <?php
-
+/**
+ * Settings text strings
+ * Contains all text strings used in the general settings sections of BookStack
+ * including users and roles.
+ * 
+ * Cadenas de texto de la configuración
+ * Contiene todas las cadenas de texto usadas en la sección de configuración general
+ * de BookStack, incluyendo usuarios y roles.
+ */
 return [
 
-    /**
-     * Settings text strings
-     * Contains all text strings used in the general settings sections of BookStack
-     * including users and roles.
-     */
-
+    // Common Messages - Mensajes Comunes
     'settings' => 'Ajustes',
     'settings_save' => 'Guardar ajustes',
     'settings_save_success' => 'Ajustes guardados',
 
-    /**
-     * App settings
-     */
-
-    'app_settings' => 'Ajustes de Aplicación',
+    // App Settings - Configuraciones de la aplicación
+    'app_customization' => 'Personalización',
+    'app_features_security' => 'Características y Seguridad',
     'app_name' => 'Nombre de aplicación',
     'app_name_desc' => 'Este nombre se muestra en la cabecera y en cualquier correo electrónico de la aplicación',
     'app_name_header' => '¿Mostrar el nombre de la aplicación en la cabecera?',
+    'app_public_access' => 'Acceso Público',
+    'app_public_access_desc' => 'Habilitar esta opción permitirá a los visitantes, que no estén autenticados, acceder al contenido en la instancia de BookStack.',
+    'app_public_access_desc_guest' => 'El acceso de visitantes públicos se puede controlar mediante el usuario "Guest/Invitado".',
+    'app_public_access_toggle' => 'Permitir el acceso público',
     'app_public_viewing' => '¿Permitir vista pública?',
     'app_secure_images' => '¿Habilitar mayor seguridad para subir imágenes?',
+    'app_secure_images_toggle' => 'Habilitar seguridad alta para subir imágenes',
     'app_secure_images_desc' => 'Por razones de rendimiento, todas las imágenes son públicas. Esta opción agrega una cadena larga difícil de adivinar, asegúrese que los índices de directorios no están habilitados para prevenir el acceso fácil a las imágenes.',
     'app_editor' => 'Editor de página',
     'app_editor_desc' => 'Seleccione cuál editor será usado por todos los usuarios para editar páginas.',
     'app_custom_html' => 'Contenido de cabecera HTML personalizable',
     'app_custom_html_desc' => 'Cualquier contenido agregado aquí será agregado al final de la sección <head> de cada página. Esto es útil para sobreescribir estilos o agregar código para analíticas.',
+    'app_custom_html_disabled_notice' => 'El contenido personailzado para la cabecera HTML está deshabilitado en esta configuración para garantizar que cualquier cambio importante se pueda revertir.',
     'app_logo' => 'Logo de la aplicación',
     'app_logo_desc' => 'Esta imagen debería ser de 43px en altura. <br>Las imágenes grandes seán escaladas.',
     'app_primary_color' => 'Color primario de la aplicación',
@@ -35,25 +42,23 @@ return [
     'app_homepage_desc' => 'Seleccione una página de inicio para mostrar en lugar de la vista por defecto. Se ignoran los permisos de página para las páginas seleccionadas.',
     'app_homepage_select' => 'Seleccione una página',
     'app_disable_comments' => 'Deshabilitar comentarios',
+    'app_disable_comments_toggle' => 'Deshabilitar comentarios',
     'app_disable_comments_desc' => 'Deshabilitar comentarios en todas las páginas de la aplicación. Los comentarios existentes no se muestran.',
 
-    /**
-     * Registration settings
-     */
-
+    // Registration settings - Configuraciones de registro
     'reg_settings' => 'Ajustes de registro',
-    'reg_allow' => '¿Permitir registro?',
+    'reg_enable' => 'Habilitar Registro',
+    'reg_enable_toggle' => 'Habilitar registro',
+    'reg_enable_desc' => 'Cuando se habilita el registro, el usuario podrá crear su usuario en la aplicación. Con el regsitro, se le otorga un rol de usuario único y por defecto.',
     'reg_default_role' => 'Rol de usuario por defecto despúes del registro',
-    'reg_confirm_email' => '¿Requerir correo electrónico de confirmación?',
+    'reg_email_confirmation' => 'Confirmación de correo electrónico',
+    'reg_email_confirmation_toggle' => 'Requerir confirmación de correo electrónico',
     'reg_confirm_email_desc' => 'Si se utiliza la restricción por dominio, entonces se requerirá la confirmación por correo electrónico y se ignorará el valor a continuación.',
     'reg_confirm_restrict_domain' => 'Restringir registro al dominio',
     'reg_confirm_restrict_domain_desc' => 'Introduzca una lista separada por comas de los correos electrónicos del dominio a los que les gustaría restringir el registro por dominio. A los usuarios les será enviado un correo elctrónico para confirmar la dirección antes de que se le permita interactuar con la aplicación. <br> Note que a los usuarios se les permitirá cambiar sus direcciones de correo electrónico luego de un registro éxioso.',
     'reg_confirm_restrict_domain_placeholder' => 'Ninguna restricción establecida',
 
-    /**
-     * Maintenance settings
-     */
-
+    // Maintenance settings - Configuraciones de mantenimiento
     'maint' => 'Mantenimiento',
     'maint_image_cleanup' => 'Limpiar imágenes',
     'maint_image_cleanup_desc' => "Analizar contenido de páginas y revisiones para detectar cuáles imágenes y dibujos están en uso y cuáles son redundantes. Asegúrese de crear un respaldo completo de imágenes y base de datos antes de ejecutar esta tarea.",
@@ -63,10 +68,7 @@ return [
     'maint_image_cleanup_success' => 'Se encontraron y se eliminaron :count imágenes pontencialmente sin uso!',
     'maint_image_cleanup_nothing_found' => 'No se encotraron imágenes sin usar, Nada eliminado!',
 
-    /**
-     * Role settings
-     */
-
+    // Role settings - Configuraciones de roles
     'roles' => 'Roles',
     'role_user_roles' => 'Roles de usuario',
     'role_create' => 'Crear nuevo rol',
@@ -86,7 +88,9 @@ return [
     'role_manage_users' => 'Gestionar usuarios',
     'role_manage_roles' => 'Gestionar roles y permisos de roles',
     'role_manage_entity_permissions' => 'Gestionar todos los permisos de libros, capítulos y páginas',
-    'role_manage_own_entity_permissions' => 'Gestionar permisos en libros propios, capítulos y páginas',
+    'role_manage_own_entity_permissions' => 'Gestionar permisos en libro
+    s propios, capítulos y páginas',
+    'role_manage_page_templates' => 'Gestionar las plantillas de páginas',
     'role_manage_settings' => 'Gestionar ajustes de activos',
     'role_asset' => 'Permisos de activos',
     'role_asset_desc' => 'Estos permisos controlan el acceso por defecto a los activos del sistema. Permisos a Libros, Capítulos y Páginas sobreescribiran estos permisos.',
@@ -99,16 +103,22 @@ return [
     'role_users' => 'Usuarios en este rol',
     'role_users_none' => 'No hay usuarios asignados a este rol',
 
-    /**
-     * Users
-     */
-
+    // Users - Usuarios
     'users' => 'Usuarios',
     'user_profile' => 'Perfil de usuario',
     'users_add_new' => 'Agregar nuevo usuario',
     'users_search' => 'Buscar usuarios',
+    'users_details' => 'Detalles del usuario',
+    'users_details_desc' => 'Asigne un nombre de visualización y una dirección de correo electrónico para este usuario. La dirección de correo electrónico se usará pra ingresar a la aplicación.',
+    'users_details_desc_no_email' => 'Asigne un nombre de visualización a este usuario para que los demás puedan reconocerlo.',
     'users_role' => 'Roles de usuario',
+    'users_role_desc' => 'Select which roles this user will be assigned to. If a user is assigned to multiple roles the permissions from those roles will stack and they will receive all abilities of the assigned roles.',
+    'users_password' => 'User Password',
+    'users_password_desc' => 'Set a password used to log-in to the application. This must be at least 5 characters long.',
+    'users_send_invite_text' => 'Puede optar por enviar a este usuario un correo electrónico de invitación que les permita establecer su propia contraseña; de lo contrario, puede establecerla contraseña usted mismo.',
+    'users_send_invite_option' => 'Enviar correo electrónico de invitación al usuario.',
     'users_external_auth_id' => 'ID externo de autenticación',
+    'users_external_auth_id_desc' => 'This is the ID used to match this user when communicating with your LDAP system.',
     'users_password_warning' => 'Solo rellene a continuación si desea cambiar su password:',
     'users_system_public' => 'Este usuario representa cualquier usuario invitado que visita la aplicación. No puede utilizarse para hacer login sino que es asignado automáticamente.',
     'users_delete' => 'Borrar usuario',
@@ -122,6 +132,7 @@ return [
     'users_avatar' => 'Avatar del usuario',
     'users_avatar_desc' => 'Esta imagen debe ser de aproximadamente 256px por lado.',
     'users_preferred_language' => 'Lenguaje preferido',
+    'users_preferred_language_desc' => 'This option will change the language used for the user-interface of the application. This will not affect any user-created content.',
     'users_social_accounts' => 'Cuentas sociales',
     'users_social_accounts_info' => 'Aquí puede conectar sus otras cuentas para un acceso rápido y más fácil. Desconectando una cuenta aquí no revoca accesos ya autorizados. Revoque el acceso desde los ajustes de perfil en la cuenta social conectada.',
     'users_social_connect' => 'Conectar cuenta',
index 76bb03f0d3cef3d65d59b0e66274599eb5e39d23..1df55cdaebae60cf01438deb06095d15ad7de5be 100644 (file)
@@ -1,18 +1,19 @@
 <?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.
+ * 
+ * Líneas de validación
+ * Las líneas de lenguaje siguientes contienen los mensajes de error por
+ * defecto usados por el validador de la clase. Algunas de esta reglas tienen
+ * varias versiones, como las reglas de tamaño. Siéntase libre de ajustar cada
+ * uno de los mensajes.
+ */
 return [
 
-    /*
-    |--------------------------------------------------------------------------
-    | Validation Language 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.
-    |
-    */
-
+    // Standard laravel validation lines - Líneas de validación estándar de laravel
     'accepted'             => 'El :attribute debe ser aceptado.',
     'active_url'           => 'El :attribute no es una URl válida.',
     'after'                => 'El :attribute debe ser una fecha posterior :date.',
@@ -35,12 +36,41 @@ return [
     'digits'               => ':attribute debe ser de :digits dígitos.',
     'digits_between'       => ':attribute debe ser un valor entre :min y :max dígios.',
     'email'                => ':attribute debe ser una dirección álida.',
+    'ends_with'            => 'El :attribute debe terminar con uno de los siguientes: :values',
     'filled'               => 'El campo :attribute es requerido.',
+    'gt'                   => [
+        'numeric' => 'El :attribute debe ser mayor que :value.',
+        'file'    => 'El :attribute debe ser mayor que :value kilobytes.',
+        'string'  => 'El :attribute debe ser mayor que :value caracteres.',
+        'array'   => 'El :attribute debe tener más de :value objetos.',
+    ],
+    'gte'                  => [
+        'numeric' => 'El :attribute debe ser mayor o igual a :value.',
+        'file'    => 'El :attribute debe ser mayor o igual a :value kilobytes.',
+        'string'  => 'El :attribute debe ser mayor o igual a :value caracteres.',
+        'array'   => 'El :attribute debe tener :value objetos o más.',
+    ],
     'exists'               => 'El :attribute seleccionado es inválido.',
     'image'                => 'El :attribute debe ser una imagen.',
     'in'                   => 'El selected :attribute es inválio.',
+    'image_extension'      => 'El :attribute debe tener una extensión de imagen válida y soportada.',
     'integer'              => 'El :attribute debe ser un entero.',
     'ip'                   => 'El :attribute debe ser una dirección IP álida.',
+    'ipv4'                 => 'El :attribute debe ser una dirección IPv4 válida.',
+    'ipv6'                 => 'El :attribute debe ser una dirección IPv6 válida.',
+    'json'                 => 'El :attribute debe ser una cadena JSON válida.',
+    'lt'                   => [
+        'numeric' => 'El :attribute debe ser menor que :value.',
+        'file'    => 'El :attribute debe ser menor que :value kilobytes.',
+        'string'  => 'El :attribute debe ser menor que :value caracteres.',
+        'array'   => 'El :attribute debe tener menos de :value objetos.',
+    ],
+    'lte'                  => [
+        'numeric' => 'El :attribute debe ser menor o igual a :value.',
+        'file'    => 'El :attribute debe ser menor o igual a :value kilobytes.',
+        'string'  => 'El :attribute debe ser menor o igual a :value caracteres.',
+        'array'   => 'El :attribute no debe tener más de :value objetos.',
+    ],
     'max'                  => [
         'numeric' => ':attribute no puede ser mayor que :max.',
         'file'    => ':attribute no puede ser mayor que :max kilobytes.',
@@ -54,7 +84,9 @@ return [
         'string'  => ':attribute debe ser al menos :min caracteres.',
         'array'   => ':attribute debe tener como mínimo :min items.',
     ],
-    'not_in'               => ':attribute seleccionado es inválio.',
+    'no_double_extension'  => 'El :attribute debe tener una única extensión de archivo.',
+    'not_in'               => ':attribute seleccionado es inválido.',
+    'not_regex'            => 'El formato de :attribute es inválido.',
     'numeric'              => ':attribute debe ser numérico.',
     'regex'                => ':attribute con formato inválido',
     'required'             => ':attribute es requerido.',
@@ -73,37 +105,17 @@ return [
     'string'               => 'El atributo :attribute debe ser una cadena.',
     'timezone'             => 'El atributo :attribute debe ser una zona válida.',
     'unique'               => 'El atributo :attribute ya ha sido tomado.',
-    'url'                  => 'El atributo :attribute tiene un formato inválid.',
-    'is_image'             => 'El atributo :attribute debe ser una imagen válida.',
-
-    /*
-    |--------------------------------------------------------------------------
-    | Custom Validation Language Lines
-    |--------------------------------------------------------------------------
-    |
-    | Here you may specify custom validation messages for attributes using the
-    | convention "attribute.rule" to name the lines. This makes it quick to
-    | specify a specific custom language line for a given attribute rule.
-    |
-    */
+    '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.',
 
+    // Custom validation lines - Líneas de validación personalizadas
     'custom' => [
         'password-confirm' => [
             'required_with' => 'Confirmación de Password requerida',
         ],
     ],
 
-    /*
-    |--------------------------------------------------------------------------
-    | Custom Validation Attributes
-    |--------------------------------------------------------------------------
-    |
-    | The following language lines are used to swap attribute place-holders
-    | with something more reader friendly such as E-Mail Address instead
-    | of "email". This simply helps us make messages a little cleaner.
-    |
-    */
-
+    // Custom validation attributes - Atributos de validación personalizados
     'attributes' => [],
 
 ];
index 934dd56da36895819f572961ec83df5eac53d315..8b4bb1f52d2e78c794db9998aef1590928654b7b 100644 (file)
@@ -27,18 +27,20 @@ return [
     'email' => 'E-mail',
     'password' => 'Mot de passe',
     'password_confirm' => 'Confirmez le mot de passe',
-    'password_hint' => 'Doit faire plus de 5 caractères',
-    'forgot_password' => 'Mot de passe oublié ?',
+    'password_hint' => 'Doit faire plus de 7 caractères',
+    'forgot_password' => 'Mot de passe oublié ?',
     'remember_me' => 'Se souvenir de moi',
-    'ldap_email_hint' => "Merci d'entrer une adresse e-mail pour ce compte",
+    'ldap_email_hint' => "Merci d'entrer une adresse e-mail pour ce compte.",
     'create_account' => 'Créer un compte',
-    'social_login' => 'Social Login',
-    'social_registration' => 'Enregistrement Social',
-    'social_registration_text' => "S'inscrire et se connecter avec un réseau social",
+    'already_have_account' => 'Vous avez déjà un compte ?',
+    'dont_have_account' => 'Vous n\'avez pas de compte ?',
+    'social_login' => 'Connexion avec un réseau social',
+    'social_registration' => 'Inscription avec un réseau social',
+    'social_registration_text' => "S'inscrire et se connecter avec un réseau social.",
 
-    'register_thanks' => 'Merci pour votre enregistrement',
+    'register_thanks' => 'Merci pour votre inscription !',
     'register_confirm' => 'Vérifiez vos e-mails et cliquez sur le lien de confirmation pour rejoindre :appName.',
-    'registrations_disabled' => "L'inscription est désactivée pour le moment",
+    'registrations_disabled' => "Les inscriptions sont désactivées pour le moment",
     'registration_email_domain_invalid' => 'Cette adresse e-mail ne peut pas accéder à l\'application',
     'register_success' => 'Merci pour votre inscription. Vous êtes maintenant inscrit(e) et connecté(e)',
 
@@ -46,14 +48,14 @@ return [
     /**
      * Password Reset
      */
-    'reset_password' => 'Reset Password',
-    'reset_password_send_instructions' => 'Entrez votre adresse e-mail ci-dessous et un e-mail avec un lien de réinitialisation de mot de passe vous sera envoyé',
+    'reset_password' => 'Réinitialiser le mot de passe',
+    'reset_password_send_instructions' => 'Entrez votre adresse e-mail ci-dessous et un e-mail avec un lien de réinitialisation de mot de passe vous sera envoyé.',
     'reset_password_send_button' => 'Envoyer un lien de réinitialisation',
     'reset_password_sent_success' => 'Un lien de réinitialisation a été envoyé à :email.',
     'reset_password_success' => 'Votre mot de passe a été réinitialisé avec succès.',
 
     'email_reset_subject' => 'Réinitialisez votre mot de passe pour :appName',
-    'email_reset_text' => 'Vous recevez cet e-mail parce que nous avons reçu une demande de réinitialisation pour votre compte',
+    'email_reset_text' => 'Vous recevez cet e-mail parce que nous avons reçu une demande de réinitialisation pour votre compte.',
     'email_reset_not_requested' => 'Si vous n\'avez pas effectué cette demande, vous pouvez ignorer cet e-mail.',
 
 
@@ -61,11 +63,11 @@ return [
      * Email Confirmation
      */
     'email_confirm_subject' => 'Confirmez votre adresse e-mail pour :appName',
-    'email_confirm_greeting' => 'Merci d\'avoir rejoint :appName !',
-    'email_confirm_text' => 'Merci de confirmer en cliquant sur le lien ci-dessous :',
+    'email_confirm_greeting' => 'Merci d\'avoir rejoint :appName !',
+    'email_confirm_text' => 'Merci de confirmer en cliquant sur le lien ci-dessous :',
     '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_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_not_confirmed' => 'Adresse e-mail non confirmée',
@@ -73,4 +75,14 @@ return [
     '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',
-];
\ No newline at end of file
+
+    // User Invite
+    'user_invite_email_subject' => 'Vous avez été invité(e) à rejoindre :appName !',
+    'user_invite_email_greeting' => 'Un compte vous a été créé sur :appName.',
+    'user_invite_email_text' => 'Cliquez sur le bouton ci-dessous pour renseigner le mot de passe et récupérer l\'accès :',
+    'user_invite_email_action' => 'Renseignez le mot de passe de votre compte',
+    '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 !'
+];
index 3bff2841b83c40deebabceda90a24bf1410cfcf1..ae33ef3eb5e428e3a4a761a96a7c47bb01a36bff 100644 (file)
@@ -10,6 +10,7 @@ return [
     'save' => 'Enregistrer',
     'continue' => 'Continuer',
     'select' => 'Sélectionner',
+    'toggle_all' => 'Tout sélectionner',
     'more' => 'Montrer plus',
 
     /**
@@ -19,13 +20,14 @@ return [
     'description' => 'Description',
     'role' => 'Rôle',
     'cover_image' => 'Image de couverture',
-    'cover_image_description' => 'Cette image doit être environ 300x170px.',
+    'cover_image_description' => 'Cette image doit faire environ 440x250 px.',
     
     /**
      * Actions
      */
     'actions' => 'Actions',
     'view' => 'Voir',
+    'view_all' => 'Tout afficher',
     'create' => 'Créer',
     'update' => 'Modifier',
     'edit' => 'Editer',
@@ -40,6 +42,17 @@ return [
     'remove' => 'Enlever',
     'add' => 'Ajouter',
 
+    /**
+     * Sort Options
+     */
+    'sort_options' => 'Options de tri',
+    'sort_direction_toggle' => 'Inverser la direction du tri',
+    'sort_ascending' => 'Tri ascendant',
+    'sort_descending' => 'Tri descendant',
+    'sort_name' => 'Nom',
+    'sort_created_at' => 'Date de création',
+    'sort_updated_at' => 'Date de mise à jour',
+
     /**
      * Misc
      */
@@ -53,16 +66,22 @@ return [
     'grid_view' => 'Vue en grille',
     'list_view' => 'Vue en liste',
     'default' => 'Défaut',
+    'breadcrumb' => 'Fil d\'Ariane',
 
     /**
      * Header
      */
+    'profile_menu' => 'Menu du profil',
     'view_profile' => 'Voir le profil',
     'edit_profile' => 'Modifier le profil',
 
+    // Layout tabs
+    'tab_info' => 'Info',
+    'tab_content' => 'Contenu',
+
     /**
      * Email Content
      */
-    'email_action_help' => 'Si vous rencontrez des problèmes pour cliquer sur le bouton ":actionText", copiez et collez l\'adresse ci-dessous dans votre navigateur :',
+    'email_action_help' => 'Si vous rencontrez des problèmes pour cliquer sur le bouton ":actionText", copiez et collez l\'adresse ci-dessous dans votre navigateur :',
     'email_rights' => 'Tous droits réservés',
-];
\ No newline at end of file
+];
index b07a5c465e348e72c673aeaaeea5171c6fe6f89d..958a162da04f6a9726c792d3e93d1eff5dd3b9f6 100644 (file)
@@ -9,6 +9,7 @@ return [
     'recently_updated_pages' => 'Pages mises à jour récemment',
     'recently_created_chapters' => 'Chapitres créés récemment',
     'recently_created_books' => 'Livres créés récemment',
+    'recently_created_shelves' => 'Étagères créés récemment',
     'recently_update' => 'Mis à jour récemment',
     'recently_viewed' => 'Vus récemment',
     'recent_activity' => 'Activité récente',
@@ -71,11 +72,13 @@ return [
      */
     'shelf' => 'Étagère',
     'shelves' => 'Étagères',
+    'x_shelves' => ':count Étagère|:count Étagères',
     'shelves_long' => 'Étagères',
     '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 É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',
@@ -89,7 +92,7 @@ return [
     'shelves_delete' => 'Supprimer l\'étagère',
     'shelves_delete_named' => 'Supprimer l\'étagère :name',
     'shelves_delete_explain' => "Ceci va supprimer l\'étagère nommée \':bookName\'. Les livres contenus dans cette étagère ne seront pas supprimés.",
-    'shelves_delete_confirmation' => 'Êtes-vous sûr(e) de vouloir supprimer cette étagère ?',
+    'shelves_delete_confirmation' => 'Êtes-vous sûr(e) de vouloir supprimer cette étagère ?',
     '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',
@@ -108,13 +111,14 @@ return [
     'books_popular' => 'Livres populaires',
     'books_recent' => 'Livres récents',
     'books_new' => 'Nouveaux livres',
+    'books_new_action' => 'Nouveau livre',
     'books_popular_empty' => 'Les livres les plus populaires apparaîtront ici.',
     'books_new_empty' => 'Les livres les plus récents apparaitront ici.',
     'books_create' => 'Créer un nouveau livre',
     'books_delete' => 'Supprimer un livre',
     'books_delete_named' => 'Supprimer le livre :bookName',
     'books_delete_explain' => 'Ceci va supprimer le livre nommé \':bookName\', tous les chapitres et pages seront supprimés.',
-    'books_delete_confirmation' => 'Êtes-vous sûr(e) de vouloir supprimer ce livre ?',
+    'books_delete_confirmation' => 'Êtes-vous sûr(e) de vouloir supprimer ce livre ?',
     'books_edit' => 'Modifier le livre',
     'books_edit_named' => 'Modifier le livre :bookName',
     'books_form_book_name' => 'Nom du livre',
@@ -131,6 +135,11 @@ return [
     'books_navigation' => 'Navigation dans le livre',
     'books_sort' => 'Trier les contenus du livre',
     'books_sort_named' => 'Trier le livre :bookName',
+    'books_sort_name' => 'Trier par le nom',
+    'books_sort_created' => 'Trier par la date de création',
+    'books_sort_updated' => 'Trier par la date de mise à jour',
+    'books_sort_chapters_first' => 'Les chapitres en premier',
+    'books_sort_chapters_last' => 'Les chapitres en dernier',
     'books_sort_show_other' => 'Afficher d\'autres livres',
     'books_sort_save' => 'Enregistrer l\'ordre',
 
@@ -146,7 +155,7 @@ return [
     'chapters_delete' => 'Supprimer le chapitre',
     'chapters_delete_named' => 'Supprimer le chapitre :chapterName',
     'chapters_delete_explain' => 'Ceci va supprimer le chapitre \':chapterName\', toutes les pages seront déplacées dans le livre parent.',
-    'chapters_delete_confirm' => 'Etes-vous sûr(e) de vouloir supprimer ce chapitre ?',
+    'chapters_delete_confirm' => 'Etes-vous sûr(e) de vouloir supprimer ce chapitre ?',
     'chapters_edit' => 'Modifier le chapitre',
     'chapters_edit_named' => 'Modifier le chapitre :chapterName',
     'chapters_save' => 'Enregistrer le chapitre',
@@ -175,8 +184,8 @@ return [
     'pages_delete_draft' => 'Supprimer le brouillon',
     'pages_delete_success' => 'Page supprimée',
     'pages_delete_draft_success' => 'Brouillon supprimé',
-    'pages_delete_confirm' => 'Êtes-vous sûr(e) de vouloir supprimer cette page ?',
-    'pages_delete_draft_confirm' => 'Êtes-vous sûr(e) de vouloir supprimer ce brouillon ?',
+    'pages_delete_confirm' => 'Êtes-vous sûr(e) de vouloir supprimer cette page ?',
+    'pages_delete_draft_confirm' => 'Êtes-vous sûr(e) de vouloir supprimer ce brouillon ?',
     'pages_editing_named' => 'Modification de la page :pageName',
     'pages_edit_toggle_header' => 'Afficher/cacher l\'en-tête',
     'pages_edit_save_draft' => 'Enregistrer le brouillon',
@@ -230,10 +239,11 @@ return [
         'start_b' => ':userName a commencé à éditer cette page',
         'time_a' => 'depuis la dernière sauvegarde',
         'time_b' => 'dans les :minCount dernières minutes',
-        'message' => ':start :time. Attention à ne pas écraser les mises à jour de quelqu\'un d\'autre !',
+        '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_is_template' => 'Modèle de page',
 
     /**
      * Editor sidebar
@@ -272,6 +282,12 @@ return [
     'attachments_file_uploaded' => 'Fichier ajouté avec succès',
     'attachments_file_updated' => 'Fichier mis à jour avec succès',
     'attachments_link_attached' => 'Lien attaché à la page avec succès',
+    'templates' => 'Modèles',
+    'templates_set_as_template' => 'La page est un modèle',
+    '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' => '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',
 
     /**
      * Profile View
@@ -281,6 +297,7 @@ return [
     'profile_not_created_pages' => ':userName n\'a pas créé de page',
     'profile_not_created_chapters' => ':userName n\'a pas créé de chapitre',
     'profile_not_created_books' => ':userName n\'a pas créé de livre',
+    'profile_not_created_shelves' => ':userName n\'a pas créé d\'étagère',
 
     /**
      * Comments
@@ -289,23 +306,24 @@ return [
     'comments' => 'Commentaires',
     'comment_add' => 'Ajouter un commentaire',
     'comment_placeholder' => 'Entrez vos commentaires ici',
-    'comment_count' => '{0} Pas de commentaires|{1} 1 Commentaire|[2,*] :count Commentaires',
+    'comment_count' => '{0} Pas de commentaires|{1} Un commentaire|[2,*] :count commentaires',
     'comment_save' => 'Enregistrer le commentaire',
-    'comment_saving' => 'Enregistrement du commentaire...',
-    'comment_deleting' => 'Suppression du commentaire...',
+    'comment_saving' => 'Enregistrement du commentaire',
+    'comment_deleting' => 'Suppression du commentaire',
     'comment_new' => 'Nouveau commentaire',
     'comment_created' => 'commenté :createDiff',
     'comment_updated' => 'Mis à jour :updateDiff par :username',
     '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' => 'Etes-vous sûr de vouloir supprimer ce commentaire ?',
     'comment_in_reply_to' => 'En réponse à :commentId',
 
      /**
      * Revision
      */
-    'revision_delete_confirm' => 'Êtes-vous sûr de vouloir supprimer cette révision?',
+    'revision_delete_confirm' => 'Êtes-vous sûr de vouloir supprimer cette révision ?',
+    'revision_restore_confirm' => 'Êtes-vous sûr de vouloir restaurer cette révision ? Le contenu courant de la page va être remplacé.',
     'revision_delete_success' => 'Révision supprimée',
     'revision_cannot_delete_latest' => 'Impossible de supprimer la dernière révision.'
-];
\ No newline at end of file
+];
index e758f6e5e5c7da99c4a3d663dc6430efac561b6c..ecb751f25c0681fc89cc8047d700cd5b37196c7f 100644 (file)
@@ -20,7 +20,7 @@ return [
     'ldap_extension_not_installed' => 'L\'extension LDAP PHP n\'est pas installée',
     'ldap_cannot_connect' => 'Impossible de se connecter au serveur LDAP, la connexion initiale a échoué',
     'social_no_action_defined' => 'Pas d\'action définie',
-    'social_login_bad_response' => "Erreur pendant la tentative de connexion à :socialAccount : \n:error",
+    '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_existing' => 'Ce compte :socialAccount est déjà rattaché à votre profil.',
@@ -29,6 +29,7 @@ return [
     '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_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.',
 
     // System
     'path_not_writable' => 'Impossible d\'écrire dans :filePath. Assurez-vous d\'avoir les droits d\'écriture sur le serveur',
@@ -66,6 +67,7 @@ return [
     'role_cannot_be_edited' => 'Ce rôle ne peut pas être modifié',
     'role_system_cannot_be_deleted' => 'Ceci est un rôle du système et ne peut pas être supprimé',
     'role_registration_default_cannot_delete' => 'Ce rôle ne peut pas être supprimé tant qu\'il est le rôle par défaut',
+    'role_cannot_remove_only_admin' => 'Ceci est le seul compte administrateur. Assignez un nouvel administrateur avant de le supprimer ici.',
 
     // Error pages
     '404_page_not_found' => 'Page non trouvée',
index 484b4b20c295c6f7862e72cfff6cf3a22e2ee172..16d5e4d2c58597d45e4bd55e54086dc679b22423 100644 (file)
@@ -16,7 +16,7 @@ return [
     'password' => 'Les mots de passe doivent faire au moins 6 caractères et correspondre à la confirmation.',
     'user' => "Nous n'avons pas trouvé d'utilisateur avec cette adresse.",
     'token' => 'Le jeton de réinitialisation est invalide.',
-    'sent' => 'Nous vous avons envoyé un lien de réinitialisation de mot de passe !',
-    'reset' => 'Votre mot de passe a été réinitialisé !',
+    'sent' => 'Nous vous avons envoyé un lien de réinitialisation de mot de passe !',
+    'reset' => 'Votre mot de passe a été réinitialisé !',
 
 ];
index 4251fe87dcd0e58f27fef2ed287b49cc9da459be..1f32d746ad53895b3d4dbcace9d0e528b0d82599 100644 (file)
@@ -16,12 +16,18 @@ return [
      * App settings
      */
 
-    'app_settings' => 'Préférences de l\'application',
+    'app_customization' => 'Personnalisation',
+    'app_features_security' => 'Fonctionnalités et sécurité',
     'app_name' => 'Nom de l\'application',
     'app_name_desc' => 'Ce nom est affiché dans l\'en-tête et les e-mails.',
-    'app_name_header' => 'Afficher le nom dans l\'en-tête ?',
-    'app_public_viewing' => 'Accepter le visionnage public des pages ?',
-    'app_secure_images' => 'Activer l\'ajout d\'image sécurisé ?',
+    'app_name_header' => 'Afficher le nom dans l\'en-tête ?',
+    'app_public_access' => 'Accès public',
+    '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_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_desc' => 'Sélectionnez l\'éditeur qui sera utilisé pour modifier les pages.',
@@ -35,6 +41,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_disable_comments' => 'Désactiver les commentaires',
+    'app_disable_comments_toggle' => 'Désactiver les commentaires',
     'app_disable_comments_desc' => 'Désactive les commentaires sur toutes les pages de l\'application. Les commentaires existants ne sont pas affichés.',
     
     /**
@@ -42,9 +49,12 @@ return [
      */
 
     'reg_settings' => 'Préférence pour l\'inscription',
-    'reg_allow' => 'Accepter l\'inscription ?',
+    'reg_enable' => 'Activer l\'inscription',
+    'reg_enable_toggle' => 'Activer l\'inscription',
+    'reg_enable_desc' => 'Lorsque l\'inscription est activée, l\'utilisateur pourra s\'enregistrer en tant qu\'utilisateur de l\'application. Lors de l\'inscription, ils se voient attribuer un rôle par défaut.',
     'reg_default_role' => 'Rôle par défaut lors de l\'inscription',
-    'reg_confirm_email' => 'Obliger la confirmation par e-mail ?',
+    'reg_email_confirmation' => 'Confirmation de l\'e-mail',
+    'reg_email_confirmation_toggle' => 'Obliger la confirmation par e-mail ?',
     'reg_confirm_email_desc' => 'Si la restriction de domaine est activée, la confirmation sera automatiquement obligatoire et cette valeur sera ignorée.',
     'reg_confirm_restrict_domain' => 'Restreindre l\'inscription à un domaine',
     'reg_confirm_restrict_domain_desc' => 'Entrez une liste de domaines acceptés lors de l\'inscription, séparés par une virgule. Les utilisateurs recevront un e-mail de confirmation à cette adresse. <br> Les utilisateurs pourront changer leur adresse après inscription s\'ils le souhaitent.',
@@ -59,9 +69,9 @@ return [
     'maint_image_cleanup_desc' => "Scan le contenu des pages et des révisions pour vérifier les images et les dessins en cours d'utilisation et lesquels sont redondant. Veuillez à faire une sauvegarde de la base de données et des images avant de lancer ceci.",
     'maint_image_cleanup_ignore_revisions' => 'Ignorer les images dans les révisions',
     'maint_image_cleanup_run' => 'Lancer le nettoyage',
-    'maint_image_cleanup_warning' => ':count images potentiellement inutilisées trouvées. Etes-vous sûr de vouloir supprimer ces images ?',
-    'maint_image_cleanup_success' => ':count images potentiellement inutilisées trouvées et supprimées !',
-    'maint_image_cleanup_nothing_found' => 'Aucune image inutilisée trouvée, rien à supprimer !',
+    'maint_image_cleanup_warning' => ':count images potentiellement inutilisées trouvées. Etes-vous sûr de vouloir supprimer ces images ?',
+    'maint_image_cleanup_success' => ':count images potentiellement inutilisées trouvées et supprimées !',
+    'maint_image_cleanup_nothing_found' => 'Aucune image inutilisée trouvée, rien à supprimer !',
     
     /**
      * Role settings
@@ -75,7 +85,7 @@ return [
     'role_delete_confirm' => 'Ceci va supprimer le rôle \':roleName\'.',
     'role_delete_users_assigned' => 'Ce rôle a :userCount utilisateurs assignés. Vous pouvez choisir un rôle de remplacement pour ces utilisateurs.',
     'role_delete_no_migration' => "Ne pas assigner de nouveau rôle",
-    'role_delete_sure' => 'Êtes-vous sûr de vouloir supprimer ce rôle ?',
+    'role_delete_sure' => 'Êtes-vous sûr de vouloir supprimer ce rôle ?',
     'role_delete_success' => 'Le rôle a été supprimé avec succès',
     'role_edit' => 'Modifier le rôle',
     'role_details' => 'Détails du rôle',
@@ -86,7 +96,7 @@ return [
     'role_manage_users' => 'Gérer les utilisateurs',
     'role_manage_roles' => 'Gérer les rôles et permissions',
     'role_manage_entity_permissions' => 'Gérer les permissions sur les livres, chapitres et pages',
-    'role_manage_own_entity_permissions' => 'Gérer les permissions de ses propres livres, chapitres, et pages',
+    'role_manage_own_entity_permissions' => 'Gérer les permissions de ses propres livres, chapitres et pages',
     'role_manage_settings' => 'Gérer les préférences de l\'application',
     'role_asset' => 'Permissions des ressources',
     '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',
@@ -107,21 +117,29 @@ return [
     'user_profile' => 'Profil d\'utilisateur',
     'users_add_new' => 'Ajouter un nouvel utilisateur',
     'users_search' => 'Chercher les utilisateurs',
-    'users_role' => 'Rôles des utilisateurs',
+    'users_details' => 'Informations de l\'utilisateur',
+    'users_details_desc' => 'Définissez un nom et une adresse e-mail pour cet utilisateur. L\'adresse e-mail sera utilisée pour se connecter à l\'application.',
+    'users_details_desc_no_email' => 'Définissez un nom d\'affichage pour cet utilisateur afin que les autres puissent le reconnaître.',
+    '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_external_auth_id' => 'Identifiant d\'authentification externe',
+    'users_external_auth_id_desc' => 'Il s\'agit de l\'identifiant utilisé pour appairer cet utilisateur lors de la communication avec votre système LDAP.',
     '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_delete_confirm' => 'Êtes-vous sûr(e) de vouloir supprimer cet utilisateur ?',
     'users_delete_success' => 'Utilisateurs supprimés avec succès',
     'users_edit' => 'Modifier l\'utilisateur',
     'users_edit_profile' => 'Modifier le profil',
     'users_edit_success' => 'Utilisateur mis à jour avec succès',
     'users_avatar' => 'Avatar de l\'utilisateur',
-    'users_avatar_desc' => 'Cette image doit être un carré d\'environ 256px.',
+    '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_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',
index 9204f4e2d35c27ad8fb61e050d74bbe589a77a76..04993f249fb09729512261f00cd70b538edbb39b 100644 (file)
@@ -35,12 +35,41 @@ return [
     'digits'               => ':attribute doit être de longueur :digits.',
     'digits_between'       => ':attribute doit avoir une longueur entre :min et :max.',
     'email'                => ':attribute doit être une adresse e-mail valide.',
+    'ends_with'            => ':attribute doit se terminer par une des valeurs suivantes : :values',
     'filled'               => ':attribute est un champ requis.',
+    'gt'                   => [
+        'numeric' => ':attribute doit être plus grand que :value.',
+        'file'    => ':attribute doit être plus grand que :value kilobytes.',
+        '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.',
+        'string'  => ':attribute doit être plus grand ou égal à :value caractères.',
+        'array'   => ':attribute doit avoir :value éléments ou plus.',
+    ],
     'exists'               => 'L\'attribut :attribute est invalide.',
     'image'                => ':attribute doit être une image.',
+    'image_extension'      => ':attribute doit avoir une extension d\'image valide et supportée.',
     'in'                   => 'L\'attribut :attribute est invalide.',
     'integer'              => ':attribute doit être un chiffre entier.',
     '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.',
+    'lt'                   => [
+        'numeric' => ':attribute doit être plus petit que :value.',
+        'file'    => ':attribute doit être plus petit que :value kilobytes.',
+        '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.',
+        '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.',
@@ -54,7 +83,9 @@ return [
         'string'  => ':attribute doit contenir au moins :min caractères.',
         'array'   => ':attribute doit contenir au moins :min éléments.',
     ],
+    'no_double_extension'  => ':attribute ne doit avoir qu\'une seule extension de fichier.',
     'not_in'               => 'L\'attribut sélectionné :attribute est invalide.',
+    'not_regex'            => ':attribute a un format invalide.',
     'numeric'              => ':attribute doit être un nombre.',
     'regex'                => ':attribute a un format invalide.',
     'required'             => ':attribute est un champ requis.',
@@ -74,6 +105,7 @@ return [
     'timezone'             => ':attribute doit être une zone valide.',
     '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.',
 
     /*
     |--------------------------------------------------------------------------
diff --git a/resources/lang/hu/activities.php b/resources/lang/hu/activities.php
new file mode 100644 (file)
index 0000000..575e9e5
--- /dev/null
@@ -0,0 +1,48 @@
+<?php
+/**
+ * Activity text strings.
+ * Is used for all the text within activity logs & notifications.
+ */
+return [
+
+    // Pages
+    'page_create'                 => 'létrehozta az oldalt:',
+    'page_create_notification'    => 'Oldal sikeresen létrehozva',
+    'page_update'                 => 'frissítette az oldalt:',
+    'page_update_notification'    => 'Oldal sikeresen frissítve',
+    'page_delete'                 => 'törölte az oldalt:',
+    'page_delete_notification'    => 'Oldal sikeresen törölve',
+    'page_restore'                => 'visszaállította az oldalt:',
+    'page_restore_notification'   => 'Oldal sikeresen visszaállítva',
+    'page_move'                   => 'áthelyezte az oldalt:',
+
+    // Chapters
+    'chapter_create'              => 'létrehozta a fejezetet:',
+    'chapter_create_notification' => 'Fejezet sikeresen létrehozva',
+    'chapter_update'              => 'frissítette a fejezetet:',
+    'chapter_update_notification' => 'Fejezet sikeresen frissítve',
+    'chapter_delete'              => 'törölte a fejezetet:',
+    'chapter_delete_notification' => 'Fejezet sikeresen törölve',
+    'chapter_move'                => 'áthelyezte a fejezetet:',
+
+    // Books
+    'book_create'                 => 'létrehozott egy könyvet:',
+    'book_create_notification'    => 'Könyv sikeresen létrehozva',
+    'book_update'                 => 'frissítette a könyvet:',
+    'book_update_notification'    => 'Könyv sikeresen frissítve',
+    'book_delete'                 => 'törölte a könyvet:',
+    'book_delete_notification'    => 'Könyv sikeresen törölve',
+    'book_sort'                   => 'átrendezte a könyvet:',
+    'book_sort_notification'      => 'Könyv sikeresen újrarendezve',
+
+    // Bookshelves
+    'bookshelf_create'            => 'létrehozta a könyvespolcot:',
+    'bookshelf_create_notification'    => 'Könyvespolc sikeresen létrehozva',
+    'bookshelf_update'                 => 'frissítette a könyvespolcot:',
+    'bookshelf_update_notification'    => 'Könyvespolc sikeresen frissítve',
+    'bookshelf_delete'                 => 'törölte a könyvespolcot:',
+    'bookshelf_delete_notification'    => 'Könyvespolc sikeresen törölve',
+
+    // Other
+    'commented_on'                => 'megjegyzést fűzött hozzá:',
+];
diff --git a/resources/lang/hu/auth.php b/resources/lang/hu/auth.php
new file mode 100644 (file)
index 0000000..d96509d
--- /dev/null
@@ -0,0 +1,67 @@
+<?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' => 'Ezek a hitelesítő adatok nem egyeznek a rögzítettekkel.',
+    'throttle' => 'Túl sok bejelentkezési próbálkozás. :seconds múlva lehet újra megpróbálni.',
+
+    // Login & Register
+    'sign_up' => 'Regisztráció',
+    'log_in' => 'Bejelentkezés',
+    'log_in_with' => 'Bejelentkezés ezzel: :socialDriver',
+    'sign_up_with' => 'Regisztráció ezzel: :socialDriver',
+    'logout' => 'Kijelentkezés',
+
+    'name' => 'Név',
+    'username' => 'Felhasználónév',
+    'email' => 'Email',
+    'password' => 'Jelszó',
+    'password_confirm' => 'Jelszó megerősítése',
+    'password_hint' => 'Négy karakternél hosszabbnak kell lennie',
+    'forgot_password' => 'Elfelejtett jelszó?',
+    'remember_me' => 'Emlékezzen rám',
+    'ldap_email_hint' => 'A fiókhoz használt email cím megadása.',
+    'create_account' => 'Fiók létrehozása',
+    'already_have_account' => 'Korábban volt beállítva fiók?',
+    'dont_have_account' => 'Még nincs beállítva fiók?',
+    'social_login' => 'Közösségi bejelentkezés',
+    'social_registration' => 'Közösségi regisztráció',
+    'social_registration_text' => 'Regisztráció és bejelentkezés másik szolgáltatással.',
+
+    'register_thanks' => 'Köszönjük a regisztrációt!',
+    'register_confirm' => 'Ellenőrizni kell a megadott email címet és a megerősítő gombra kell kattintani :appName eléréséhez.',
+    'registrations_disabled' => 'A regisztráció jelenleg le van tiltva',
+    'registration_email_domain_invalid' => 'Ebből az email tartományról nem lehet hozzáférni ehhez az alkalmazáshoz',
+    'register_success' => 'Köszönjük a regisztrációt! A regisztráció és a bejelentkezés megtörtént.',
+
+
+    // Password Reset
+    'reset_password' => 'Jelszó visszaállítása',
+    'reset_password_send_instructions' => 'Meg kell adni az email címet amire egy jelszó visszaállító hivatkozás lesz elküldve.',
+    'reset_password_send_button' => 'Visszaállító hivatkozás elküldése',
+    'reset_password_sent_success' => 'Jelszó visszaállító hivatkozás elküldve :email címre.',
+    'reset_password_success' => 'A jelszó sikeresen visszaállítva.',
+    'email_reset_subject' => ':appName jelszó visszaállítása',
+    'email_reset_text' => 'Ezt az emailt azért küldtük mert egy jelszó visszaállításra vonatkozó kérést kaptunk ebből a fiókból.',
+    'email_reset_not_requested' => 'Ha nem történt jelszó visszaállításra vonatkozó kérés, akkor nincs szükség további intézkedésre.',
+
+
+    // Email Confirmation
+    'email_confirm_subject' => ':appName alkalmazásban beállított email címet meg kell erősíteni',
+    'email_confirm_greeting' => ':appName köszöni a csatlakozást!',
+    'email_confirm_text' => 'Az email címet a lenti gombra kattintva lehet megerősíteni:',
+    'email_confirm_action' => 'Email megerősítése',
+    'email_confirm_send_error' => 'Az email megerősítés kötelező, de a rendszer nem tudta elküldeni az emailt. Fel kell venni a kapcsolatot az adminisztrátorral és meg kell győződni róla, hogy az email beállítások megfelelőek.',
+    'email_confirm_success' => 'Az email cím megerősítve!',
+    'email_confirm_resent' => 'Megerősítő email újraküldve. Ellenőrizni kell a bejövő üzeneteket.',
+
+    'email_not_confirmed' => 'Az email cím nincs megerősítve',
+    'email_not_confirmed_text' => 'Az email cím még nincs megerősítve.',
+    'email_not_confirmed_click_link' => 'Rá kell kattintani a regisztráció után nem sokkal elküldött emailben található hivatkozásra.',
+    'email_not_confirmed_resend' => 'Ha nem érkezik meg a megerősítő email, a lenti űrlap beküldésével újra lehet küldeni.',
+    'email_not_confirmed_resend_button' => 'Megerősítő email újraküldése',
+];
diff --git a/resources/lang/hu/common.php b/resources/lang/hu/common.php
new file mode 100644 (file)
index 0000000..4e72d5f
--- /dev/null
@@ -0,0 +1,70 @@
+<?php
+/**
+ * Common elements found throughout many areas of BookStack.
+ */
+return [
+
+    // Buttons
+    'cancel' => 'Mégsem',
+    'confirm' => 'Megerősítés',
+    'back' => 'Vissza',
+    'save' => 'Mentés',
+    'continue' => 'Tovább',
+    'select' => 'Kiválasztás',
+    'toggle_all' => 'Összes átkapcsolása',
+    'more' => 'Több',
+
+    // Form Labels
+    'name' => 'Név',
+    'description' => 'Leírás',
+    'role' => 'Szerepkör',
+    'cover_image' => 'Borítókép',
+    'cover_image_description' => 'A kép méretének kb. 440x250px-nek kell lennie.',
+    
+    // Actions
+    'actions' => 'Műveletek',
+    'view' => 'Megtekintés',
+    'view_all' => 'Összes megtekintése',
+    'create' => 'Létrehozás',
+    'update' => 'Frissítés',
+    'edit' => 'Szerkesztés',
+    'sort' => 'Rendezés',
+    'move' => 'Áthelyezés',
+    'copy' => 'Másolás',
+    'reply' => 'Válasz',
+    'delete' => 'Törlés',
+    'search' => 'Keresés',
+    'search_clear' => 'Keresés törlése',
+    'reset' => 'Visszaállítás',
+    'remove' => 'Eltávolítás',
+    'add' => 'Hozzáadás',
+
+    // Sort Options
+    'sort_name' => 'Név',
+    'sort_created_at' => 'Létrehozás dátuma',
+    'sort_updated_at' => 'Frissítés dátuma',
+
+    // Misc
+    'deleted_user' => 'Törölt felhasználó',
+    'no_activity' => 'Nincs megjeleníthető aktivitás',
+    'no_items' => 'Nincsenek elérhető elemek',
+    'back_to_top' => 'Oldal eleje',
+    'toggle_details' => 'Részletek átkapcsolása',
+    'toggle_thumbnails' => 'Bélyegképek átkapcsolása',
+    'details' => 'Részletek',
+    'grid_view' => 'Rács nézet',
+    'list_view' => 'Lista nézet',
+    'default' => 'Alapértelmezés szerinti',
+
+    // Header
+    'view_profile' => 'Profil megtekintése',
+    'edit_profile' => 'Profil szerkesztése',
+
+    // Layout tabs
+    'tab_info' => 'Információ',
+    'tab_content' => 'Tartalom',
+
+    // Email Content
+    'email_action_help' => 'Probléma esetén a lenti ":actionText" gombra kell kattintani, majd ki kell másolni a lenti webcímet és be kell illeszteni egy böngészőbe:',
+    'email_rights' => 'Minden jog fenntartva',
+];
diff --git a/resources/lang/hu/components.php b/resources/lang/hu/components.php
new file mode 100644 (file)
index 0000000..1f98df2
--- /dev/null
@@ -0,0 +1,33 @@
+<?php
+/**
+ * Text used in custom JavaScript driven components.
+ */
+return [
+
+    // Image Manager
+    'image_select' => 'Kép kiválasztása',
+    'image_all' => 'Összes',
+    'image_all_title' => 'Összes kép megtekintése',
+    'image_book_title' => 'A könyv feltöltött képek megtekintése',
+    'image_page_title' => 'Az oldalra feltöltött képek megtekintése',
+    'image_search_hint' => 'Keresés kép neve alapján',
+    'image_uploaded' => 'Feltöltve ekkor: :uploadedDate',
+    'image_load_more' => 'Több betöltése',
+    'image_image_name' => 'Kép neve',
+    'image_delete_used' => 'Ez a kép a lenti oldalakon van használatban.',
+    'image_delete_confirm' => 'A kép törléséhez ismét rá kell kattintani a törlésre.',
+    'image_select_image' => 'Kép kiválasztása',
+    'image_dropzone' => 'Képek feltöltése ejtéssel vagy kattintással',
+    'images_deleted' => 'Képek törölve',
+    'image_preview' => 'Kép előnézete',
+    'image_upload_success' => 'Kép sikeresen feltöltve',
+    'image_update_success' => 'Kép részletei sikeresen frissítve',
+    'image_delete_success' => 'Kép sikeresen törölve',
+    'image_upload_remove' => 'Eltávolítás',
+
+    // Code Editor
+    'code_editor' => 'Kód szerkesztése',
+    'code_language' => 'Kód nyelve',
+    'code_content' => 'Kód tartalom',
+    'code_save' => 'Kód mentése',
+];
diff --git a/resources/lang/hu/entities.php b/resources/lang/hu/entities.php
new file mode 100644 (file)
index 0000000..5bd865d
--- /dev/null
@@ -0,0 +1,305 @@
+<?php
+/**
+ * Text used for 'Entities' (Document Structure Elements) such as
+ * Books, Shelves, Chapters & Pages
+ */
+return [
+
+    // Shared
+    'recently_created' => 'Legutóbb létrehozott',
+    'recently_created_pages' => 'Legutóbb létrehozott oldalak',
+    'recently_updated_pages' => 'Legutóbb frissített oldalak',
+    'recently_created_chapters' => 'Legutóbb létrehozott fejezetek',
+    'recently_created_books' => 'Legutóbb létrehozott könyvek',
+    'recently_created_shelves' => 'Legutóbb létrehozott polcok',
+    'recently_update' => 'Legutóbb frissített',
+    'recently_viewed' => 'Legutóbb megtekintett',
+    'recent_activity' => 'Legutóbbi tevékenység',
+    'create_now' => 'Létrehozás most',
+    'revisions' => 'Változatok',
+    'meta_revision' => 'Változat #:revisionCount',
+    'meta_created' => 'Létrehozva :timeLength',
+    'meta_created_name' => ':user hozta létre :timeLength',
+    'meta_updated' => 'Frissítve :timeLength',
+    'meta_updated_name' => ':user frissítette :timeLength',
+    'entity_select' => 'Entitás kiválasztása',
+    'images' => 'Képek',
+    'my_recent_drafts' => 'Legutóbbi vázlataim',
+    'my_recently_viewed' => 'Általam legutóbb megtekintett',
+    'no_pages_viewed' => 'Még nincsenek általam megtekintett oldalak',
+    'no_pages_recently_created' => 'Nincsenek legutóbb létrehozott oldalak',
+    'no_pages_recently_updated' => 'Nincsenek legutóbb frissített oldalak',
+    'export' => 'Exportálás',
+    'export_html' => 'Webfájlt tartalmaz',
+    'export_pdf' => 'PDF fájl',
+    'export_text' => 'Egyszerű szövegfájl',
+
+    // Permissions and restrictions
+    'permissions' => 'Jogosultságok',
+    'permissions_intro' => 'Ha engedélyezett, ezek a jogosultságok elsőbbséget élveznek bármely beállított szerepkör jogosultsággal szemben.',
+    'permissions_enable' => 'Egyéni jogosultságok engedélyezése',
+    'permissions_save' => 'Jogosultságok mentése',
+
+    // Search
+    'search_results' => 'Keresési eredmények',
+    'search_total_results_found' => ':count találat|összesen :count találat',
+    'search_clear' => 'Keresés törlése',
+    'search_no_pages' => 'Nincsenek a keresésnek megfelelő oldalak',
+    'search_for_term' => ':term keresése',
+    'search_more' => 'További eredmények',
+    'search_filters' => 'Keresési szűrők',
+    'search_content_type' => 'Tartalomtípus',
+    'search_exact_matches' => 'Pontos egyezések',
+    'search_tags' => 'Címke keresések',
+    'search_options' => 'Beállítások',
+    'search_viewed_by_me' => 'Általam megtekintett',
+    'search_not_viewed_by_me' => 'Általam nem megtekintett',
+    'search_permissions_set' => 'Jogosultságok beállítva',
+    'search_created_by_me' => 'Általam létrehozott',
+    'search_updated_by_me' => 'Általam frissített',
+    'search_date_options' => 'Dátum beállítások',
+    'search_updated_before' => 'Frissítve ez előtt',
+    'search_updated_after' => 'Frissítve ez után',
+    'search_created_before' => 'Létrehozva ez előtt',
+    'search_created_after' => 'Létrehozva ez után',
+    'search_set_date' => 'Dátum beállítása',
+    'search_update' => 'Keresés frissítése',
+
+    // Shelves
+    'shelf' => 'Polc',
+    'shelves' => 'Polcok',
+    'x_shelves' => ':count polc|:count polcok',
+    'shelves_long' => 'Könyvespolcok',
+    'shelves_empty' => 'Nincsenek könyvespolcok létrehozva',
+    'shelves_create' => 'Új polc létrehozása',
+    'shelves_popular' => 'Népszerű polcok',
+    'shelves_new' => 'Új polcok',
+    'shelves_new_action' => 'Új polc',
+    'shelves_popular_empty' => 'A legnépszerűbb polcok itt fognak megjelenni.',
+    'shelves_new_empty' => 'A legutoljára létrehozott polcok itt fognak megjelenni.',
+    'shelves_save' => 'Polc mentése',
+    'shelves_books' => 'Könyvek ezen a polcon',
+    'shelves_add_books' => 'Könyvek hozzáadása ehhez a polchoz',
+    'shelves_drag_books' => 'Könyveket áthúzással lehet elhelyezni ezen a polcon',
+    'shelves_empty_contents' => 'Ehhez a polchoz nincsenek könyvek rendelve',
+    'shelves_edit_and_assign' => 'Polc szerkesztése könyvek hozzárendeléséhez',
+    'shelves_edit_named' => ':name könyvespolc szerkesztése',
+    'shelves_edit' => 'Könyvespolc szerkesztése',
+    'shelves_delete' => 'Könyvespolc törlése',
+    'shelves_delete_named' => ':name könyvespolc törlése',
+    'shelves_delete_explain' => "':name'. nevű könyvespolc ezzel le lesz törölve. A benne található könyvek nem lesznek törölve.",
+    'shelves_delete_confirmation' => 'Biztosan törölhető ez a könyvespolc?',
+    '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_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.',
+    'shelves_copy_permission_success' => 'Könyvespolc jogosultságok átmásolva :count könyvre',
+
+    // Books
+    'book' => 'Könyv',
+    'books' => 'Könyvek',
+    'x_books' => ':count könyv|:count könyv',
+    'books_empty' => 'Nincsenek könyvek létrehozva',
+    'books_popular' => 'Népszerű könyvek',
+    'books_recent' => 'Legutóbbi könyvek',
+    'books_new' => 'Új könyvek',
+    'books_new_action' => 'Új könyv',
+    'books_popular_empty' => 'A legnépszerűbb könyvek itt fognak megjelenni.',
+    'books_new_empty' => 'A legutoljára létrehozott könyvek itt fognak megjelenni.',
+    'books_create' => 'Új könyv létrehozása',
+    'books_delete' => 'Könyv törlése',
+    'books_delete_named' => ':bookName könyv törlése',
+    'books_delete_explain' => '\':bookName\' nevű könyv törölve lesz. Minden oldal és fejezet el lesz távolítva.',
+    'books_delete_confirmation' => 'Biztosan törölhető ez a könyv?',
+    'books_edit' => 'Könyv szerkesztése',
+    'books_edit_named' => ':bookName könyv szerkesztése',
+    'books_form_book_name' => 'Könyv neve',
+    'books_save' => 'Könyv mentése',
+    'books_permissions' => 'Könyv jogosultságok',
+    'books_permissions_updated' => 'Könyv jogosultságok frissítve',
+    'books_empty_contents' => 'Ehhez a könyvhöz nincsenek oldalak vagy fejezetek létrehozva.',
+    'books_empty_create_page' => 'Új oldal létrehozása',
+    'books_empty_sort_current_book' => 'Aktuális könyv rendezése',
+    'books_empty_add_chapter' => 'Fejezet hozzáadása',
+    'books_permissions_active' => 'Könyv jogosultságok aktívak',
+    'books_search_this' => 'Keresés ebben a könyvben',
+    'books_navigation' => 'Könyv navigáció',
+    'books_sort' => 'Könyv tartalmak rendezése',
+    'books_sort_named' => ':bookName könyv rendezése',
+    'books_sort_name' => 'Rendezés név szerint',
+    'books_sort_created' => 'Rendezés létrehozás dátuma szerint',
+    'books_sort_updated' => 'Rendezés frissítés dátuma szerint',
+    'books_sort_chapters_first' => 'Fejezetek elől',
+    'books_sort_chapters_last' => 'Fejezetek hátul',
+    'books_sort_show_other' => 'Egyéb könyvek mutatása',
+    'books_sort_save' => 'Új elrendezés mentése',
+
+    // Chapters
+    'chapter' => 'Fejezet',
+    'chapters' => 'Fejezetek',
+    'x_chapters' => ':count fejezet|:count fejezetek',
+    'chapters_popular' => 'Népszerű fejezetek',
+    'chapters_new' => 'Új fejezet',
+    'chapters_create' => 'Új fejezet létrehozása',
+    'chapters_delete' => 'Fejezet törlése',
+    'chapters_delete_named' => ':chapterName fejezet törlése',
+    'chapters_delete_explain' => '\':chapterName\' nevű fejezet törölve lesz. Minden oldal el lesz távolítva és közvetlenül a szülő könyvhöz lesz hozzáadva.',
+    'chapters_delete_confirm' => 'Biztosan törölhető ez a fejezet?',
+    'chapters_edit' => 'Fejezet szerkesztése',
+    'chapters_edit_named' => ':chapterName fejezet szerkesztése',
+    'chapters_save' => 'Fejezet mentése',
+    'chapters_move' => 'Fejezet áthelyezése',
+    'chapters_move_named' => ':chapterName fejezet áthelyezése',
+    'chapter_move_success' => 'Fejezet áthelyezve :bookName könyvbe',
+    'chapters_permissions' => 'Fejezet jogosultságok',
+    'chapters_empty' => 'Jelenleg nincsenek oldalak ebben a fejezetben.',
+    'chapters_permissions_active' => 'Fejezet jogosultságok aktívak',
+    'chapters_permissions_success' => 'Fejezet jogosultságok frissítve',
+    'chapters_search_this' => 'Keresés ebben a fejezetben',
+
+    // Pages
+    'page' => 'Oldal',
+    'pages' => 'Oldalak',
+    'x_pages' => ':count oldal|:count oldalak',
+    'pages_popular' => 'Népszerű oldalak',
+    'pages_new' => 'Új oldal',
+    'pages_attachments' => 'Csatolmányok',
+    'pages_navigation' => 'Oldal navigáció',
+    'pages_delete' => 'Oldal törlése',
+    'pages_delete_named' => ':pageName oldal törlése',
+    'pages_delete_draft_named' => ':pageName vázlat oldal törlése',
+    'pages_delete_draft' => 'Vázlat oldal törlése',
+    'pages_delete_success' => 'Oldal törölve',
+    'pages_delete_draft_success' => 'Vázlat oldal törölve',
+    'pages_delete_confirm' => 'Biztosan törölhető ez az oldal?',
+    'pages_delete_draft_confirm' => 'Biztosan törölhető ez a vázlatoldal?',
+    'pages_editing_named' => ':pageName oldal szerkesztése',
+    'pages_edit_toggle_header' => 'Fejléc átkapcsolása',
+    'pages_edit_save_draft' => 'Vázlat mentése',
+    'pages_edit_draft' => 'Oldal vázlat szerkesztése',
+    'pages_editing_draft' => 'Vázlat szerkesztése',
+    'pages_editing_page' => 'Oldal szerkesztése',
+    'pages_edit_draft_save_at' => 'Vázlat elmentve:',
+    'pages_edit_delete_draft' => 'Vázlat törlése',
+    'pages_edit_discard_draft' => 'Vázlat elvetése',
+    'pages_edit_set_changelog' => 'Változásnapló beállítása',
+    'pages_edit_enter_changelog_desc' => 'A végrehajtott módosítások rövid leírása',
+    'pages_edit_enter_changelog' => 'Változásnapló megadása',
+    'pages_save' => 'Oldal mentése',
+    'pages_title' => 'Oldal címe',
+    'pages_name' => 'Oldal neve',
+    'pages_md_editor' => 'Szerkesztő',
+    'pages_md_preview' => 'Előnézet',
+    'pages_md_insert_image' => 'Kép beillesztése',
+    'pages_md_insert_link' => 'Entitás hivatkozás beillesztése',
+    'pages_md_insert_drawing' => 'Rajz beillesztése',
+    'pages_not_in_chapter' => 'Az oldal nincs fejezetben',
+    'pages_move' => 'Oldal áthelyezése',
+    'pages_move_success' => 'Oldal áthelyezve ide: ":parentName"',
+    'pages_copy' => 'Oldal másolása',
+    'pages_copy_desination' => 'Másolás célja',
+    'pages_copy_success' => 'Oldal sikeresen lemásolva',
+    'pages_permissions' => 'Oldal jogosultságok',
+    'pages_permissions_success' => 'Oldal jogosultságok frissítve',
+    'pages_revision' => 'Változat',
+    'pages_revisions' => 'Oldal változatai',
+    'pages_revisions_named' => ':pageName oldal változatai',
+    'pages_revision_named' => ':pageName oldal változata',
+    'pages_revisions_created_by' => 'Létrehozta:',
+    'pages_revisions_date' => 'Változat dátuma',
+    'pages_revisions_number' => '#',
+    'pages_revisions_numbered' => 'Változat #:id',
+    'pages_revisions_numbered_changes' => '#:id változat módosításai',
+    'pages_revisions_changelog' => 'Változásnapló',
+    'pages_revisions_changes' => 'Módosítások',
+    'pages_revisions_current' => 'Aktuális verzió',
+    'pages_revisions_preview' => 'Előnézet',
+    'pages_revisions_restore' => 'Visszaállítás',
+    'pages_revisions_none' => 'Ennek az oldalnak nincsenek változatai',
+    'pages_copy_link' => 'Hivatkozás másolása',
+    'pages_edit_content_link' => 'Tartalom szerkesztése',
+    'pages_permissions_active' => 'Oldal jogosultságok aktívak',
+    'pages_initial_revision' => 'Kezdeti közzététel',
+    'pages_initial_name' => 'Új oldal',
+    'pages_editing_draft_notification' => 'A jelenleg szerkesztett vázlat legutóbb ekkor volt elmentve: :timeDiff.',
+    'pages_draft_edited_notification' => 'Ezt az oldalt azóta már frissítették. Javasolt ennek a vázlatnak az elvetése.',
+    'pages_draft_edit_active' => [
+        'start_a' => ':count felhasználók kezdte el szerkeszteni ezt az oldalt',
+        'start_b' => ':userName elkezdte szerkeszteni ezt az oldalt',
+        'time_a' => 'mióta az oldal utoljára frissítve volt',
+        'time_b' => 'az utolsó :minCount percben',
+        'message' => ':start :time. Ügyeljen arra, hogy ne írjuk felül egymás frissítéseit!',
+    ],
+    'pages_draft_discarded' => 'Vázlat elvetve, a szerkesztő frissítve lesz az oldal aktuális tartalmával',
+    'pages_specific' => 'Egy bizonyos oldal',
+
+    // Editor Sidebar
+    'page_tags' => 'Oldal címkék',
+    'chapter_tags' => 'Fejezet címkék',
+    'book_tags' => 'Könyv címkék',
+    'shelf_tags' => 'Polc címkék',
+    'tag' => 'Címke',
+    'tags' =>  'Címkék',
+    'tag_value' => 'Címke érték (nem kötelező)',
+    'tags_explain' => "Címkék hozzáadása a tartalom jobb kategorizálásához.\nA mélyebb szervezettség megvalósításához hozzá lehet rendelni egy értéket a címkéhez.",
+    'tags_add' => 'Másik címke hozzáadása',
+    'attachments' => 'Csatolmányok',
+    'attachments_explain' => 'Az oldalon megjelenő fájlok feltöltése vagy hivatkozások csatolása. Az oldal oldalsávjában fognak megjelenni.',
+    'attachments_explain_instant_save' => 'Az itt történt módosítások azonnal el lesznek mentve.',
+    'attachments_items' => 'Csatolt elemek',
+    'attachments_upload' => 'Fájlfeltöltés',
+    'attachments_link' => 'Hivatkozás csatolása',
+    'attachments_set_link' => 'Hivatkozás beállítása',
+    'attachments_delete_confirm' => 'A csatolmány törléséhez ismét rá kell kattintani a törlésre.',
+    'attachments_dropzone' => 'Fájlok csatolása ejtéssel vagy kattintással',
+    'attachments_no_files' => 'Nincsenek fájlok feltöltve',
+    'attachments_explain_link' => 'Fájl feltöltése helyett hozzá lehet kapcsolni egy hivatkozást. Ez egy hivatkozás lesz egy másik oldalra vagy egy fájlra a felhőben.',
+    'attachments_link_name' => 'Hivatkozás neve',
+    'attachment_link' => 'Csatolmány hivatkozás',
+    'attachments_link_url' => 'Hivatkozás fájlra',
+    'attachments_link_url_hint' => 'Weboldal vagy fájl webcíme',
+    'attach' => 'Csatolás',
+    'attachments_edit_file' => 'Fájl szerkesztése',
+    'attachments_edit_file_name' => 'Fájl neve',
+    'attachments_edit_drop_upload' => 'Feltöltés és felülírás ejtéssel vagy kattintással',
+    'attachments_order_updated' => 'Csatolmány sorrend frissítve',
+    'attachments_updated_success' => 'Csatolmány részletei frissítve',
+    'attachments_deleted' => 'Csatolmány törölve',
+    'attachments_file_uploaded' => 'Fájl sikeresen feltöltve',
+    'attachments_file_updated' => 'Fájl sikeresen frissítve',
+    'attachments_link_attached' => 'Hivatkozás sikeresen hozzácsatolva az oldalhoz',
+
+    // Profile View
+    'profile_user_for_x' => 'Felhasználó ez óta: :time',
+    'profile_created_content' => 'Létrehozott tartalom',
+    'profile_not_created_pages' => ':userName még nem hozott létre oldalt',
+    'profile_not_created_chapters' => ':userName még nem hozott létre fejezetet',
+    'profile_not_created_books' => ':userName még nem hozott létre könyvet',
+    'profile_not_created_shelves' => ':userName még nem hozott létre polcot',
+
+    // Comments
+    'comment' => 'Megjegyzés',
+    'comments' => 'Megjegyzések',
+    'comment_add' => 'Megjegyzés hozzáadása',
+    'comment_placeholder' => 'Megjegyzés írása',
+    'comment_count' => '{0} Nincs megjegyzés|{1} 1 megjegyzés|[2,*] :count megjegyzés',
+    'comment_save' => 'Megjegyzés mentése',
+    'comment_saving' => 'Megjegyzés mentése...',
+    'comment_deleting' => 'Megjegyzés törlése...',
+    'comment_new' => 'Új megjegyzés',
+    'comment_created' => 'megjegyzést fűzött hozzá :createDiff',
+    'comment_updated' => 'Frissítve :updateDiff :username által',
+    'comment_deleted_success' => 'Megjegyzés törölve',
+    'comment_created_success' => 'Megjegyzés hozzáadva',
+    'comment_updated_success' => 'Megjegyzés frissítve',
+    'comment_delete_confirm' => 'Biztosan törölhető ez a megjegyzés?',
+    'comment_in_reply_to' => 'Válasz erre: :commentId',
+
+    // Revision
+    'revision_delete_confirm' => 'Biztosan törölhető ez a változat?',
+    'revision_restore_confirm' => 'Biztosan visszaállítható ez a változat? A oldal jelenlegi tartalma le lesz cserélve.',
+    'revision_delete_success' => 'Változat törölve',
+    'revision_cannot_delete_latest' => 'A legutolsó változat nem törölhető.'
+];
\ No newline at end of file
diff --git a/resources/lang/hu/errors.php b/resources/lang/hu/errors.php
new file mode 100644 (file)
index 0000000..6791428
--- /dev/null
@@ -0,0 +1,84 @@
+<?php
+/**
+ * Text shown in error messaging.
+ */
+return [
+
+    // Permissions
+    'permission' => 'Nincs jogosultság a kért oldal eléréséhez.',
+    'permissionJson' => 'Nincs jogosultság a kért művelet végrehajtásához.',
+
+    // Auth
+    'error_user_exists_different_creds' => ':email címmel már létezik felhasználó, de más hitelesítő adatokkal.',
+    'email_already_confirmed' => 'Az email cím már meg van erősítve, meg lehet próbálni a bejelentkezést.',
+    'email_confirmation_invalid' => 'A megerősítő vezérjel nem érvényes vagy használva volt. Meg kell próbálni újraregisztrálni.',
+    'email_confirmation_expired' => 'A megerősítő vezérjel lejárt. Egy új megerősítő email lett elküldve.',
+    'ldap_fail_anonymous' => 'Nem sikerült az LDAP elérése névtelen csatlakozással',
+    'ldap_fail_authed' => 'Az LDAP hozzáférés nem sikerült a megadott DN és jelszó beállításokkal',
+    'ldap_extension_not_installed' => 'LDAP PHP kiterjesztés nincs telepítve',
+    'ldap_cannot_connect' => 'Nem lehet kapcsolódni az LDAP kiszolgálóhoz, a kezdeti kapcsolatfelvétel nem sikerült',
+    'social_no_action_defined' => 'Nincs művelet meghatározva',
+    'social_login_bad_response' => "Hiba történt :socialAccount bejelentkezés közben:\n:error",
+    'social_account_in_use' => ':socialAccount fiók már használatban van. :socialAccount opción keresztül érdemes megpróbálni a bejelentkezést.',
+    'social_account_email_in_use' => ':email email cím már használatban van. Ha már van fiók létrehozva, :egy socialAccount fiókot hozzá lehet csatolni a profil beállításainál.',
+    'social_account_existing' => ':socialAccount már hozzá van kapcsolva a fiókhoz.',
+    'social_account_already_used_existing' => ':socialAccount fiókot már egy másik felhasználó használja.',
+    'social_account_not_used' => ':socialAccount fiók nincs felhasználóhoz kapcsolva. A hozzákapcsolást a profil oldalon lehet elvégezni. ',
+    'social_account_register_instructions' => ':socialAccount beállítása használatával is lehet fiókot regisztrálni, ha még nem volt fiók létrehozva.',
+    'social_driver_not_found' => 'Közösségi meghajtó nem található',
+    'social_driver_not_configured' => ':socialAccount közösségi beállítások nem megfelelőek.',
+
+    // System
+    'path_not_writable' => ':filePath elérési út nem tölthető fel. Ellenőrizni kell, hogy az útvonal a kiszolgáló számára írható.',
+    'cannot_get_image_from_url' => 'Nem lehet lekérni a képet innen: :url',
+    'cannot_create_thumbs' => 'A kiszolgáló nem tud létrehozni bélyegképeket. Ellenőrizni kell, hogy telepítve van-a a GD PHP kiterjesztés.',
+    'server_upload_limit' => 'A kiszolgáló nem engedélyez ilyen méretű feltöltéseket. Kisebb fájlmérettel kell próbálkozni.',
+    'uploaded'  => 'A kiszolgáló nem engedélyez ilyen méretű feltöltéseket. Kisebb fájlmérettel kell próbálkozni.',
+    'image_upload_error' => 'Hiba történt a kép feltöltése közben',
+    'image_upload_type_error' => 'A feltöltött kép típusa érvénytelen',
+    'file_upload_timeout' => 'A fáj feltöltése időtúllépést okozott.',
+
+    // Attachments
+    'attachment_page_mismatch' => 'Oldal eltárás csatolmány frissítése közben',
+    'attachment_not_found' => 'Csatolmány nem található',
+
+    // Pages
+    'page_draft_autosave_fail' => 'Nem sikerült a vázlat mentése. Mentés előtt meg kell róla győződni, hogy van internetkapcsolat',
+    'page_custom_home_deletion' => 'Nem lehet oldalt törölni ha kezdőlapnak van beállítva',
+
+    // Entities
+    'entity_not_found' => 'Entitás nem található',
+    'bookshelf_not_found' => 'Könyvespolc nem található',
+    'book_not_found' => 'Könyv nem található',
+    'page_not_found' => 'Oldal nem található',
+    'chapter_not_found' => 'Fejezet nem található',
+    'selected_book_not_found' => 'A kiválasztott könyv nem található',
+    'selected_book_chapter_not_found' => 'A kiválasztott könyv vagy fejezet nem található',
+    'guests_cannot_save_drafts' => 'Vendégek nem menthetnek el vázlatokat',
+
+    // Users
+    'users_cannot_delete_only_admin' => 'Nem lehet törölni az egyetlen adminisztrátort',
+    'users_cannot_delete_guest' => 'A vendég felhasználót nem lehet törölni',
+
+    // Roles
+    'role_cannot_be_edited' => 'Ezt a szerepkört nem lehet szerkeszteni',
+    'role_system_cannot_be_deleted' => 'Ez a szerepkör egy rendszer szerepkör ezért nem törölhető',
+    'role_registration_default_cannot_delete' => 'Ezt a szerepkört nem lehet törölni amíg alapértelmezés szerinti regisztrációs szerepkörnek van beállítva',
+    'role_cannot_remove_only_admin' => 'Ez a felhasználó az egyetlen, az adminisztrátor szerepkörhöz rendelt felhasználó. Eltávolítása előtt az adminisztrátor szerepkört át kell ruházni egy másik felhasználóra.',
+
+    // Comments
+    'comment_list' => 'Hiba történt a megjegyzések lekérése közben.',
+    'cannot_add_comment_to_draft' => 'Vázlathoz nem lehet megjegyzéseket fűzni.',
+    'comment_add' => 'Hiba történt a megjegyzés hozzáadása / frissítése közben.',
+    'comment_delete' => 'Hiba történt a megjegyzés törlése közben.',
+    'empty_comment' => 'Üres megjegyzést nem lehet hozzáadni.',
+
+    // Error pages
+    '404_page_not_found' => 'Oldal nem található',
+    'sorry_page_not_found' => 'Sajnáljuk, a keresett oldal nem található.',
+    'return_home' => 'Vissza a kezdőlapra',
+    'error_occurred' => 'Hiba örtént',
+    'app_down' => ':appName jelenleg nem üzemel',
+    'back_soon' => 'Hamarosan újra elérhető lesz.',
+
+];
diff --git a/resources/lang/hu/pagination.php b/resources/lang/hu/pagination.php
new file mode 100644 (file)
index 0000000..87be04c
--- /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; Előző',
+    'next'     => 'Következő &raquo;',
+
+];
diff --git a/resources/lang/hu/passwords.php b/resources/lang/hu/passwords.php
new file mode 100644 (file)
index 0000000..bacc08b
--- /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' => 'A jelszónak legalább hat karakterből kell állnia és egyeznie kell a megerősítéssel.',
+    'user' => "Nem található felhasználó ezzel az e-mail címmel.",
+    'token' => 'Ez a jelszó visszaállító vezérjel érvénytelen.',
+    'sent' => 'E-mailben elküldtük a jelszó visszaállító hivatkozást!',
+    'reset' => 'A jelszó visszaállítva!',
+
+];
diff --git a/resources/lang/hu/settings.php b/resources/lang/hu/settings.php
new file mode 100644 (file)
index 0000000..efebb4a
--- /dev/null
@@ -0,0 +1,161 @@
+<?php
+/**
+ * Settings text strings
+ * Contains all text strings used in the general settings sections of BookStack
+ * including users and roles.
+ */
+return [
+
+    // Common Messages
+    'settings' => 'Beállítások',
+    'settings_save' => 'Beállítások mentése',
+    'settings_save_success' => 'Beállítások elmentve',
+
+    // App Settings
+    'app_customization' => 'Személyre szabás',
+    'app_features_security' => 'Jellemzők és biztonság',
+    'app_name' => 'Alkalmazás neve',
+    'app_name_desc' => 'Ez a név meg fog jelenni a fejlécben és minden a rendszer által küldött emailben.',
+    'app_name_header' => 'Név mutatása a fejlécben',
+    'app_public_access' => 'Nyilvános hozzáférés',
+    'app_public_access_desc' => 'Ha engedélyezett, a nem bejelentkezett felhasználók is hozzá tudnak férni a BookStack példány tartalmaihoz.',
+    'app_public_access_desc_guest' => 'A nyilvános látogatók hozzáférése a "Guest" felhasználón keresztül irányítható.',
+    'app_public_access_toggle' => 'Nyilvános hozzáférés engedélyezése',
+    'app_public_viewing' => 'Nyilvános megtekintés engedélyezve?',
+    'app_secure_images' => 'Magasabb biztonságú képfeltöltés',
+    'app_secure_images_toggle' => 'Magasabb biztonságú képfeltöltés engedélyezése',
+    'app_secure_images_desc' => 'Teljesítmény optimalizálási okokból minden kép nyilvános. Ez a beállítás egy véletlenszerű, nehezen kitalálható karakterláncot illeszt a képek útvonalának elejére. Meg kell győződni róla, hogy a könnyű hozzáférés megakadályozása érdekében a könyvtár indexek nincsenek engedélyezve.',
+    'app_editor' => 'Oldalszerkesztő',
+    'app_editor_desc' => 'Annak kiválasztása, hogy a felhasználók melyik szerkesztőt használhatják az oldalak szerkesztéséhez.',
+    'app_custom_html' => 'Egyéni HTML fejléc tartalom',
+    'app_custom_html_desc' => 'Az itt hozzáadott bármilyen tartalom be lesz illesztve minden oldal <head> szekciójának aljára. Ez hasznos a stílusok felülírásához van analitikai kódok hozzáadásához.',
+    'app_logo' => 'Alkalmazás logó',
+    'app_logo_desc' => 'A képnek 43px magasnak kell lennie.<br>A nagy képek át lesznek méretezve.',
+    'app_primary_color' => 'Alkalmazás elsődleges színe',
+    'app_primary_color_desc' => 'Hexadecimális értéknek kell lennie.<br>Az alapértelmezés szerinti szín visszaállításához üresen kell hagyni.',
+    'app_homepage' => 'Alkalmazás kezdőlapja',
+    'app_homepage_desc' => 'A kezdőlapon az alapértelmezés szerinti nézet helyett megjelenő nézet kiválasztása. A kiválasztott oldalakon figyelmen kívül lesznek hagyva az oldal engedélyek.',
+    'app_homepage_select' => 'Egy oldal kiválasztása',
+    'app_disable_comments' => 'Megjegyzések letiltása',
+    'app_disable_comments_toggle' => 'Megjegyzések letiltása',
+    'app_disable_comments_desc' => 'Megjegyzések letiltása az alkalmazás összes oldalán.<br>A már létező megjegyzések el lesznek rejtve.',
+
+    // Registration Settings
+    'reg_settings' => 'Regisztráció',
+    'reg_enable' => 'Regisztráció engedélyezése',
+    'reg_enable_toggle' => 'Regisztráció engedélyezése',
+    'reg_enable_desc' => 'Ha a regisztráció engedélyezett, akkor a felhasználó képes lesz bejelentkezni mint az alkalmazás egy felhasználója. Regisztráció után egy egyszerű, alapértelmezés szerinti felhasználói szerepkör lesz hozzárendelve.',
+    'reg_default_role' => 'Regisztráció utáni alapértelmezett felhasználói szerepkör',
+    'reg_email_confirmation' => 'Email megerősítés',
+    'reg_email_confirmation_toggle' => 'Email megerősítés szükséges',
+    'reg_confirm_email_desc' => 'Ha a tartomány korlátozás be van állítva, akkor email megerősítés szükséges és ez a beállítás figyelmen kívül lesz hagyva.',
+    'reg_confirm_restrict_domain' => 'Tartomány korlátozás',
+    'reg_confirm_restrict_domain_desc' => 'Azoknak az email tartományoknak a vesszővel elválasztott listája, melyekre a regisztráció korlátozva lesz. A felhasználók egy emailt fognak kapni, hogy megerősítsék az email címüket mielőtt használni kezdhetnék az alkalmazást.<br>Fontos tudni, hogy a felhasználók a sikeres regisztráció után megváltoztathatják az email címüket.',
+    'reg_confirm_restrict_domain_placeholder' => 'Nincs beállítva korlátozás',
+
+    // Maintenance settings
+    'maint' => 'Karbantartás',
+    'maint_image_cleanup' => 'Képek tisztítása',
+    'maint_image_cleanup_desc' => "Végigolvassa az oldalakat és a tartalmak változatait, hogy leellenőrizze jelenleg mely képek és rajzok vannak használatban, és mely képek szerepelnek többször. A futtatása előtt feltétlen készíteni kell egy teljes adatbázis és lemezkép mentést.",
+    'maint_image_cleanup_ignore_revisions' => 'Képek figyelmen kívül hagyása a változatokban',
+    'maint_image_cleanup_run' => 'Tisztítás futtatása',
+    'maint_image_cleanup_warning' => ':count potenciálisan nem használt képet találtam. Biztosan törölhetőek ezek a képek?',
+    'maint_image_cleanup_success' => ':count potenciálisan nem használt kép megtalálva és törölve!',
+    'maint_image_cleanup_nothing_found' => 'Nincsenek nem használt képek, semmi sem lett törölve!',
+
+    // Role Settings
+    'roles' => 'Szerepkörök',
+    'role_user_roles' => 'Felhasználói szerepkörök',
+    'role_create' => 'Új szerepkör létrehozása',
+    'role_create_success' => 'Szerepkör sikeresen létrehozva',
+    'role_delete' => 'Szerepkör törlése',
+    'role_delete_confirm' => 'Ez törölni fogja \':roleName\' szerepkört.',
+    'role_delete_users_assigned' => 'Ehhez a szerepkörhöz :userCount felhasználó van hozzárendelve. Ha a felhasználókat át kell helyezni ebből a szerepkörből, akkor ki kell választani egy új szerepkört.',
+    'role_delete_no_migration' => "Nincs felhasználó áthelyezés",
+    'role_delete_sure' => 'Biztosan törölhető ez a szerepkör?',
+    'role_delete_success' => 'Szerepkör sikeresen törölve',
+    'role_edit' => 'Szerepkör szerkesztése',
+    'role_details' => 'Szerepkör részletei',
+    'role_name' => 'Szerepkör neve',
+    'role_desc' => 'Szerepkör rövid leírása',
+    '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',
+    'role_manage_roles' => 'Szerepkörök és szerepkör engedélyek kezelése',
+    'role_manage_entity_permissions' => 'Minden könyv, fejezet és oldalengedély kezelése',
+    'role_manage_own_entity_permissions' => 'Saját könyv, fejezet és oldalak engedélyeinek kezelése',
+    'role_manage_settings' => 'Alkalmazás beállításainak kezelése',
+    'role_asset' => 'Eszköz jogosultságok',
+    '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.',
+    'role_asset_admins' => 'Az adminisztrátorok automatikusan hozzáférést kapnak minden tartalomhoz, de ezek a beállítások megjeleníthetnek vagy elrejthetnek felhasználói felület beállításokat.',
+    'role_all' => 'Összes',
+    'role_own' => 'Saját',
+    'role_controlled_by_asset' => 'Az általuk feltöltött eszköz által ellenőrzött',
+    'role_save' => 'Szerepkör mentése',
+    'role_update_success' => 'Szerepkör sikeresen frissítve',
+    'role_users' => 'Felhasználók ebben a szerepkörben',
+    'role_users_none' => 'Jelenleg nincsenek felhasználók hozzárendelve ehhez a szerepkörhöz',
+
+    // Users
+    'users' => 'Felhasználók',
+    'user_profile' => 'Felhasználói profil',
+    'users_add_new' => 'Új felhasználó hozzáadása',
+    'users_search' => 'Felhasználók keresése',
+    'users_details' => 'Felhasználó részletei',
+    'users_details_desc' => 'Egy megjelenítendő név és email cím beállítása ennek a felhasználónak. Az email cím az alkalmazásba történő bejelentkezéshez lesz használva.',
+    'users_details_desc_no_email' => 'Egy megjelenítendő név beállítása ennek a felhasználónak amiről mások felismerik.',
+    'users_role' => 'Felhasználói szerepkörök',
+    'users_role_desc' => 'A felhasználó melyik szerepkörhöz lesz rendelve. Ha a felhasználó több szerepkörhöz van rendelve, akkor ezeknek a szerepköröknek a jogosultságai összeadódnak, és a a felhasználó a hozzárendelt szerepkörök minden képességét megkapja.',
+    'users_password' => 'Felhasználó jelszava',
+    'users_password_desc' => 'Az alkalmazásba bejelentkezéshez használható jelszó beállítása. Legalább 5 karakter hosszúnak kell lennie.',
+    'users_external_auth_id' => 'Külső hitelesítés azonosítója',
+    'users_external_auth_id_desc' => 'Ez az azonosító lesz használva a felhasználó ellenőrzéséhez mikor az LDAP rendszerrel kommunikál.',
+    'users_password_warning' => 'A lenti mezőket csak a jelszó módosításához kell kitölteni.',
+    'users_system_public' => 'Ez a felhasználó bármelyik, a példányt megtekintő felhasználót képviseli. Nem lehet vele bejelentkezni de automatikusan hozzá lesz rendelve.',
+    'users_delete' => 'Felhasználó törlése',
+    'users_delete_named' => ':userName felhasználó törlése',
+    'users_delete_warning' => '\':userName\' felhasználó teljesen törölve lesz a rendszerből.',
+    'users_delete_confirm' => 'Biztosan törölhető ez a felhasználó?',
+    'users_delete_success' => 'Felhasználó sikeresen eltávolítva',
+    'users_edit' => 'Felhasználó szerkesztése',
+    'users_edit_profile' => 'Profil szerkesztése',
+    'users_edit_success' => 'Felhasználó sikeresen frissítve',
+    'users_avatar' => 'Avatar használata',
+    'users_avatar_desc' => 'A felhasználót ábrázoló kép kiválasztása. Kb. 256px méretű négyzetes képnek kell lennie.',
+    'users_preferred_language' => 'Előnyben részesített nyelv',
+    'users_preferred_language_desc' => 'Ez a beállítás megváltoztatja az alkalmazás felhasználói felületén használt nyelvet. Nincs hatása a felhasználók által létrehozott tartalomra.',
+    'users_social_accounts' => 'Közösségi fiókok',
+    'users_social_accounts_info' => 'Itt lehet egyéb fiókokat hozzákapcsolni a gyorsabb és könnyebb bejelentkezés érdekében. Itt olyan fiókot lehet lecsatlakoztatni, melynek korábban nem volt engedélyezett hozzáférése. Visszavonja a hozzáférést a csatlakoztatott szociális fiók profilbeállításaiból.',
+    'users_social_connect' => 'Fiók csatlakoztatása',
+    'users_social_disconnect' => 'Fiók lecsatlakoztatása',
+    'users_social_connected' => ':socialAccount fiók sikeresen csatlakoztatva a profilhoz.',
+    'users_social_disconnected' => ':socialAccount fiók sikeresen lecsatlakoztatva a profilról.',
+
+    //! Since these labels are already localized this array does not need to be
+    //! translated in the language-specific files.
+    //! DELETE BELOW IF COPIED FROM EN
+    //!////////////////////////////////
+    'language_select' => [
+        'en' => 'English',
+        'ar' => 'العربية',
+        'de' => 'Deutsch (Sie)',
+        'de_informal' => 'Deutsch (Du)',
+        'es' => 'Español',
+        'es_AR' => 'Español Argentina',
+        'fr' => 'Français',
+        'nl' => 'Nederlands',
+        'pt_BR' => 'Português do Brasil',
+        'sk' => 'Slovensky',
+        'cs' => 'Česky',
+        'sv' => 'Svenska',
+        'kr' => '한국어',
+        'ja' => '日本語',
+        'pl' => 'Polski',
+        'it' => 'Italian',
+        'ru' => 'Русский',
+        'uk' => 'Українська',
+        'zh_CN' => '简体中文',
+        'zh_TW' => '繁體中文'
+    ]
+    //!////////////////////////////////
+];
diff --git a/resources/lang/hu/validation.php b/resources/lang/hu/validation.php
new file mode 100644 (file)
index 0000000..68a4446
--- /dev/null
@@ -0,0 +1,85 @@
+<?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 elfogadott kell legyen.',
+    'active_url'           => ':attribute nem érvényes webcím.',
+    'after'                => ':attribute dátumnak :date utáninak kell lennie.',
+    'alpha'                => ':attribute csak betűket tartalmazhat.',
+    '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.',
+    'before'               => ':attribute dátumnak :date előttinek kell lennie.',
+    'between'              => [
+        'numeric' => ':attribute értékének :min és :max között kell lennie.',
+        'file'    => ':attribute értékének :min és :max kilobájt között kell lennie.',
+        'string'  => ':attribute hosszának :min és :max karakter között kell lennie.',
+        'array'   => ':attribute mennyiségének :min és :max elem között kell lennie.',
+    ],
+    'boolean'              => ':attribute mezőnek igaznak vagy hamisnak kell lennie.',
+    'confirmed'            => ':attribute megerősítés nem egyezik.',
+    'date'                 => ':attribute nem érvényes dátum.',
+    'date_format'          => ':attribute nem egyezik :format formátummal.',
+    'different'            => ':attribute és :other értékének különböznie kell.',
+    'digits'               => ':attribute :digits számból kell álljon.',
+    'digits_between'       => ':attribute hosszának :min és :max számjegy között kell lennie.',
+    'email'                => ':attribute érvényes email cím kell legyen.',
+    'filled'               => ':attribute mező kötelező.',
+    'exists'               => 'A kiválasztott :attribute érvénytelen.',
+    'image'                => ':attribute kép kell legyen.',
+    'image_extension'      => 'A :attribute kép kiterjesztése érvényes és támogatott kell legyen.',
+    'in'                   => 'A kiválasztott :attribute érvénytelen.',
+    'integer'              => ':attribute egész szám kell legyen.',
+    'ip'                   => ':attribute érvényes IP cím kell legyen.',
+    'max'                  => [
+        'numeric' => ':attribute nem lehet nagyobb mint :max.',
+        'file'    => ':attribute nem lehet nagyobb mint :max kilobájt.',
+        'string'  => ':attribute nem lehet nagyobb mint :max karakter.',
+        'array'   => ':attribute mennyisége nem lehet több mint :max elem.',
+    ],
+    'mimes'                => 'A :attribute típusa csak :values lehet.',
+    'min'                  => [
+        'numeric' => ':attribute legalább :min kell legyen.',
+        'file'    => ':attribute legalább :min kilobájt kell legyen.',
+        'string'  => ':attribute legalább :min karakter kell legyen.',
+        'array'   => ':attribute legalább :min elem kell legyen.',
+    ],
+    'no_double_extension'  => ':attribute csak egy fájlkiterjesztéssel rendelkezhet.',
+    'not_in'               => 'A kiválasztott :attribute érvénytelen.',
+    'numeric'              => ':attribute szám kell legyen.',
+    'regex'                => ':attribute formátuma érvénytelen.',
+    'required'             => ':attribute mező kötelező.',
+    'required_if'          => ':attribute mező kötelező ha :other értéke :value.',
+    'required_with'        => ':attribute mező kötelező ha :values be van állítva.',
+    'required_with_all'    => ':attribute mező kötelező ha van :value.',
+    'required_without'     => ':attribute mező kötelező ha :values nincs beállítva.',
+    'required_without_all' => ':attribute mező kötelező ha egyik :values sincs beállítva.',
+    'same'                 => ':attribute és :other értékének egyeznie kell.',
+    'size'                 => [
+        'numeric' => ':attribute :size méretű kell legyen.',
+        'file'    => ':attribute :size kilobájt méretű kell legyen.',
+        'string'  => ':attribute :size karakter kell legyen.',
+        'array'   => ':attribute : size elemet kell tartalmazzon.',
+    ],
+    'string'               => ':attribute karaktersorozatnak kell legyen.',
+    'timezone'             => ':attribute érvényes zóna kell legyen.',
+    '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.',
+
+    // Custom validation lines
+    'custom' => [
+        'password-confirm' => [
+            'required_with' => 'Jelszó megerősítés szükséges',
+        ],
+    ],
+
+    // Custom validation attributes
+    'attributes' => [],
+];
index 68fee41a56496436aeadbf0a4fba8cab4dd779b0..59af1fe0d28b46ad356002ef3ce1b10c580914c9 100755 (executable)
@@ -27,7 +27,7 @@ return [
     'email' => 'Email',
     'password' => 'Password',
     'password_confirm' => 'Conferma Password',
-    'password_hint' => 'Deve essere più di 5 caratteri',
+    'password_hint' => 'Deve essere più di 7 caratteri',
     'forgot_password' => 'Password dimenticata?',
     'remember_me' => 'Ricordami',
     'ldap_email_hint' => 'Inserisci un email per usare quest\'account.',
index 4d5aee8b38b921b3d39857c18a2c1bb5eecd8ab7..fdfac5f39eac1a77a3c48fb60870a748648a12e5 100644 (file)
@@ -27,7 +27,7 @@ return [
     'email' => 'メールアドレス',
     'password' => 'パスワード',
     'password_confirm' => 'パスワード (確認)',
-    'password_hint' => '5文字以上である必要があります',
+    'password_hint' => '7文字以上である必要があります',
     'forgot_password' => 'パスワードをお忘れですか?',
     'remember_me' => 'ログイン情報を保存する',
     'ldap_email_hint' => 'このアカウントで使用するEメールアドレスを入力してください。',
index 671ddc654109ee6c06732f679d34a2960fa7bd87..36534f0d4fca1018243c08574fed45c6697f061c 100644 (file)
@@ -27,7 +27,7 @@ return [
     'email' => '이메일',
     'password' => '비밀번호',
     'password_confirm' => '비밀번호 (확인)',
-    'password_hint' => '5자 이상이어야 합니다.',
+    'password_hint' => '7자 이상이어야 합니다.',
     'forgot_password' => '비밀번호를 잊으셨습니까?',
     'remember_me' => '자동로그인',
     'ldap_email_hint' => '이 계정에서 사용하는 이메일을 입력해 주세요.',
index 31bd330cc3d5ae28acb9663883cfce9f775d3800..30dfdd78df7064b5e38b4e796a6dbd3792148543 100644 (file)
@@ -27,7 +27,7 @@ return [
     'email' => 'Email',
     'password' => 'Wachtwoord',
     'password_confirm' => 'Wachtwoord Bevestigen',
-    'password_hint' => 'Minimaal 6 tekens',
+    'password_hint' => 'Minimaal 8 tekens',
     'forgot_password' => 'Wachtwoord vergeten?',
     'remember_me' => 'Mij onthouden',
     'ldap_email_hint' => 'Geef een email op waarmee je dit account wilt gebruiken.',
index 5cec651a9aeafec5ef73231990ae2cb081feac05..40c458c618d621207163d1bcbcfd7c16cd07f1e5 100644 (file)
@@ -27,7 +27,7 @@ return [
     'email' => 'E-mail',
     'password' => 'Hasło',
     'password_confirm' => 'Potwierdzenie hasła',
-    'password_hint' => 'Musi mieć więcej niż 5 znaków',
+    'password_hint' => 'Musi mieć więcej niż 7 znaków',
     'forgot_password' => 'Zapomniałem hasła',
     'remember_me' => 'Zapamiętaj mnie',
     'ldap_email_hint' => 'Wprowadź adres e-mail dla tego konta.',
index 03ff80c9fc4e1dc09c4c57ab4a4022da3cbb2d94..91417cc8b97f08e31bcb466552c674a811533a0d 100644 (file)
@@ -1,12 +1,10 @@
 <?php
-
+/**
+ * Activity text strings.
+ * Is used for all the text within activity logs & notifications.
+ */
 return [
-
-    /**
-     * Activity text strings.
-     * Is used for all the text within activity logs & notifications.
-     */
-
+    
     // Pages
     'page_create'                 => 'página criada',
     'page_create_notification'    => 'Página criada com sucesso',
index 73228f21a2c2afd3db439c261bf0faf8889b3d73..79f743617d625624685424a12e8d30182dc06f02 100644 (file)
@@ -1,21 +1,15 @@
 <?php
+/**
+ * Authentication Language Lines
+ * The following language lines are used during authentication for various
+ * messages that we need to display to the user.
+ */
 return [
-    /*
-    |--------------------------------------------------------------------------
-    | Authentication Language Lines
-    |--------------------------------------------------------------------------
-    |
-    | The following language lines are used during authentication for various
-    | messages that we need to display to the user. You are free to modify
-    | these language lines according to your application's requirements.
-    |
-    */
+
     'failed' => 'As credenciais fornecidas não puderam ser validadas em nossos registros..',
     'throttle' => 'Muitas tentativas de login. Por favor, tente novamente em :seconds segundos.',
 
-    /**
-     * Login & Register
-     */
+    // Login & Register
     'sign_up' => 'Registrar-se',
     'log_in' => 'Entrar',
     'log_in_with' => 'Entrar com :socialDriver',
@@ -27,11 +21,13 @@ return [
     'email' => 'E-mail',
     'password' => 'Senha',
     'password_confirm' => 'Confirmar Senha',
-    'password_hint' => 'Senha deverá ser maior que 5 caracteres',
+    'password_hint' => 'Senha deverá ser maior que 7 caracteres',
     'forgot_password' => 'Esqueceu a senha?',
     'remember_me' => 'Lembrar de mim',
     'ldap_email_hint' => 'Por favor, digite um e-mail para essa conta.',
     'create_account' => 'Criar conta',
+    'already_have_account' => 'Você já possui uma conta?',
+    'dont_have_account' => 'Não possui uma conta?',
     'social_login' => 'Login social',
     'social_registration' => 'Registro social',
     'social_registration_text' => 'Registre e entre usando outro serviço.',
@@ -43,23 +39,18 @@ return [
     'register_success' => 'Obrigado por se registrar! Você agora encontra-se registrado e logado..',
 
 
-    /**
-     * Password Reset
-     */
+    // Password Reset
     'reset_password' => 'Resetar senha',
     'reset_password_send_instructions' => 'Digite seu e-mail abaixo e o sistema enviará uma mensagem com o link de reset de senha.',
     'reset_password_send_button' => 'Enviar o link de reset de senha',
     'reset_password_sent_success' => 'Um link de reset de senha foi enviado para :email.',
     'reset_password_success' => 'Sua senha foi resetada com sucesso.',
-
     'email_reset_subject' => 'Resetar a senha de :appName',
     'email_reset_text' => 'Você recebeu esse e-mail pois recebemos uma solicitação de reset de senha para sua conta.',
     'email_reset_not_requested' => 'Caso não tenha sido você a solicitar o reset de senha, ignore esse e-mail.',
 
 
-    /**
-     * Email Confirmation
-     */
+    // Email Confirmation
     'email_confirm_subject' => 'Confirme seu e-mail para :appName',
     'email_confirm_greeting' => 'Obrigado por se registrar em :appName!',
     'email_confirm_text' => 'Por favor, confirme seu endereço de e-mail clicando no botão abaixo:',
index 0741541eb01fb58297aa282ff7467aa2438cd816..c6750a9540e47770dfdf3f926fd696b4496c1e60 100644 (file)
@@ -1,31 +1,30 @@
 <?php
+/**
+ * Common elements found throughout many areas of BookStack.
+ */
 return [
 
-    /**
-     * Buttons
-     */
+    // Buttons
     'cancel' => 'Cancelar',
     'confirm' => 'Confirmar',
     'back' => 'Voltar',
     'save' => 'Salvar',
     'continue' => 'Continuar',
     'select' => 'Selecionar',
+    'toggle_all' => 'Alternar Tudo',
     'more' => 'Mais',
 
-    /**
-     * Form Labels
-     */
+    // Form Labels
     'name' => 'Nome',
     'description' => 'Descrição',
     'role' => 'Regra',
     'cover_image' => 'Imagem de capa',
     'cover_image_description' => 'Esta imagem deve ser aproximadamente 300x170px.',
     
-    /**
-     * Actions
-     */
+    // Actions
     'actions' => 'Ações',
     'view' => 'Visualizar',
+    'view_all' => 'Ver Tudo',
     'create' => 'Criar',
     'update' => 'Atualizar',
     'edit' => 'Editar',
@@ -40,9 +39,12 @@ return [
     'remove' => 'Remover',
     'add' => 'Adicionar',
 
-    /**
-     * Misc
-     */
+    // Sort Options
+    'sort_name' => 'Nome',
+    'sort_created_at' => 'Data de Criação',
+    'sort_updated_at' => 'Data de Atualização',
+
+    // Misc
     'deleted_user' => 'Usuário excluído',
     'no_activity' => 'Nenhuma atividade a mostrar',
     'no_items' => 'Nenhum item disponível',
@@ -54,15 +56,15 @@ return [
     'list_view' => 'Visualização em Lista',
     'default' => 'Padrão',
 
-    /**
-     * Header
-     */
+    // Header
     'view_profile' => 'Visualizar Perfil',
     'edit_profile' => 'Editar Perfil',
 
-    /**
-     * Email Content
-     */
+    // Layout tabs
+    'tab_info' => 'Info',
+    'tab_content' => 'Conteúdo',
+
+    // Email Content
     'email_action_help' => 'Se você estiver tendo problemas ao clicar o botão ":actionText", copie e cole a URL abaixo no seu navegador:',
     'email_rights' => 'Todos os direitos reservados',
 ];
\ No newline at end of file
index 4ea4d88c50523b9c2e4c9f314f22a1421a6c68a5..b9f1c3a38b456219e4be5f166b0e72dbe31e111c 100644 (file)
@@ -1,9 +1,10 @@
 <?php
+/**
+ * Text used in custom JavaScript driven components.
+ */
 return [
 
-    /**
-     * Image Manager
-     */
+    // Image Manager
     'image_select' => 'Selecionar imagem',
     'image_all' => 'Todos',
     'image_all_title' => 'Visualizar todas as imagens',
@@ -24,9 +25,7 @@ return [
     'image_delete_success' => 'Imagem excluída com sucesso',
     'image_upload_remove' => 'Remover',
 
-    /**
-     * Code editor
-     */
+    // Code editor
     'code_editor' => 'Editar Código',
     'code_language' => 'Linguagem do Código',
     'code_content' => 'Código',
index 9e6678146a3e79e8f96e7e6ecef239f080b0e1b1..7ce5ef01ef89fc24d977bf6dc67a75cf9244ae98 100644 (file)
@@ -1,17 +1,20 @@
 <?php
+/**
+ * Text used for 'Entities' (Document Structure Elements) such as
+ * Books, Shelves, Chapters & Pages
+ */
 return [
 
-    /**
-     * Shared
-     */
-    'recently_created' => 'Recentemente criado',
-    'recently_created_pages' => 'Páginas recentemente criadas',
-    'recently_updated_pages' => 'Páginas recentemente atualizadas',
-    'recently_created_chapters' => 'Capítulos recentemente criados',
-    'recently_created_books' => 'Livros recentemente criados',
-    'recently_update' => 'Recentemente atualizado',
-    'recently_viewed' => 'Recentemente visualizado',
-    'recent_activity' => 'Atividade recente',
+    // Shared
+    'recently_created' => 'Recentemente Criado',
+    'recently_created_pages' => 'Páginas Recentemente Criadas',
+    'recently_updated_pages' => 'Páginas Recentemente Atualizadas',
+    'recently_created_chapters' => 'Capítulos Recentemente Criados',
+    'recently_created_books' => 'Livros Recentemente Criados',
+    'recently_created_shelves' => 'Prateleiras Recentemente Criadas',
+    'recently_update' => 'Recentemente Atualizado',
+    'recently_viewed' => 'Recentemente Visualizado',
+    'recent_activity' => 'Atividade Recente',
     'create_now' => 'Criar um agora',
     'revisions' => 'Revisões',
     'meta_revision' => 'Revisão #:revisionCount',
@@ -31,17 +34,13 @@ return [
     'export_pdf' => 'Arquivo PDF',
     'export_text' => 'Arquivo Texto',
 
-    /**
-     * Permissions and restrictions
-     */
+    // Permissions and restrictions
     'permissions' => 'Permissões',
     'permissions_intro' => 'Uma vez habilitado, as permissões terão prioridade sobre outro conjunto de permissões.',
     'permissions_enable' => 'Habilitar Permissões Customizadas',
     'permissions_save' => 'Salvar Permissões',
 
-    /**
-     * Search
-     */
+    // Search
     'search_results' => 'Resultado(s) da Pesquisa',
     'search_total_results_found' => ':count resultado encontrado|:count resultados encontrados',
     'search_clear' => 'Limpar Pesquisa',
@@ -66,16 +65,16 @@ return [
     'search_set_date' => 'Definir data',
     'search_update' => 'Refazer Pesquisa',
 
-    /**
-     * Shelves
-     */
+    // Shelves
     'shelf' => 'Prateleira',
     'shelves' => 'Prateleiras',
+    'x_shelves' => ':count Prateleira|:count Prateleiras',
     'shelves_long' => 'Prateleiras de Livros',
     'shelves_empty' => 'Nenhuma prateleira foi criada',
-    'shelves_create' => 'Criar nova Prateleira',
-    'shelves_popular' => 'Prateleiras populares',
-    'shelves_new' => 'Prateleiras novas',
+    'shelves_create' => 'Criar Nova Prateleira',
+    'shelves_popular' => 'Prateleiras Populares',
+    'shelves_new' => 'Prateleiras Novas',
+    'shelves_new_action' => 'Nova Prateleira',
     'shelves_popular_empty' => 'As prateleiras mais populares aparecerão aqui.',
     'shelves_new_empty' => 'As prateleiras criadas mais recentemente aparecerão aqui.',
     'shelves_save' => 'Salvar Prateleira',
@@ -98,16 +97,15 @@ return [
     'shelves_copy_permissions_explain' => 'Isto aplicará as configurações de permissões atuais desta prateleira de livros a todos os livros contidos nela. Antes de ativar, assegure-se de que quaisquer alterações nas permissões desta prateleira de livros tenham sido salvas.',
     'shelves_copy_permission_success' => 'Permissões da prateleira de livros copiada para :count livros',
 
-    /**
-     * Books
-     */
+    // Books
     'book' => 'Livro',
     'books' => 'Livros',
     'x_books' => ':count Livro|:count Livros',
     'books_empty' => 'Nenhum livro foi criado',
-    'books_popular' => 'Livros populares',
-    'books_recent' => 'Livros recentes',
-    'books_new' => 'Livros novos',
+    'books_popular' => 'Livros Populares',
+    'books_recent' => 'Livros Recentes',
+    'books_new' => 'Livros Novos',
+    'books_new_action' => 'Novo Livro',
     'books_popular_empty' => 'Os livros mais populares aparecerão aqui.',
     'books_new_empty' => 'Os livros criados mais recentemente aparecerão aqui.',
     'books_create' => 'Criar novo Livro',
@@ -123,45 +121,45 @@ return [
     'books_permissions_updated' => 'Permissões do Livro Atualizadas',
     'books_empty_contents' => 'Nenhuma página ou capítulo criado para esse livro.',
     'books_empty_create_page' => 'Criar uma nova página',
-    'books_empty_or' => 'ou',
     'books_empty_sort_current_book' => 'Ordenar o livro atual',
     'books_empty_add_chapter' => 'Adicionar um capítulo',
-    'books_permissions_active' => 'Permissões do Livro ativadas',
+    'books_permissions_active' => 'Permissões do Livro Ativadas',
     'books_search_this' => 'Pesquisar esse livro',
     'books_navigation' => 'Navegação do Livro',
-    'books_sort' => 'Ordenar conteúdos do Livro',
+    'books_sort' => 'Ordenar Conteúdos do Livro',
     'books_sort_named' => 'Ordenar Livro :bookName',
-    'books_sort_show_other' => 'Mostrar outros livros',
-    'books_sort_save' => 'Salvar nova ordenação',
+    'books_sort_name' => 'Ordernar por Nome',
+    'books_sort_created' => 'Ordenar por Data de Criação',
+    'books_sort_updated' => 'Ordenar por Data de Atualização',
+    'books_sort_chapters_first' => 'Capítulos Primeiro',
+    'books_sort_chapters_last' => 'Capítulos por Último',
+    'books_sort_show_other' => 'Mostrar Outros Livros',
+    'books_sort_save' => 'Salvar Nova Ordenação',
 
-    /**
-     * Chapters
-     */
+    // Chapters
     'chapter' => 'Capítulo',
     'chapters' => 'Capítulos',
     'x_chapters' => ':count Capítulo|:count Capítulos',
     'chapters_popular' => 'Capítulos Populares',
     'chapters_new' => 'Novo Capítulo',
-    'chapters_create' => 'Criar novo Capítulo',
+    'chapters_create' => 'Criar Novo Capítulo',
     'chapters_delete' => 'Excluír Capítulo',
     'chapters_delete_named' => 'Excluir Capítulo :chapterName',
     'chapters_delete_explain' => 'A ação vai excluír o capítulo de nome \':chapterName\'. Todas as páginas do capítulo serão removidas e adicionadas diretamente ao livro pai.',
     'chapters_delete_confirm' => 'Tem certeza que deseja excluír o capítulo?',
     'chapters_edit' => 'Editar Capítulo',
-    'chapters_edit_named' => 'Editar capítulo :chapterName',
+    'chapters_edit_named' => 'Editar Capítulo :chapterName',
     'chapters_save' => 'Salvar Capítulo',
     'chapters_move' => 'Mover Capítulo',
     'chapters_move_named' => 'Mover Capítulo :chapterName',
     'chapter_move_success' => 'Capítulo movido para :bookName',
     'chapters_permissions' => 'Permissões do Capítulo',
     'chapters_empty' => 'Nenhuma página existente nesse capítulo.',
-    'chapters_permissions_active' => 'Permissões de Capítulo ativadas',
-    'chapters_permissions_success' => 'Permissões de Capítulo atualizadas',
+    'chapters_permissions_active' => 'Permissões de Capítulo Ativadas',
+    'chapters_permissions_success' => 'Permissões de Capítulo Atualizadas',
     'chapters_search_this' => 'Pesquisar este Capítulo',
 
-    /**
-     * Pages
-     */
+    // Pages
     'page' => 'Página',
     'pages' => 'Páginas',
     'x_pages' => ':count Página|:count Páginas',
@@ -178,7 +176,6 @@ return [
     'pages_delete_confirm' => 'Tem certeza que deseja excluir a página?',
     'pages_delete_draft_confirm' => 'Tem certeza que deseja excluir o rascunho de página?',
     'pages_editing_named' => 'Editando a Página :pageName',
-    'pages_edit_toggle_header' => 'Alternar cabeçalho',
     'pages_edit_save_draft' => 'Salvar Rascunho',
     'pages_edit_draft' => 'Editar rascunho de Página',
     'pages_editing_draft' => 'Editando Rascunho',
@@ -212,7 +209,9 @@ return [
     'pages_revisions_created_by' => 'Criado por',
     'pages_revisions_date' => 'Data da Revisão',
     'pages_revisions_number' => '#',
+    'pages_revisions_numbered' => 'Revisão #:id',
     'pages_revisions_changelog' => 'Changelog',
+    'pages_revisions_numbered_changes' => 'Alterações da Revisão #:id',
     'pages_revisions_changes' => 'Mudanças',
     'pages_revisions_current' => 'Versão atual',
     'pages_revisions_preview' => 'Preview',
@@ -235,9 +234,7 @@ return [
     'pages_draft_discarded' => 'Rascunho descartado. O editor foi atualizado com a página atualizada',
     'pages_specific' => 'Página Específica',
 
-    /**
-     * Editor sidebar
-     */
+    // Editor sidebar
     'page_tags' => 'Tags de Página',
     'chapter_tags' => 'Tags de Capítulo',
     'book_tags' => 'Tags de Livro',
@@ -273,18 +270,15 @@ return [
     'attachments_file_updated' => 'Arquivo atualizado com sucesso',
     'attachments_link_attached' => 'Link anexado com sucesso à página',
 
-    /**
-     * Profile View
-     */
+    // Profile View
     'profile_user_for_x' => 'Usuário por :time',
     'profile_created_content' => 'Conteúdo Criado',
     'profile_not_created_pages' => ':userName não criou páginas',
     'profile_not_created_chapters' => ':userName não criou capítulos',
     'profile_not_created_books' => ':userName não criou livros',
+    'profile_not_created_shelves' => ':userName não criou prateleiras',
 
-    /**
-     * Comments
-     */
+    // Comments
     'comment' => 'Comentário',
     'comments' => 'Comentários',
     'comment_add' => 'Adicionar Comentário',
@@ -302,10 +296,9 @@ return [
     'comment_delete_confirm' => 'Você tem certeza de que quer deletar este comentário?',
     'comment_in_reply_to' => 'Em resposta à :commentId',
 
-    /**
-     * Revision
-     */
+    // Revision
     'revision_delete_confirm' => 'Tem certeza de que deseja excluir esta revisão?',
+    'revision_restore_confirm' => 'Tem certeza que deseja restaurar esta revisão? O conteúdo atual da página será substituído.',
     'revision_delete_success' => 'Revisão excluída',
     'revision_cannot_delete_latest' => 'Não é possível excluir a revisão mais recente.'
 ];
\ No newline at end of file
index 023254182a2660cc7b87b48d91e749fa4c2f6691..c5b1a9f9213a24c21e73e0d53d86d59dd4aedc06 100644 (file)
@@ -1,11 +1,9 @@
 <?php
-
+/**
+ * Text shown in error messaging.
+ */
 return [
 
-    /**
-     * Error text strings.
-     */
-
     // Permissions
     'permission' => 'Você não tem permissões para acessar a página requerida.',
     'permissionJson' => 'Você não tem permissão para realizar a ação requerida.',
@@ -66,6 +64,7 @@ return [
     'role_cannot_be_edited' => 'Esse perfil não pode ser editado',
     'role_system_cannot_be_deleted' => 'Esse perfil é um perfil de sistema e não pode ser excluído',
     'role_registration_default_cannot_delete' => 'Esse perfil não poderá se excluído enquando estiver registrado como o perfil padrão',
+    'role_cannot_remove_only_admin' => 'Este usuário é o único usuário atribuído ao perfil de administrador. Atribua o perfil de administrador a outro usuário antes de tentar removê-lo aqui.',
 
     // comments
     'comment_list' => 'Ocorreu um erro ao buscar os comentários.',
@@ -81,4 +80,5 @@ return [
     'error_occurred' => 'Um erro ocorreu',
     'app_down' => ':appName está fora do ar no momento',
     'back_soon' => 'Voltaremos em seguida.',
+    
 ];
index 6a32f34ac013545cedb0201b413ed331b7b71d84..3ae5dd3e0f65482f58e21237bd07dcb771b11549 100644 (file)
@@ -1,18 +1,11 @@
 <?php
-
+/**
+ * Pagination Language Lines
+ * The following language lines are used by the paginator library to build
+ * the simple pagination links.
+ */
 return [
 
-    /*
-    |--------------------------------------------------------------------------
-    | Pagination Language Lines
-    |--------------------------------------------------------------------------
-    |
-    | The following language lines are used by the paginator library to build
-    | the simple pagination links. You are free to change them to anything
-    | you want to customize your views to better match your application.
-    |
-    */
-
     'previous' => '&laquo; Anterior',
     'next'     => 'Próximo &raquo;',
 
index f75c24ea5475f5ca80f6143653a74337c6afda72..61a49f57a1f0d240ee5cc28dc0d396cdda5b27c5 100644 (file)
@@ -1,18 +1,11 @@
 <?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 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, such as for an invalid token or invalid new password.
-    |
-    */
-
     'password' => 'Senhas devem ter ao menos 6 caraceres e combinar com os atributos mínimos para a senha.',
     'user' => "Não pudemos encontrar um usuário com o e-mail fornecido.",
     'token' => 'O token de reset de senha é inválido.',
index aab2c2591f77f379241867f18269c38c713a706e..4bb8f37e0752023f2824e07509514b01ec1c7c8e 100644 (file)
@@ -1,32 +1,35 @@
 <?php
-
+/**
+ * Settings text strings
+ * Contains all text strings used in the general settings sections of BookStack
+ * including users and roles.
+ */
 return [
 
-    /**
-     * Settings text strings
-     * Contains all text strings used in the general settings sections of BookStack
-     * including users and roles.
-     */
-
+    // Common Messages
     'settings' => 'Configurações',
     'settings_save' => 'Salvar Configurações',
     'settings_save_success' => 'Configurações Salvas',
 
-    /**
-     * App settings
-     */
-
-    'app_settings' => 'Configurações do App',
+    // App Settings
+    'app_customization' => 'Customização',
+    'app_features_security' => 'Recursos & Segurança',
     'app_name' => 'Nome da Aplicação',
     'app_name_desc' => 'Esse nome será mostrado no cabeçalho e em e-mails.',
     'app_name_header' => 'Mostrar o nome da Aplicação no cabeçalho?',
+    'app_public_access' => 'Acesso Público',
+    'app_public_access_desc' => 'Habilitar esta opção irá permitir que visitantes, que não estão logados, acessem o conteúdo em sua instância do BookStack.',
+    'app_public_access_desc_guest' => 'O acesso de visitantes públicos pode ser controlado através do usuário "Convidado".',
+    'app_public_access_toggle' => 'Permitir acesso público',
     'app_public_viewing' => 'Permitir visualização pública?',
     'app_secure_images' => 'Permitir upload de imagens com maior segurança?',
+    'app_secure_images_toggle' => 'Habilitar uploads de imagem de maior segurança',
     'app_secure_images_desc' => 'Por questões de performance, todas as imagens são públicas. Essa opção adiciona uma string randômica na frente da imagem. Certifique-se de que os índices do diretórios permitem o acesso fácil.',
     'app_editor' => 'Editor de Página',
     'app_editor_desc' => 'Selecione qual editor a ser usado pelos usuários para editar páginas.',
     'app_custom_html' => 'Conteúdo para tag HTML HEAD customizado',
     'app_custom_html_desc' => 'Quaisquer conteúdos aqui inseridos serão inseridos no final da seção <head> do HTML de cada página. Essa é uma maneira útil de sobrescrever estilos e adicionar códigos de análise de site.',
+    'app_custom_html_disabled_notice' => 'O conteúdo personalizado do head do HTML está desabilitado nesta página de configurações para garantir que quaisquer alterações significativas possam ser revertidas.',
     'app_logo' => 'Logo da Aplicação',
     'app_logo_desc' => 'A imagem deve ter 43px de altura. <br>Imagens mais largas devem ser reduzidas.',
     'app_primary_color' => 'Cor primária da Aplicação',
@@ -34,26 +37,24 @@ return [
     'app_homepage' => 'Página incial',
     'app_homepage_desc' => 'Selecione a página para ser usada como página inicial em vez da padrão. Permissões da página serão ignoradas.',
     'app_homepage_select' => 'Selecione uma página',
-    'app_disable_comments' => 'Desativar comentários',
+    'app_disable_comments' => 'Desativar Comentários',
+    'app_disable_comments_toggle' => 'Desativar comentários',
     'app_disable_comments_desc' => 'Desativar comentários em todas as páginas no aplicativo. Os comentários existentes não são exibidos.',
 
-    /**
-     * Registration settings
-     */
-
-    'reg_settings' => 'Parâmetros de Registro',
-    'reg_allow' => 'Permitir Registro?',
+    // Registration settings
+    'reg_settings' => 'Registro',
+    'reg_enable' => 'Habilitar Registro',
+    'reg_enable_toggle' => 'Habilitar registro',
+    'reg_enable_desc' => 'Quando o registro é habilitado, o usuário poderá se registrar como usuário do aplicativo. No registro, eles recebem um único perfil padrão.',
     'reg_default_role' => 'Perfil padrão para usuários após o registro',
-    'reg_confirm_email' => 'Requerer confirmação por e-mail?',
+    'reg_email_confirmation' => 'Confirmação de E-mail',
+    'reg_email_confirmation_toggle' => 'Requer confirmação de e-mail',
     'reg_confirm_email_desc' => 'Se restrições de domínio são usadas a confirmação por e-mail será requerida e o valor abaixo será ignorado.',
     'reg_confirm_restrict_domain' => 'Restringir registro ao domínio',
     'reg_confirm_restrict_domain_desc' => 'Entre com uma lista de domínios de e-mails separados por vírgula para os quais você deseja restringir os registros. Será enviado um e-mail de confirmação para o usuário validar o e-mail antes de ser permitido interação com a aplicação. <br> Note que os usuários serão capazes de alterar o e-mail cadastrado após o sucesso na confirmação do registro.',
     'reg_confirm_restrict_domain_placeholder' => 'Nenhuma restrição configurada',
 
-    /**
-     * Maintenance settings
-     */
-
+    // Maintenance settings
     'maint' => 'Manutenção',
     'maint_image_cleanup' => 'Limpeza de Imagens',
     'maint_image_cleanup_desc' => "Examina páginas & revisa o conteúdo para verificar quais imagens e desenhos estão atualmente em uso e quais imagens são redundantes. Certifique-se de criar um backup completo do banco de dados e imagens antes de executar isso.",
@@ -63,10 +64,7 @@ return [
     'maint_image_cleanup_success' => ':count imagens potencialmente não utilizadas foram encontradas e excluídas!',
     'maint_image_cleanup_nothing_found' => 'Nenhuma imagem não utilizada foi encontrada, nada foi excluído!',
 
-    /**
-     * Role settings
-     */
-
+    // Role settings
     'roles' => 'Perfis',
     'role_user_roles' => 'Perfis de Usuário',
     'role_create' => 'Criar novo Perfil',
@@ -99,16 +97,20 @@ return [
     'role_users' => 'Usuários neste Perfil',
     'role_users_none' => 'Nenhum usuário está atualmente atrelado a esse Perfil',
 
-    /**
-     * Users
-     */
-
+    // Users
     'users' => 'Usuários',
-    'user_profile' => 'Perfil de Usuário',
+    'user_profile' => 'Perfil do Usuário',
     'users_add_new' => 'Adicionar Novo Usuário',
     'users_search' => 'Pesquisar Usuários',
-    'users_role' => 'Perfis de Usuário',
+    'users_details' => 'Detalhes do Usuário',
+    'users_details_desc' => 'Defina um nome de exibição e um endereço de e-mail para este usuário. O endereço de e-mail será usado para fazer login na aplicação.',
+    'users_details_desc_no_email' => 'Defina um nome de exibição para este usuário para que outros usuários possam reconhecê-lo',
+    'users_role' => 'Perfis do Usuário',
+    'users_role_desc' => 'Selecione os perfis para os quais este usuário será atribuído. Se um usuário for atribuído a multiplos perfis, as permissões destes perfis serão empilhadas e eles receberão todas as habilidades dos perfis atribuídos.',
+    'users_password' => 'Senha do Usuário',
+    'users_password_desc' => 'Defina uma senha usada para fazer login na aplicação. Esta deve ter pelo menos 5 caracteres.',
     'users_external_auth_id' => 'ID de Autenticação Externa',
+    'users_external_auth_id_desc' => 'Este é o ID usado para corresponder a este usuário ao se comunicar com seu sistema LDAP.',
     'users_password_warning' => 'Preencha os dados abaixo caso queira modificar a sua senha:',
     'users_system_public' => 'Esse usuário representa quaisquer convidados que visitam o aplicativo. Ele não pode ser usado para login.',
     'users_delete' => 'Excluir Usuário',
@@ -122,12 +124,14 @@ return [
     'users_avatar' => 'Imagem de Usuário',
     'users_avatar_desc' => 'Essa imagem deve ser um quadrado com aproximadamente 256px de altura e largura.',
     'users_preferred_language' => 'Linguagem de Preferência',
+    'users_preferred_language_desc' => 'Esta opção irá alterar o idioma usado para a interface de usuário da aplicação. Isto não afetará nenhum conteúdo criado pelo usuário.',
     'users_social_accounts' => 'Contas Sociais',
     'users_social_accounts_info' => 'Aqui você pode conectar outras contas para acesso mais rápido. Desconectar uma conta não retira a possibilidade de acesso usando-a. Para revogar o acesso ao perfil através da conta social, você deverá fazê-lo na sua conta social.',
     'users_social_connect' => 'Contas conectadas',
     'users_social_disconnect' => 'Desconectar Conta',
     'users_social_connected' => 'Conta :socialAccount foi conectada com sucesso ao seu perfil.',
     'users_social_disconnected' => 'Conta :socialAccount foi desconectada com sucesso de seu perfil.',
+
 ];
 
 
index 451dbe99caa398c112d9c02962c3ba32705a8904..3d4b51f036560a917b535d6d4b3569296a1eb7e5 100644 (file)
@@ -1,18 +1,13 @@
 <?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 [
 
-    /*
-    |--------------------------------------------------------------------------
-    | Validation Language 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.
-    |
-    */
-
+    // Standard laravel validation lines
     'accepted'             => 'O :attribute deve ser aceito.',
     'active_url'           => 'O :attribute não é uma URL válida.',
     'after'                => 'O :attribute deve ser uma data posterior à data :date.',
@@ -38,6 +33,7 @@ return [
     'filled'               => 'O campo :attribute é requerido.',
     'exists'               => 'O atributo :attribute selecionado não é válido.',
     'image'                => 'O campo :attribute deve ser uma imagem.',
+    'image_extension'      => 'O campo :attribute deve ter uma extensão de imagem válida & suportada.',
     'in'                   => 'The selected :attribute is invalid.',
     'integer'              => 'O campo :attribute deve ser um número inteiro.',
     'ip'                   => 'O campo :attribute deve ser um IP válido.',
@@ -54,6 +50,7 @@ return [
         'string'  => 'O valor para o campo :attribute não deve ter menos que :min caracteres.',
         'array'   => 'O valor para o campo :attribute não deve ter menos que :min itens.',
     ],
+    'no_double_extension'  => 'O campo :attribute deve ter apenas uma extensão de arquivo.',
     'not_in'               => 'O campo selecionado :attribute é inválido.',
     'numeric'              => 'O campo :attribute deve ser um número.',
     'regex'                => 'O formato do campo :attribute é inválido.',
@@ -74,35 +71,15 @@ return [
     'timezone'             => 'O campo :attribute deve conter uma timezone válida.',
     '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.',
 
-    /*
-    |--------------------------------------------------------------------------
-    | Custom Validation Language Lines
-    |--------------------------------------------------------------------------
-    |
-    | Here you may specify custom validation messages for attributes using the
-    | convention "attribute.rule" to name the lines. This makes it quick to
-    | specify a specific custom language line for a given attribute rule.
-    |
-    */
-
+    // Custom validation lines
     'custom' => [
         'password-confirm' => [
             'required_with' => 'Confirmação de senha requerida',
         ],
     ],
 
-    /*
-    |--------------------------------------------------------------------------
-    | Custom Validation Attributes
-    |--------------------------------------------------------------------------
-    |
-    | The following language lines are used to swap attribute place-holders
-    | with something more reader friendly such as E-Mail Address instead
-    | of "email". This simply helps us make messages a little cleaner.
-    |
-    */
-
+    // Custom validation attributes
     'attributes' => [],
-
 ];
index 3a1fbbf9793aa43a4cf33531ca87cbdbc89a4424..d9baec686c2df18035c5f70a7df369abadbde62c 100644 (file)
@@ -1,21 +1,15 @@
 <?php
+/**
+ * Authentication Language Lines
+ * The following language lines are used during authentication for various
+ * messages that we need to display to the user.
+ */
 return [
-    /*
-    |--------------------------------------------------------------------------
-    | Authentication Language Lines
-    |--------------------------------------------------------------------------
-    |
-    | The following language lines are used during authentication for various
-    | messages that we need to display to the user. You are free to modify
-    | these language lines according to your application's requirements.
-    |
-    */
+    
     'failed' => 'Учетная запись не найдена.',
     'throttle' => 'Слишком много попыток входа. Пожалуйста, попробуйте позже через :seconds секунд.',
 
-    /**
-     * Login & Register
-     */
+    // Login & Register
     'sign_up' => 'Регистрация',
     'log_in' => 'Вход',
     'log_in_with' => 'Вход с :socialDriver',
@@ -27,11 +21,13 @@ return [
     'email' => 'Email',
     'password' => 'Пароль',
     'password_confirm' => 'Подтверждение пароля',
-    'password_hint' => 'Должен быть больше 5 символов',
+    'password_hint' => 'Должен быть больше 7 символов',
     'forgot_password' => 'Забыли пароль?',
     'remember_me' => 'Запомнить меня',
     'ldap_email_hint' => 'Введите email адрес для данной учетной записи.',
     'create_account' => 'Создать аккаунт',
+    'already_have_account' => 'Уже есть аккаунт?',
+    'dont_have_account' => 'У вас нет аккаунта?',
     'social_login' => 'Вход через Соцсеть',
     'social_registration' => 'Регистрация через Соцсеть',
     'social_registration_text' => 'Регистрация и вход через другой сервис.',
@@ -43,23 +39,18 @@ return [
     'register_success' => 'Спасибо за регистрацию! Регистрация и вход в систему выполнены.',
 
 
-    /**
-     * Password Reset
-     */
+    //Password Reset
     'reset_password' => 'Сброс пароля',
-    'reset_password_send_instructions' => 'Введите свой адрес электронной почты ниже, и вам будет отправлено письмо со ссылкой для сброса пароля.',
+    'reset_password_send_instructions' => 'Введите свой email ниже, и вам будет отправлено письмо со ссылкой для сброса пароля.',
     'reset_password_send_button' => 'Отправить ссылку для сброса',
     'reset_password_sent_success' => 'Ссылка для сброса была отправлена на :email.',
     'reset_password_success' => 'Ваш пароль был успешно сброшен.',
-
     'email_reset_subject' => 'Сбросить ваш :appName пароль',
-    'email_reset_text' => 'Ð\92Ñ\8b Ð¿Ð¾Ð»Ñ\83Ñ\87или Ñ\8dÑ\82о Ð¿Ð¸Ñ\81Ñ\8cмо, Ð¿Ð¾Ñ\82омÑ\83 Ñ\87Ñ\82о Ð²Ñ\8b Ð·Ð°Ð¿Ñ\80оÑ\81или Ñ\81бÑ\80оÑ\81 Ð¿Ð°Ñ\80олÑ\8f Ð´Ð»Ñ\8f Ð²Ð°Ñ\88ей Ñ\83Ñ\87еÑ\82ной Ð·Ð°Ð¿Ð¸Ñ\81и.',
+    'email_reset_text' => 'Вы получили это письмо, потому что запросили сброс пароля для вашей учетной записи.',
     'email_reset_not_requested' => 'Если вы не запрашивали сброса пароля, то никаких дополнительных действий не требуется.',
 
 
-    /**
-     * Email Confirmation
-     */
+    //Email Confirmation
     'email_confirm_subject' => 'Подтвердите ваш почтовый адрес на :appName',
     'email_confirm_greeting' => 'Благодарим за участие :appName!',
     'email_confirm_text' => 'Пожалуйста, подтвердите ваш email адрес кликнув на кнопку ниже:',
@@ -73,4 +64,14 @@ return [
     'email_not_confirmed_click_link' => 'Пожалуйста, нажмите на ссылку в письме, которое было отправлено при регистрации.',
     'email_not_confirmed_resend' => 'Если вы не можете найти электронное письмо, вы можете снова отправить письмо с подтверждением по форме ниже.',
     'email_not_confirmed_resend_button' => 'Переотправить письмо с подтверждением',
+    
+    // User Invite
+    '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!'
 ];
index b4cef7764954d5d701bfd73ded364936e55781b5..e70b0a3a7272991983c4bcd0f19ab378c62193e5 100644 (file)
@@ -1,29 +1,27 @@
 <?php
+/**
+ * Common elements found throughout many areas of BookStack.
+ */
 return [
 
-    /**
-     * Buttons
-     */
+    //Buttons
     'cancel' => 'Отмена',
     'confirm' => 'Применить',
     'back' => 'Назад',
     'save' => 'Сохранить',
     'continue' => 'Продолжить',
     'select' => 'Выбрать',
+    'toggle_all' => 'Переключить все',
     'more' => 'Еще',
 
-    /**
-     * Form Labels
-     */
+    //Form Labels
     'name' => 'Имя',
     'description' => 'Описание',
     'role' => 'Роль',
     'cover_image' => 'Обложка',
     'cover_image_description' => 'Изображение должно быть размером около 440x250px.',
 
-    /**
-     * Actions
-     */
+    //Actions
     'actions' => 'Действия',
     'view' => 'Просмотр',
     'view_all' => 'Показать все',
@@ -42,13 +40,15 @@ return [
     'add' => 'Добавить',
     
     // Sort Options
+    'sort_options' => 'Параметры сортировки',
+    'sort_direction_toggle' => 'Переключить направления сортировки',
+    'sort_ascending' => 'По возрастанию',
+    'sort_descending' => 'По убыванию',
     'sort_name' => 'По имени',
     'sort_created_at' => 'По дате создания',
     'sort_updated_at' => 'По дате обновления',
 
-    /**
-     * Misc
-     */
+    //Misc
     'deleted_user' => 'Удаленный пользователь',
     'no_activity' => 'Нет действий для просмотра',
     'no_items' => 'Нет доступных элементов',
@@ -59,10 +59,10 @@ return [
     'grid_view' => 'Вид сеткой',
     'list_view' => 'Вид списком',
     'default' => 'По умолчанию',
+    'breadcrumb' => 'Навигация',
 
-    /**
-     * Header
-     */
+    //Header
+    'profile_menu' => 'Меню профиля',
     'view_profile' => 'Просмотреть профиль',
     'edit_profile' => 'Редактировать профиль',
     
@@ -70,9 +70,7 @@ return [
     'tab_info' => 'Информация',
     'tab_content' => 'Содержание',
 
-    /**
-     * Email Content
-     */
+    //Email Content
     'email_action_help' => 'Если у вас возникли проблемы с нажатием кнопки \':actionText\', то скопируйте и вставьте указанный URL-адрес в свой веб-браузер:',
     'email_rights' => 'Все права зарезервированы',
 ];
index ed4fa6704f86116eaecee0a8e653e86b3a88b818..2fc88d46218ae29c2d2f011b84e0e65744744192 100644 (file)
@@ -1,9 +1,10 @@
 <?php
+/**
+ * Text used in custom JavaScript driven components.
+ */
 return [
 
-    /**
-     * Image Manager
-     */
+    //Image Manager
     'image_select' => 'Выбрать изображение',
     'image_all' => 'Все',
     'image_all_title' => 'Простмотр всех изображений',
@@ -24,9 +25,7 @@ return [
     'image_delete_success' => 'Изображение успешно удалено',
     'image_upload_remove' => 'Удалить изображение',
 
-    /**
-     * Code editor
-     */
+    //Code editor
     'code_editor' => 'Изменить код',
     'code_language' => 'Язык кода',
     'code_content' => 'Содержимое кода',
index 448e4e6a12f8b5256308982c385776631de48ba3..99c5d6d9f1376db0384f103a9070725b129c1cd2 100644 (file)
@@ -1,14 +1,17 @@
 <?php
+/**
+ * Text used for 'Entities' (Document Structure Elements) such as
+ * Books, Shelves, Chapters & Pages
+ */
 return [
 
-    /**
-     * Shared
-     */
+    //Shared
     'recently_created' => 'Недавно созданные',
     'recently_created_pages' => 'Недавно созданные страницы',
     'recently_updated_pages' => 'Недавно обновленные страницы',
     'recently_created_chapters' => 'Недавно созданные главы',
     'recently_created_books' => 'Недавно созданные книги',
+    'recently_created_shelves' => 'Недавно созданные полки',
     'recently_update' => 'Недавно обновленные',
     'recently_viewed' => 'Недавно просмотренные',
     'recent_activity' => 'Недавние действия',
@@ -31,17 +34,13 @@ return [
     'export_pdf' => 'PDF файл',
     'export_text' => 'Текстовый файл',
 
-    /**
-     * Permissions and restrictions
-     */
+    //Permissions and restrictions
     'permissions' => 'Разрешения',
     'permissions_intro' => 'После включения эти разрешения будут иметь приоритет над любыми установленными полномочиями.',
     'permissions_enable' => 'Включение пользовательских разрешений',
     'permissions_save' => 'Сохранить разрешения',
 
-    /**
-     * Search
-     */
+    //Search
     'search_results' => 'Результаты поиска',
     'search_total_results_found' => ':count результатов найдено|:count всего результатов найдено',
     'search_clear' => 'Очистить поиск',
@@ -52,11 +51,13 @@ return [
     'search_content_type' => 'Тип содержимого',
     'search_exact_matches' => 'Точные соответствия',
     'search_tags' => 'Поиск по тегам',
+    'search_options' => 'Параметры',
     'search_viewed_by_me' => 'Просмотрено мной',
     'search_not_viewed_by_me' => 'Не просматривалось мной',
     'search_permissions_set' => 'Набор разрешений',
     'search_created_by_me' => 'Создано мной',
     'search_updated_by_me' => 'Обновлено мной',
+    'search_date_options' => 'Параметры даты',
     'search_updated_before' => 'Обновлено до',
     'search_updated_after' => 'Обновлено после',
     'search_created_before' => 'Создано до',
@@ -64,9 +65,39 @@ return [
     'search_set_date' => 'Установить дату',
     'search_update' => 'Обновить поиск',
 
-    /**
-     * Books
-     */
+     //Shelves
+    'shelf' => 'Полка',
+    'shelves' => 'Полки',
+    'x_shelves' => ':count полок|:count полок',
+    'shelves_long' => 'Книжные полки',
+    'shelves_empty' => 'Полки не созданы',
+    'shelves_create' => 'Создать новую полку',
+    'shelves_popular' => 'Популярные полки',
+    'shelves_new' => 'Новые полки',
+    'shelves_new_action' => 'Новая полка',
+    'shelves_popular_empty' => 'Популярные полки появятся здесь.',
+    'shelves_new_empty' => 'Последние созданные полки появятся здесь.',
+    'shelves_save' => 'Сохранить полку',
+    'shelves_books' => 'Книги из этой полки',
+    'shelves_add_books' => 'Добавить книгу в эту полку',
+    'shelves_drag_books' => 'Перетащите книгу сюда, чтобы добавить на эту полку',
+    'shelves_empty_contents' => 'На этой полке нет книг',
+    'shelves_edit_and_assign' => 'Изменить полку для привязки книг',
+    'shelves_edit_named' => 'Редактировать полку :name',
+    'shelves_edit' => 'Редактировать книжную полку',
+    'shelves_delete' => 'Удалить книжную полку',
+    'shelves_delete_named' => 'Удалить книжную полку :name',
+    'shelves_delete_explain' => "Это приведет к удалению полки с именем ':name'. Привязанные книги удалены не будут.",
+    'shelves_delete_confirmation' => 'Вы уверены, что хотите удалить эту полку?',
+    'shelves_permissions' => 'Доступы к книжной полке',
+    'shelves_permissions_updated' => 'Доступы к книжной полке обновлены',
+    'shelves_permissions_active' => 'Доступы к книжной полке активны',
+    'shelves_copy_permissions_to_books' => 'Наследовать доступы книгам',
+    'shelves_copy_permissions' => 'Копировать доступы',
+    'shelves_copy_permissions_explain' => 'Это применит текущие настройки доступов этой книжной полки ко всем книгам, содержащимся внутри. Перед активацией убедитесь, что все изменения в доступах этой книжной полки сохранены.',
+    'shelves_copy_permission_success' => 'Доступы книжной полки скопированы для :count books',
+    
+    //Books
     'book' => 'Книга',
     'books' => 'Книги',
     'x_books' => ':count книга|:count книг',
@@ -74,6 +105,7 @@ return [
     'books_popular' => 'Популярные книги',
     'books_recent' => 'Недавние книги',
     'books_new' => 'Новые книги',
+    'books_new_action' => 'Новая книга',
     'books_popular_empty' => 'Здесь появятся самые популярные книги.',
     'books_new_empty' => 'Здесь появятся самые последние созданные книги.',
     'books_create' => 'Создать новую книгу',
@@ -89,7 +121,6 @@ return [
     'books_permissions_updated' => 'Разрешения на книгу обновлены',
     'books_empty_contents' => 'Для этой книги нет страниц или разделов.',
     'books_empty_create_page' => 'Создать новую страницу',
-    'books_empty_or' => 'или',
     'books_empty_sort_current_book' => 'Сортировка текущей книги',
     'books_empty_add_chapter' => 'Добавить главу',
     'books_permissions_active' => 'действующие разрешения на книгу',
@@ -97,46 +128,15 @@ return [
     'books_navigation' => 'Навигация по книге',
     'books_sort' => 'Сортировка содержимого книги',
     'books_sort_named' => 'Сортировка книги :bookName',
+    'books_sort_name' => 'Сортировать по имени',
+    'books_sort_created' => 'Сортировать по дате создания',
+    'books_sort_updated' => 'Сортировать по дате обновления',
+    'books_sort_chapters_first' => 'Сначала главы',
+    'books_sort_chapters_last' => 'Главы последние',
     'books_sort_show_other' => 'Показать другие книги',
     'books_sort_save' => 'Сохранить новый порядок',
 
-    /**
-     * Shelves
-     */
-    'shelf' => 'Полка',
-    'shelves' => 'Полки',
-    'x_shelves' => ':count полок|:count полок',
-    'shelves_long' => 'Книжные полки',
-    'shelves_empty' => 'Полки не созданы',
-    'shelves_create' => 'Создать новую полку',
-    'shelves_popular' => 'Популярные полки',
-    'shelves_new' => 'Новые полки',
-    'shelves_new_action' => 'Новая полка',
-    'shelves_popular_empty' => 'Популярные полки появятся здесь.',
-    'shelves_new_empty' => 'Последние созданные полки появятся здесь.',
-    'shelves_save' => 'Сохранить полку',
-    'shelves_books' => 'Книги из этой полки',
-    'shelves_add_books' => 'Добавить книгу в эту полку',
-    'shelves_drag_books' => 'Перетащите книгу сюда, чтобы добавить на эту полку',
-    'shelves_empty_contents' => 'На этой полке нет книг',
-    'shelves_edit_and_assign' => 'Изменить полку для привязки книг',
-    'shelves_edit_named' => 'Редактировать полку :name',
-    'shelves_edit' => 'Редактировать книжную полку',
-    'shelves_delete' => 'Удалить книжную полку',
-    'shelves_delete_named' => 'Удалить книжную полку :name',
-    'shelves_delete_explain' => "Это приведет к удалению полки с именем ':name'. Привязанные книги удалены не будут.",
-    'shelves_delete_confirmation' => 'Вы уверены, что хотите удалить эту полку?',
-    'shelves_permissions' => 'Доступы к книжной полке',
-    'shelves_permissions_updated' => 'Доступы к книжной полке обновлены',
-    'shelves_permissions_active' => 'Доступы к книжной полке активны',
-    'shelves_copy_permissions_to_books' => 'Наследовать доступы книгам',
-    'shelves_copy_permissions' => 'Копировать доступы',
-    'shelves_copy_permissions_explain' => 'Это применит текущие настройки доступов этой книжной полки ко всем книгам, содержащимся внутри. Перед активацией убедитесь, что все изменения в доступах этой книжной полки сохранены.',
-    'shelves_copy_permission_success' => 'Доступы книжной полки скопированы для :count books',
-
-    /**
-     * Chapters
-     */
+     //Chapters
     'chapter' => 'Глава',
     'chapters' => 'Главы',
     'x_chapters' => ':count глава|:count главы',
@@ -159,9 +159,7 @@ return [
     'chapters_permissions_success' => 'Разрешения главы обновлены',
     'chapters_search_this' => 'Искать в этой главе',
 
-    /**
-     * Pages
-     */
+    //Pages
     'page' => 'Страница',
     'pages' => 'Страницы',
     'x_pages' => ':count страница|:count страниц',
@@ -212,6 +210,8 @@ return [
     'pages_revisions_created_by' => 'Создана',
     'pages_revisions_date' => 'Дата версии',
     'pages_revisions_number' => '#',
+    'pages_revisions_numbered' => 'Ревизия #:id',
+    'pages_revisions_numbered_changes' => 'Ревизия #:id изменения',
     'pages_revisions_changelog' => 'Список изменений',
     'pages_revisions_changes' => 'Изменения',
     'pages_revisions_current' => 'Текущая версия',
@@ -233,19 +233,21 @@ return [
         'message' => ':start :time. Будьте осторожны, чтобы не перезаписывать друг друга!',
     ],
     'pages_draft_discarded' => 'Черновик сброшен, редактор обновлен текущим содержимым страницы',
+    'pages_specific' => 'Конкретная страница',
+    'pages_is_template' => 'Шаблон страницы',
 
-    /**
-     * Editor sidebar
-     */
+    //Editor sidebar
     'page_tags' => 'Теги страницы',
     'chapter_tags' => 'Теги главы',
     'book_tags' => 'Теги книги',
     'shelf_tags' => 'Теги полки',
     'tag' => 'Тег',
     'tags' =>  'Теги',
+    'tag_name' =>  'Имя тега',
     'tag_value' => 'Значение тега (опционально)',
     'tags_explain' => 'Добавьте теги, чтобы лучше классифицировать ваш контент. \n Вы можете присвоить значение тегу для более глубокой организации.',
     'tags_add' => 'Добавить тег',
+    'tags_remove' => 'Удалить этот тэг',
     'attachments' => 'Вложение',
     'attachments_explain' => 'Загрузите несколько файлов или добавьте ссылку для отображения на своей странице. Они видны на боковой панели страницы.',
     'attachments_explain_instant_save' => 'Изменения здесь сохраняются мгновенно.',
@@ -271,19 +273,22 @@ return [
     'attachments_file_uploaded' => 'Файл успешно загружен',
     'attachments_file_updated' => 'Файл успешно обновлен',
     'attachments_link_attached' => 'Ссылка успешно присоединена к странице',
+    'templates' => 'Шаблоны',
+    'templates_set_as_template' => 'Страница это шаблон',
+    'templates_explain_set_as_template' => 'Вы можете назначить эту страницу в качестве шаблона, её содержимое будет использоваться при создании других страниц. Пользователи смогут использовать этот шаблон в случае, если имеют разрешения на просмотр этой страницы.',
+    'templates_replace_content' => 'Заменить содержимое страницы',
+    'templates_append_content' => 'Добавить к содержанию страницы',
+    'templates_prepend_content' => 'Добавить в начало содержимого страницы',
 
-    /**
-     * Profile View
-     */
+    //Profile View
     'profile_user_for_x' => 'пользователь уже :time',
     'profile_created_content' => 'Созданный контент',
     'profile_not_created_pages' => ':userName не создавал страниц',
     'profile_not_created_chapters' => ':userName не создавал глав',
     'profile_not_created_books' => ':userName не создавал ни одной книги',
+    'profile_not_created_shelves' => ':userName не создал ни одной полки',
 
-    /**
-     * Comments
-     */
+    //Comments
     'comment' => 'Комментарий',
     'comments' => 'Комментарии',
     'comment_add' => 'Комментировать',
@@ -298,13 +303,12 @@ return [
     'comment_deleted_success' => 'Комментарий удален',
     'comment_created_success' => 'Комментарий добавлен',
     'comment_updated_success' => 'Комментарий обновлен',
-    'comment_delete_confirm' => 'Ð\92Ñ\8b Ñ\83веÑ\80енÑ\8b, Ñ\87Ñ\82о Ñ\85оÑ\82иÑ\82е Ñ\83далить этот комментарий?',
+    'comment_delete_confirm' => 'Удалить этот комментарий?',
     'comment_in_reply_to' => 'В ответ на :commentId',
 
-    /**
-     * Revision
-     */
-    'revision_delete_confirm' => 'Вы действительно хотите удалить эту ревизию?',
-    'revision_delete_success' => 'Редактирование удалено',
-    'revision_cannot_delete_latest' => 'Не удается удалить последнюю версию.'
+    //Revision
+    'revision_delete_confirm' => 'Удалить эту ревизию?',
+    'revision_restore_confirm' => 'Восстановить эту ревизию? Текущее содержимое будет заменено.',
+    'revision_delete_success' => 'Ревизия удалена',
+    'revision_cannot_delete_latest' => 'Нельзя удалить последнюю версию.'
 ];
index 6286425a9333e39cdd183f7fd38cb70e57561190..69063e546a2411872cb8d84e121ccac7c13120df 100644 (file)
@@ -1,11 +1,9 @@
 <?php
-
+/**
+ * Text shown in error messaging.
+ */
 return [
 
-    /**
-     * Error text strings.
-     */
-
     // Permissions
     'permission' => 'У вас нет доступа к запрашиваемой странице.',
     'permissionJson' => 'У вас нет разрешения для запрашиваемого действия.',
@@ -29,6 +27,7 @@ return [
     'social_account_register_instructions' => 'Если у вас еще нет учетной записи, вы можете зарегистрироваться, используя параметр :socialAccount.',
     'social_driver_not_found' => 'Драйвер для Соцсети не найден',
     'social_driver_not_configured' => 'Настройки вашего :socialAccount заданы неправильно.',
+    'invite_token_expired' => 'Срок действия приглашения истек. Вместо этого вы можете попытаться сбросить пароль своей учетной записи.',
 
     // System
     'path_not_writable' => 'Невозможно загрузить файл по пути :filePath . Убедитесь что сервер доступен для записи.',
@@ -50,6 +49,7 @@ return [
 
     // Entities
     'entity_not_found' => 'Объект не найден',
+    'bookshelf_not_found' => 'Полка не найдена',
     'book_not_found' => 'Книга не найдена',
     'page_not_found' => 'Страница не найдена',
     'chapter_not_found' => 'Глава не найдена',
@@ -65,6 +65,7 @@ return [
     'role_cannot_be_edited' => 'Невозможно отредактировать данную роль',
     'role_system_cannot_be_deleted' => 'Эта роль является системной и не может быть удалена',
     'role_registration_default_cannot_delete' => 'Эта роль не может быть удалена, так как она устанолена в качестве роли по умолчанию',
+    'role_cannot_remove_only_admin' => 'Этот пользователь единственный с правами администратора. Назначьте роль администратора другому пользователю, прежде чем удалить этого.',
 
     // Comments
     'comment_list' => 'При получении комментариев произошла ошибка.',
@@ -80,4 +81,5 @@ return [
     'error_occurred' => 'Произошла ошибка',
     'app_down' => ':appName в данный момент не достпуно',
     'back_soon' => 'Скоро восстановится.',
+    
 ];
index 002e4ae28f9715ea98c7b9c6cf5f2d2b793bda49..1a797e36cd6c43b85d51046d29586d4360b477b4 100644 (file)
@@ -1,18 +1,11 @@
 <?php
-
+/**
+ * Pagination Language Lines
+ * The following language lines are used by the paginator library to build
+ * the simple pagination links.
+ */
 return [
 
-    /*
-    |--------------------------------------------------------------------------
-    | Pagination Language Lines
-    |--------------------------------------------------------------------------
-    |
-    | The following language lines are used by the paginator library to build
-    | the simple pagination links. You are free to change them to anything
-    | you want to customize your views to better match your application.
-    |
-    */
-
     'previous' => '&laquo; Предыдущая',
     'next'     => 'Следующая &raquo;',
 
index 28bc6d7544d438c74476b3e4be804a1eb7699f56..760509a6aa576ce64a685bf2ea2e6fd416ca8193 100644 (file)
@@ -1,22 +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 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, such as for an invalid token or invalid new password.
-    |
-    */
-
-    'password' => 'Пароль должен содержать не менее шести символов для применения.',
-    'user' => 'Невозможно найти пользователя с указанным email адресом.',
-    'token' => 'Этот токен для сброса пароля недействителен.',
-    'sent' => 'Ссылка для сброса пароля была отправлена на электронную почту!',
+    'password' => 'Пароль должен содержать не менее шести символов и совпадать с подтверждением.',
+    'user' => 'Пользователь с указаным email отсутствует.',
+    'token' => 'Токен сброса пароля недействителен.',
+    'sent' => 'Ссылка для сброса пароля отправлена на email!',
     'reset' => 'Ваш пароль был сброшен!',
 
 ];
index 0ff1b969bf3cd61fd1513a10de7168440e96c2ec..95f453a27a9eff2fe154cef950000276e8536c24 100755 (executable)
@@ -13,7 +13,7 @@ return [
 
     // App Settings
     'app_customization' => 'Настройки',
-    'app_features_security' => 'ФÑ\83нкÑ\86ии & Безопасность',
+    'app_features_security' => 'ФÑ\83нкÑ\86ионал & Безопасность',
     'app_name' => 'Имя приложения',
     'app_name_desc' => 'Имя отображается в заголовке email отправленных системой.',
     'app_name_header' => 'Отображать имя приложения в заголовке',
@@ -29,6 +29,7 @@ return [
     'app_editor_desc' => 'Выберите, какой редактор будет использоваться всеми пользователями для редактирования страниц.',
     'app_custom_html' => 'Пользовательский контент заголовка HTML',
     'app_custom_html_desc' => 'Любой контент, добавленный здесь, будет вставлен в нижнюю часть раздела <head> каждой страницы. Это удобно для переопределения стилей или добавления кода аналитики.',
+    'app_custom_html_disabled_notice' => 'Пользовательский контент заголовка HTML отключен на этой странице, чтобы гарантировать отмену любых критических изменений',
     'app_logo' => 'Лого приложения',
     'app_logo_desc' => 'Это изображение должно быть 43px в высоту. <br>Большое изображение будет уменьшено.',
     'app_primary_color' => 'Основной цвет приложения',
@@ -42,13 +43,13 @@ return [
 
     // Registration Settings
     'reg_settings' => 'Настройки регистрации',
-    'reg_enable' => 'РазÑ\80еÑ\88иÑ\82Ñ\8c Ñ\80егиÑ\81Ñ\82Ñ\80аÑ\86иÑ\8fÑ\8e',
+    'reg_enable' => 'Разрешить регистрацию',
     'reg_enable_toggle' => 'Разрешить регистрацию',
-    'reg_enable_desc' => 'Если регистрация разрешена, пользователь сможет зарегистрироваться в системе самомтоятельно. При регистрации назначается роль пользователя по умолчанию',
+    'reg_enable_desc' => 'Если регистрация разрешена, пользователь сможет зарегистрироваться в системе самостоятельно. При регистрации назначается роль пользователя по умолчанию',
     'reg_default_role' => 'Роль пользователя по умолчанию после регистрации',
     'reg_email_confirmation' => 'Подтверждение электонной почты',
     'reg_email_confirmation_toggle' => 'Требовать подтверждение по электронной почте',
-    'reg_confirm_email_desc' => 'Ð\95Ñ\81ли Ð¸Ñ\81полÑ\8cзÑ\83еÑ\82Ñ\81Ñ\8f Ð¾Ð³Ñ\80аниÑ\87ение Ð¿Ð¾ Ð´Ð¾Ð¼ÐµÐ½Ñ\83, Ð¿Ð¾Ð´Ñ\82веÑ\80ждение Ð±Ñ\83деÑ\82 Ð¾Ð±Ñ\8fзаÑ\82елÑ\8cно, Ð° Ñ\8dÑ\82оÑ\82 Ð¿Ñ\83нкÑ\82 Ð¿Ñ\80оигноÑ\80иÑ\80ован.',
+    'reg_confirm_email_desc' => 'Ð\9fÑ\80и Ð¸Ñ\81полÑ\8cзовании Ð¾Ð³Ñ\80аниÑ\87ениÑ\8f Ð¿Ð¾ Ð´Ð¾Ð¼ÐµÐ½Ñ\83 - Ð¿Ð¾Ð´Ñ\82веÑ\80ждение Ð¾Ð±Ñ\8fзаÑ\82елÑ\8cно, Ñ\8dÑ\82оÑ\82 Ð¿Ñ\83нкÑ\82 Ð¸Ð³Ð½Ð¾Ñ\80иÑ\80Ñ\83еÑ\82Ñ\81Ñ\8f.',
     'reg_confirm_restrict_domain' => 'Ограничить регистрацию по домену',
     'reg_confirm_restrict_domain_desc' => 'Введите список доменов почты через запятую, для которых разрешена регистрация. Пользователям будет отправлено письмо для подтверждения адреса перед входом в приложение. <br> Обратите внимание, что пользователи смогут изменить свои адреса уже после регистрации.',
     'reg_confirm_restrict_domain_placeholder' => 'Без ограничений',
@@ -84,6 +85,7 @@ return [
     'role_manage_roles' => 'Управление ролями и правами на роли',
     'role_manage_entity_permissions' => 'Управление правами на все книги, главы и страницы',
     'role_manage_own_entity_permissions' => 'Управление разрешениями для собственных книг, разделов и страниц',
+    'role_manage_page_templates' => 'Управление шаблонами страниц',
     'role_manage_settings' => 'Управление настройками приложения',
     'role_asset' => 'Разрешение для активации',
     'role_asset_desc' => 'Эти разрешения контролируют доступ по умолчанию к параметрам внутри системы. Разрешения на книги, главы и страницы перезапишут эти разрешения.',
@@ -107,7 +109,9 @@ return [
     'users_role' => 'Роли пользователя',
     'users_role_desc' => 'Назначьте роли пользователю. Если назначено несколько ролей, разрешения будут суммироваться и пользователь получит все права назначенных ролей.',
     'users_password' => 'Пароль пользователя',
-    'users_password_desc' => 'Установите пароль для входа в приложение. Должно быть не менее 5 символов.',
+    'users_password_desc' => 'Установите пароль для входа в приложение. Должно быть не менее 6 символов.',
+    'users_send_invite_text' => 'Вы можете отправить этому пользователю email с приглашением, которое позволит ему установить пароль самостоятельно или задайте пароль сами.',
+    'users_send_invite_option' => 'Отправить пользователю email с приглашением.',
     'users_external_auth_id' => 'Внешний ID аутентификации',
     'users_external_auth_id_desc' => 'Этот ID используется для связи с вашей LDAP системой.',
     'users_password_warning' => 'Заполните ниже только если вы хотите сменить свой пароль.',
@@ -125,7 +129,7 @@ return [
     'users_preferred_language' => 'Предпочитаемый язык',
     'users_preferred_language_desc' => 'Этот параметр изменит язык интерфейса приложения. Это не влияет на созданный пользователем контент.',
     'users_social_accounts' => 'Аккаунты Соцсетей',
-    'users_social_accounts_info' => 'Здесь вы можете подключить другие учетные записи для более быстрого и легкого входа в систему. Отключение учетной записи здесь не разрешено. Отменить доступ к настройкам вашего профиля в подключенном социальном аккаунте.',
+    'users_social_accounts_info' => 'Здесь вы можете подключить другие учетные записи для более быстрого и легкого входа в систему. Отключение учетной записи здесь не возможно. Отмените доступ к настройкам вашего профиля в подключенном аккаунте соцсети.',
     'users_social_connect' => 'Подключить аккаунт',
     'users_social_disconnect' => 'Отключить аккаунт',
     'users_social_connected' => ':socialAccount аккаунт упешно подключен к вашему профилю.',
index ae8a3b37eee51829e72c9b45d636e9554367b171..fabb109f9c78d3fb9728c908e12f75e814d1604d 100644 (file)
@@ -1,18 +1,13 @@
 <?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 [
 
-    /*
-    |--------------------------------------------------------------------------
-    | Validation Language 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.
-    |
-    */
-
+    // Standard laravel validation lines
     'accepted'             => ':attribute должен быть принят.',
     'active_url'           => ':attribute не является корректным URL.',
     'after'                => ':attribute дата должна быть позже :date.',
@@ -38,6 +33,7 @@ return [
     'filled'               => ':attribute поле необходимо.',
     'exists'               => 'выделенный :attribute некорректен.',
     'image'                => ':attribute должен быть изображением.',
+    'image_extension'      => ':attribute должен быть исправным  и содержать расширение картинки',
     'in'                   => 'выделенный :attribute некорректен.',
     'integer'              => ':attribute должно быть целое число.',
     'ip'                   => ':attribute должен быть корректным IP адресом.',
@@ -54,6 +50,7 @@ return [
         'string'  => ':attribute должен быть минимум :min символов.',
         'array'   => ':attribute должен содержать хотя бы :min элементов.',
     ],
+    'no_double_extension'  => ':attribute должен иметь только одно расширение файла.',
     'not_in'               => 'Выбранный :attribute некорректен.',
     'numeric'              => ':attribute должен быть числом.',
     'regex'                => ':attribute неправильный формат.',
@@ -74,35 +71,16 @@ return [
     'timezone'             => ':attribute должен быть корректным часовым поясом.',
     'unique'               => ':attribute уже есть.',
     'url'                  => ':attribute имеет неправильный формат.',
+    'uploaded'             => 'Не удалось загрузить файл. Сервер не может принимать файлы такого размера.',
 
-    /*
-    |--------------------------------------------------------------------------
-    | Custom Validation Language Lines
-    |--------------------------------------------------------------------------
-    |
-    | Here you may specify custom validation messages for attributes using the
-    | convention 'attribute.rule' to name the lines. This makes it quick to
-    | specify a specific custom language line for a given attribute rule.
-    |
-    */
-
+    //Custom validation lines
     'custom' => [
         'password-confirm' => [
             'required_with' => 'Требуется подтверждение пароля',
         ],
     ],
 
-    /*
-    |--------------------------------------------------------------------------
-    | Custom Validation Attributes
-    |--------------------------------------------------------------------------
-    |
-    | The following language lines are used to swap attribute place-holders
-    | with something more reader friendly such as E-Mail Address instead
-    | of 'email'. This simply helps us make messages a little cleaner.
-    |
-    */
-
+    //Custom validation attributes
     'attributes' => [],
 
 ];
index 2fa69ac3e9b88b374495ac860faf75268828b76a..69004e87ae97b56170d5a1547f86d2526e0e0706 100644 (file)
@@ -27,7 +27,7 @@ return [
     'email' => 'Email',
     'password' => 'Heslo',
     'password_confirm' => 'Potvrdiť heslo',
-    'password_hint' => 'Musí mať viac ako 5 znakov',
+    'password_hint' => 'Musí mať viac ako 7 znakov',
     'forgot_password' => 'Zabudli ste heslo?',
     'remember_me' => 'Zapamätať si ma',
     'ldap_email_hint' => 'Zadajte prosím email, ktorý sa má použiť pre tento účet.',
index 30e1a1937744fd5678b3918b37e5eb8538612108..4eb2be028f58af2c2640a484feb329ef66a394c5 100644 (file)
@@ -27,7 +27,7 @@ return [
     'email' => 'E-post',
     'password' => 'Lösenord',
     'password_confirm' => 'Bekräfta lösenord',
-    'password_hint' => 'Måste vara fler än 5 tecken',
+    'password_hint' => 'Måste vara fler än 7 tecken',
     'forgot_password' => 'Glömt lösenord?',
     'remember_me' => 'Kom ihåg mig',
     'ldap_email_hint' => 'Vänligen ange en e-postadress att använda till kontot.',
diff --git a/resources/lang/tr/activities.php b/resources/lang/tr/activities.php
new file mode 100644 (file)
index 0000000..ccc55e1
--- /dev/null
@@ -0,0 +1,48 @@
+<?php
+/**
+ * Activity text strings.
+ * Is used for all the text within activity logs & notifications.
+ */
+return [
+
+    // Pages
+    'page_create'                 => 'saya oluşturuldu',
+    'page_create_notification'    => 'Sayfa Başarıyla Oluşturuldu',
+    'page_update'                 => 'sayfa güncellendi',
+    'page_update_notification'    => 'Sayfa Başarıyla Güncellendi',
+    'page_delete'                 => 'sayfa silindi',
+    'page_delete_notification'    => 'Sayfa Başarıyla Silindi',
+    'page_restore'                => 'sayfa kurtarıldı',
+    'page_restore_notification'   => 'Sayfa Başarıyla Kurtarıldı',
+    'page_move'                   => 'sayfa taşındı',
+
+    // Chapters
+    'chapter_create'              => 'bölüm oluşturuldu',
+    'chapter_create_notification' => 'Bölüm Başarıyla Oluşturuldu',
+    'chapter_update'              => 'bölüm güncellendi',
+    'chapter_update_notification' => 'Bölüm Başarıyla Güncellendi',
+    'chapter_delete'              => 'bölüm silindi',
+    'chapter_delete_notification' => 'Bölüm Başarıyla Silindi',
+    'chapter_move'                => 'bölüm taşındı',
+
+    // Books
+    'book_create'                 => 'kitap oluşturuldu',
+    'book_create_notification'    => 'Kitap Başarıyla Oluşturuldu',
+    'book_update'                 => 'kitap güncellendi',
+    'book_update_notification'    => 'Kitap Başarıyla Güncellendi',
+    'book_delete'                 => 'kitap silindi',
+    'book_delete_notification'    => 'Kitap Başarıyla Silindi',
+    'book_sort'                   => 'kitap düzenlendi',
+    'book_sort_notification'      => 'Kitap Başarıyla Yeniden Sıralandı',
+
+    // Bookshelves
+    'bookshelf_create'            => 'kitaplık oluşturuldu',
+    'bookshelf_create_notification'    => 'Kitaplık Başarıyla Oluşturuldu',
+    'bookshelf_update'                 => 'kitaplık güncellendi',
+    'bookshelf_update_notification'    => 'Kitaplık Başarıyla Güncellendi',
+    'bookshelf_delete'                 => 'kitaplık silindi',
+    'bookshelf_delete_notification'    => 'Kitaplık Başarıyla Silindi',
+
+    // Other
+    'commented_on'                => 'yorum yaptı',
+];
diff --git a/resources/lang/tr/auth.php b/resources/lang/tr/auth.php
new file mode 100644 (file)
index 0000000..1988c01
--- /dev/null
@@ -0,0 +1,67 @@
+<?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' => 'Girilen bilgiler bizdeki kayıtlarla uyuşmuyor.',
+    'throttle' => 'Çok fazla giriş yapmaya çalıştınız. Lütfen :seconds saniye içinde tekrar deneyin.',
+
+    // Login & Register
+    'sign_up' => 'Kayıt Ol',
+    'log_in' => 'Giriş Yap',
+    'log_in_with' => ':socialDriver ile giriş yap',
+    'sign_up_with' => ':socialDriver ile kayıt ol',
+    'logout' => 'Çıkış Yap',
+
+    'name' => 'İsim',
+    'username' => 'Kullanıcı Adı',
+    'email' => 'Email',
+    'password' => 'Şifre',
+    'password_confirm' => 'Şifreyi onayla',
+    'password_hint' => 'En az 5 karakter olmalı',
+    'forgot_password' => 'Şifrenizi mi unuttunuz?',
+    'remember_me' => 'Beni Hatırla',
+    'ldap_email_hint' => 'Hesabı kullanmak istediğiniz e-mail adresinizi giriniz.',
+    'create_account' => 'Hesap Oluştur',
+    'already_have_account' => 'Zaten bir hesabınız var mı?',
+    'dont_have_account' => 'Hesabınız yok mu?',
+    'social_login' => 'Diğer Servisler ile Giriş Yap',
+    'social_registration' => 'Diğer Servisler ile Kayıt Ol',
+    'social_registration_text' => 'Diğer servisler ile kayıt ol ve giriş yap.',
+
+    'register_thanks' => 'Kayıt olduğunuz için teşekkürler!',
+    'register_confirm' => 'Lütfen e-posta adresinizi kontrol edin ve gelen doğrulama bağlantısına tıklayınız. :appName.',
+    'registrations_disabled' => 'Kayıt olma özelliği geçici olarak kısıtlanmıştır',
+    'registration_email_domain_invalid' => 'Bu e-mail sağlayıcısının bu uygulamaya erişim izni yoktur.',
+    'register_success' => 'Artık kayıtlı bir kullanıcı olarak giriş yaptınız.',
+
+
+    // Password Reset
+    'reset_password' => 'Parolayı Sıfırla',
+    'reset_password_send_instructions' => 'Aşağıya e-mail adresinizi girdiğinizde parola yenileme bağlantısı mail adresinize gönderilecektir.',
+    'reset_password_send_button' => '>Sıfırlama Bağlantısını Gönder',
+    'reset_password_sent_success' => 'Sıfırlama bağlantısı :email adresinize gönderildi.',
+    'reset_password_success' => 'Parolanız başarıyla sıfırlandı.',
+    'email_reset_subject' => ':appName şifrenizi sıfırlayın.',
+    'email_reset_text' => ' Parola sıfırlama isteğinde bulunduğunuz için bu maili görüntülüyorsunuz.',
+    'email_reset_not_requested' => 'Eğer bu parola sıfırlama isteğinde bulunmadıysanız herhangi bir işlem yapmanıza gerek yoktur.',
+
+
+    // Email Confirmation
+    'email_confirm_subject' => ':appName için girdiğiniz mail adresiniz onaylayınız',
+    'email_confirm_greeting' => ':appName\'e katıldığınız için teşekkürler!',
+    'email_confirm_text' => 'Lütfen e-mail adresinizi aşağıda bulunan butona tıklayarak onaylayınız:',
+    'email_confirm_action' => 'E-Maili Onayla',
+    'email_confirm_send_error' => 'e-mail onayı gerekli fakat sistem mail göndermeyi başaramadı. Yöneticiniz ile görüşüp kurulumlarda bir sorun olmadığını doğrulayın.',
+    'email_confirm_success' => 'e-mail adresiniz onaylandı!',
+    'email_confirm_resent' => 'Doğrulama maili gönderildi, lütfen gelen kutunuzu kontrol ediniz...',
+
+    'email_not_confirmed' => 'E-mail Adresi Doğrulanmadı',
+    'email_not_confirmed_text' => 'Sağlamış olduğunuz e-mail adresi henüz doğrulanmadı.',
+    'email_not_confirmed_click_link' => 'Lütfen kayıt olduktan kısa süre sonra size gönderilen maildeki bağlantıya tıklayın ve mail adresinizi onaylayın.',
+    'email_not_confirmed_resend' => 'Eğer gelen maili bulamadıysanız aşağıdaki formu tekrar doldurarak onay mailini kendinize tekrar gönderebilirsiniz.',
+    'email_not_confirmed_resend_button' => 'Doğrulama Mailini Yeniden Yolla',
+];
\ No newline at end of file
diff --git a/resources/lang/tr/common.php b/resources/lang/tr/common.php
new file mode 100644 (file)
index 0000000..54425ae
--- /dev/null
@@ -0,0 +1,70 @@
+<?php
+/**
+ * Common elements found throughout many areas of BookStack.
+ */
+return [
+
+    // Buttons
+    'cancel' => 'İptal',
+    'confirm' => 'Onayla',
+    'back' => 'Geri',
+    'save' => 'Kaydet',
+    'continue' => 'Devam',
+    'select' => 'Seç',
+    'toggle_all' => 'Hepsini Değiştir',
+    'more' => 'Daha Fazla',
+
+    // Form Labels
+    'name' => 'İsim',
+    'description' => 'Açıklama',
+    'role' => 'Rol',
+    'cover_image' => 'Kapak resmi',
+    'cover_image_description' => 'Bu resim yaklaşık 440x250px boyutlarında olmalıdır.',
+    
+    // Actions
+    'actions' => 'Aksiyonlar',
+    'view' => 'Görüntüle',
+    'view_all' => 'Hepsini Görüntüle',
+    'create' => 'Oluştur',
+    'update' => 'Güncelle',
+    'edit' => 'Düzenle',
+    'sort' => 'Sırala',
+    'move' => 'Taşı',
+    'copy' => 'Kopyala',
+    'reply' => 'Yanıtla',
+    'delete' => 'Sil',
+    'search' => 'Ara',
+    'search_clear' => 'Aramayı Temizle',
+    'reset' => 'Sıfırla',
+    'remove' => 'Kaldır',
+    'add' => 'Ekle',
+
+    // Sort Options
+    'sort_name' => 'İsim',
+    'sort_created_at' => 'Oluşturulma Tarihi',
+    'sort_updated_at' => 'Güncellenme Tarihi',
+
+    // Misc
+    'deleted_user' => 'Silinmiş Kullanıcı',
+    'no_activity' => 'Gösterilecek aktivite yok',
+    'no_items' => 'Kullanılabilir öge yok',
+    'back_to_top' => 'Başa dön',
+    'toggle_details' => 'Detayları değiştir',
+    'toggle_thumbnails' => 'Küçük resimleri değiştir',
+    'details' => 'Detaylar',
+    'grid_view' => 'Grid görünümü',
+    'list_view' => 'Liste görünümü',
+    'default' => 'Varsayılan',
+
+    // Header
+    'view_profile' => 'Profili Görüntüle',
+    'edit_profile' => 'Profili Düzenle',
+
+    // Layout tabs
+    'tab_info' => 'Bilgi',
+    'tab_content' => 'İçerik',
+
+    // Email Content
+    'email_action_help' => 'Eğer ":actionText" butonuna tıklamakta zorluk çekiyorsanız, aşağıda bulunan linki kopyalayıp tarayıcınıza yapıştırabilirsiniz.',
+    'email_rights' => 'Bütün hakları saklıdır',
+];
diff --git a/resources/lang/tr/components.php b/resources/lang/tr/components.php
new file mode 100644 (file)
index 0000000..d52f2d0
--- /dev/null
@@ -0,0 +1,33 @@
+<?php
+/**
+ * Text used in custom JavaScript driven components.
+ */
+return [
+
+    // Image Manager
+    'image_select' => 'Görsel Seç',
+    'image_all' => 'Tümü',
+    'image_all_title' => 'Tüm görselleri temizle',
+    'image_book_title' => 'Bu kitaba ait görselleri görüntüle',
+    'image_page_title' => 'Bu sayfaya ait görselleri görüntüle',
+    'image_search_hint' => 'Görsel adı ile ara',
+    'image_uploaded' => ':uploadedDate tarihinde yüklendi',
+    'image_load_more' => 'Daha Fazla ',
+    'image_image_name' => 'Görsel Adı',
+    'image_delete_used' => 'Bu görsel aşağıda bulunan görsellerde kullanılmış.',
+    'image_delete_confirm' => 'Gerçekten bu görseli silmek istiyorsanız sil tuşuna basınız.',
+    'image_select_image' => 'Görsel Seç',
+    'image_dropzone' => 'Görselleri buraya sürükle veya seçmek için buraya tıkla',
+    'images_deleted' => 'Görseller Silindi',
+    'image_preview' => 'Görsel Önizleme',
+    'image_upload_success' => 'Görsel başarıyla yüklendi',
+    'image_update_success' => 'Görsel başarıyla güncellendi',
+    'image_delete_success' => 'Görsel başarıyla silindi',
+    'image_upload_remove' => 'Kaldır',
+
+    // Code Editor
+    'code_editor' => 'Kodu Güncelle',
+    'code_language' => 'Kod Dil',
+    'code_content' => 'Kod İçeriği',
+    'code_save' => 'Kodu Kaydet',
+];
diff --git a/resources/lang/tr/entities.php b/resources/lang/tr/entities.php
new file mode 100644 (file)
index 0000000..698355a
--- /dev/null
@@ -0,0 +1,304 @@
+<?php
+/**
+ * Text used for 'Entities' (Document Structure Elements) such as
+ * Books, Shelves, Chapters & Pages
+ */
+return [
+
+    // Shared
+    'recently_created' => 'Yakın Zamanda Oluşturuldu',
+    'recently_created_pages' => 'Yakın Zamanda Oluşturulmuş Sayfalar',
+    'recently_updated_pages' => 'Yakın Zamanda Güncellenmiş Sayfalar',
+    'recently_created_chapters' => 'Yakın Zamanda Oluşturulmuş Bölümler',
+    'recently_created_books' => 'Yakın Zamanda Olşturulmuş Kitaplar',
+    'recently_created_shelves' => 'Yakın Zamanda Oluşturulmuş Kitaplıklar',
+    'recently_update' => 'Yakın Zamanda Güncellenmiş',
+    'recently_viewed' => 'Yakın Zamanda Görüntülenmiş',
+    'recent_activity' => 'Son Hareketler',
+    'create_now' => 'Hemen bir tane oluştur',
+    'revisions' => 'Revizyonlar',
+    'meta_revision' => 'Revizyon #:revisionCount',
+    'meta_created' => 'Oluşturuldu :timeLength',
+    'meta_created_name' => ':user tarafından :timeLength tarihinde oluşturuldu',
+    'meta_updated' => 'Güncellendi :timeLength',
+    'meta_updated_name' => ':user tarafından :timeLength tarihinde güncellendi',
+    'entity_select' => 'Öğe Seçme',
+    'images' => 'Görseller',
+    'my_recent_drafts' => 'Son Taslaklarım',
+    'my_recently_viewed' => 'Son Görüntülemelerim',
+    'no_pages_viewed' => 'Herhangi bir sayfa görüntülemediniz',
+    'no_pages_recently_created' => 'Yakın zamanda bir sayfa oluşturulmadı',
+    'no_pages_recently_updated' => 'Yakın zamanda bir sayfa güncellenmedi',
+    'export' => 'Dışa Aktar',
+    'export_html' => 'Contained Web Dosyası',
+    'export_pdf' => 'PDF Dosyası',
+    'export_text' => 'Düz Metin Dosyası',
+
+    // Permissions and restrictions
+    'permissions' => 'İzinler',
+    'permissions_intro' => 'Etkinleştirildikten sonra bu izinler diğer diğer bütün izinlerden öncelikli olacaktır.',
+    'permissions_enable' => 'Özelleştirilmiş Yetkileri Etkinleştir',
+    'permissions_save' => 'İzinleri Kaydet',
+
+    // Search
+    'search_results' => 'Arama Sonuçları',
+    'search_total_results_found' => ':count sonuç bulundu |:count toplam sonuç bulundu',
+    'search_clear' => 'Aramaları Temizle',
+    'search_no_pages' => 'Bu aramayla herhangi bir sonuç eşleşmedi',
+    'search_for_term' => ':term için ara',
+    'search_more' => 'Daha Fazla Sonuç',
+    'search_filters' => 'Arama Filtreleri',
+    'search_content_type' => 'İçerik Türü',
+    'search_exact_matches' => 'Tam Eşleşmeler',
+    'search_tags' => 'Etiket Aramaları',
+    'search_options' => 'Ayarlar',
+    'search_viewed_by_me' => 'Benim tarafımdan görüntülendi',
+    'search_not_viewed_by_me' => 'Benim tarafımdan görüntülenmedi',
+    'search_permissions_set' => 'İzinler ayarlandı',
+    'search_created_by_me' => 'Benim tarafımdan oluşturuldu',
+    'search_updated_by_me' => 'Benim tarafımdan güncellendi',
+    'search_date_options' => 'Tarih Seçenekleri',
+    'search_updated_before' => 'Önce güncellendi',
+    'search_updated_after' => 'Sonra güncellendi',
+    'search_created_before' => 'Önce oluşturuldu',
+    'search_created_after' => 'Sonra oluşturuldu',
+    'search_set_date' => 'Tarih Ayarla',
+    'search_update' => 'Aramayı Güncelle',
+
+    // Shelves
+    'shelf' => 'Kitaplık',
+    'shelves' => 'Kitaplıklar',
+    'x_shelves' => ':count Kitaplık|:count Kitaplıklar',
+    'shelves_long' => 'Kitaplıklar',
+    'shelves_empty' => 'Hiç kitaplık oluşturulmadı',
+    'shelves_create' => 'Yeni Kitaplık Oluştur',
+    'shelves_popular' => 'Popüler Kitaplıklar',
+    'shelves_new' => 'Yeni Kitaplıklar',
+    'shelves_new_action' => 'Yeni Kitaplık',
+    'shelves_popular_empty' => 'En popüler kitaplıklar burada görüntülenecek.',
+    'shelves_new_empty' => 'En son oluşturulmuş kitaplıklar burada görüntülenecek.',
+    'shelves_save' => 'Kitaplığı Kaydet',
+    'shelves_books' => 'Bu kitaplıktaki kitaplar',
+    'shelves_add_books' => 'Bu kitaplığa kitap ekle',
+    'shelves_drag_books' => 'Bu kitaplığa kitap eklemek için kitapları buraya sürükle',
+    'shelves_empty_contents' => 'Bu kitaplığa henüz hiç bir kitap atanmamış',
+    'shelves_edit_and_assign' => 'Kitaplığa kitap eklemek için güncelle',
+    'shelves_edit_named' => ':name Kitaplığını Güncelle',
+    'shelves_edit' => 'Kitaplığı Güncelle',
+    'shelves_delete' => 'Kitaplığı Sil',
+    'shelves_delete_named' => ':name Kitaplığını Sil',
+    'shelves_delete_explain' => "Bu işlem :name kitaplığını silecektir. İçerdiği kitaplar silinmeyecektir.",
+    'shelves_delete_confirmation' => 'Bu kitaplığı silmek istediğinizden emin misiniz?',
+    'shelves_permissions' => 'Kitaplık İzinleri',
+    'shelves_permissions_updated' => 'Kitaplık İzinleri Güncellendi',
+    'shelves_permissions_active' => 'Kitaplık İzinleri Aktif',
+    '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 bu kitaplığa ait izinleri kaydettiğinizden emin olun.',
+    'shelves_copy_permission_success' => 'Kitaplık izinleri :count adet kitaba kopyalandı',
+
+    // Books
+    'book' => 'Kitap',
+    'books' => 'Kitaplar',
+    'x_books' => ':count Kitap|:count Kitaplar',
+    'books_empty' => 'Hiç kitap oluşturulmadı',
+    'books_popular' => 'Popüler Kitaplar',
+    'books_recent' => 'En Son Kitaplar',
+    'books_new' => 'Yeni Kitaplar',
+    'books_new_action' => 'Yeni Kitap',
+    'books_popular_empty' => 'En popüler kitaplar burada görüntülenecek.',
+    'books_new_empty' => 'En yeni kitaplar burada görüntülenecek.',
+    'books_create' => 'Yeni Kitap Oluştur',
+    'books_delete' => 'Kitabı Sil',
+    'books_delete_named' => ':bookName kitabını sil',
+    'books_delete_explain' => 'Bu işlem \':bookName\' kitabını silecek. Bütün sayfa ve bölümler silinecektir.',
+    'books_delete_confirmation' => 'Bu kitabı silmek istediğinizden emin misiniz?',
+    'books_edit' => 'Kitabı Güncelle',
+    'books_edit_named' => ':bookName Kitabını Güncelle',
+    'books_form_book_name' => 'Kitap Adı',
+    'books_save' => 'Kitabı Kaydet',
+    'books_permissions' => 'Kitap İzinleri',
+    'books_permissions_updated' => 'Kitap İzinleri Güncellendi',
+    'books_empty_contents' => 'Bu kitaba ait sayfa veya bölüm oluşturulmadı.',
+    'books_empty_create_page' => 'Yeni sayfa oluştur',
+    'books_empty_sort_current_book' => 'Mevcut kitabı sırala',
+    'books_empty_add_chapter' => 'Yeni bölüm ekle',
+    'books_permissions_active' => 'Kitap İzinleri Aktif',
+    'books_search_this' => 'Bu kitapta ara',
+    'books_navigation' => 'Kitap Navigasyonu',
+    'books_sort' => 'Kitap İçeriklerini Sırala',
+    'books_sort_named' => ':bookName Kitabını Sırala',
+    'books_sort_name' => 'İsme Göre Sırala',
+    'books_sort_created' => 'Oluşturulma Tarihine Göre Sırala',
+    'books_sort_updated' => 'Güncellenme Tarihine Göre Sırala',
+    'books_sort_chapters_first' => 'Önce Bölümler',
+    'books_sort_chapters_last' => 'En Son Bölümler',
+    'books_sort_show_other' => 'Diğer Kitapları Göster',
+    'books_sort_save' => 'Yeni Düzeni Kaydet',
+
+    // Chapters
+    'chapter' => 'Bölüm',
+    'chapters' => 'Bölümler',
+    'x_chapters' => ':count Bölüm|:count Bölümler',
+    'chapters_popular' => 'Popüler Bölümler',
+    'chapters_new' => 'Yeni Bölüm',
+    'chapters_create' => 'Yeni Bölüm Oluştur',
+    'chapters_delete' => 'Bölümü Sil',
+    'chapters_delete_named' => ':chapterName Bölümünü Sil',
+    'chapters_delete_explain' => 'Bu işlem \':chapterName\' kitabını silecek. Bütün sayfalar silinecek ve direkt olarak ana kitab eklenecektir.',
+    'chapters_delete_confirm' => 'Bölümü silmek istediğinizden emin misiniz?',
+    'chapters_edit' => 'Bölümü Güncelle',
+    'chapters_edit_named' => ':chapterName Bölümünü Güncelle',
+    'chapters_save' => 'Bölümü Kaydet',
+    'chapters_move' => 'Bölümü Taşı',
+    'chapters_move_named' => ':chapterName Bölümünü Taşı',
+    'chapter_move_success' => 'Bölüm :bookName Kitabına Taşındı',
+    'chapters_permissions' => 'Bölüm İzinleri',
+    'chapters_empty' => 'Bu bölümde henüz bir sayfa yok.',
+    'chapters_permissions_active' => 'Bölüm İzinleri Aktif',
+    'chapters_permissions_success' => 'Bölüm İzinleri Güncellendi',
+    'chapters_search_this' => 'Bu bölümü ara',
+
+    // Pages
+    'page' => 'Sayfa',
+    'pages' => 'Sayfalar',
+    'x_pages' => ':count Sayfa|:count Sayfalar',
+    'pages_popular' => 'Popüler Sayfalar',
+    'pages_new' => 'Yeni Sayfa',
+    'pages_attachments' => 'Ekler',
+    'pages_navigation' => 'Sayfa Navigasyonu',
+    'pages_delete' => 'Sayfayı Sil',
+    'pages_delete_named' => ':pageName Sayfasını Sil',
+    'pages_delete_draft_named' => ':pageName Taslak Sayfasını Sil',
+    'pages_delete_draft' => 'Taslak Sayfayı Sil',
+    'pages_delete_success' => 'Sayfa silindi',
+    'pages_delete_draft_success' => 'Taslak sayfa silindi',
+    'pages_delete_confirm' => 'Bu sayfayı silmek istediğinizden emin misiniz?',
+    'pages_delete_draft_confirm' => 'Bu taslak sayfayı silmek istediğinizden emin misiniz?',
+    'pages_editing_named' => ':pageName Sayfası Düzenleniyor',
+    'pages_edit_save_draft' => 'Taslağı Kaydet',
+    'pages_edit_draft' => 'Taslak Sayfasını Düzenle',
+    'pages_editing_draft' => 'Taslak Düzenleniyor',
+    'pages_editing_page' => 'Sayfa Düzenleniyor',
+    'pages_edit_draft_save_at' => 'Taslak kaydedildi ',
+    'pages_edit_delete_draft' => 'Taslağı Sl',
+    'pages_edit_discard_draft' => 'Taslağı Yoksay',
+    'pages_edit_set_changelog' => 'Değişiklik Logunu Kaydet',
+    'pages_edit_enter_changelog_desc' => 'Yaptığınız değişiklikler hakkında kısa bir bilgilendirme ekleyin',
+    'pages_edit_enter_changelog' => 'Değişim Günlüğü Ekleyin',
+    'pages_save' => 'Sayfayı Kaydet',
+    'pages_title' => 'Sayfa Başlığı',
+    'pages_name' => 'Sayfa İsmi',
+    'pages_md_editor' => 'Editör',
+    'pages_md_preview' => 'Önizleme',
+    'pages_md_insert_image' => 'Görsel Ekle',
+    'pages_md_insert_link' => 'Öge Linki Ekle',
+    'pages_md_insert_drawing' => 'Çizim Ekle',
+    'pages_not_in_chapter' => 'Sayfa Bu Bölümde Değil',
+    'pages_move' => 'Sayfayı Taşı',
+    'pages_move_success' => 'Sayfa ":parentName"\'a taşındı',
+    'pages_copy' => 'Sayfayı Kopyala',
+    'pages_copy_desination' => 'Kopyalanacak Hedef',
+    'pages_copy_success' => 'Sayfa başarıyla kopyalandı',
+    'pages_permissions' => 'Sayfa İzinleri',
+    'pages_permissions_success' => 'Sayfa izinleri güncellendi',
+    'pages_revision' => 'Revizyon',
+    'pages_revisions' => 'Sayfa Revizyonları',
+    'pages_revisions_named' => ':pageName için Sayfa Revizyonları',
+    'pages_revision_named' => ':pageName için Sayfa Revizyonu',
+    'pages_revisions_created_by' => 'Oluşturan',
+    'pages_revisions_date' => 'Revizyon Tarihi',
+    'pages_revisions_number' => '#',
+    'pages_revisions_numbered' => 'Revizyon #:id',
+    'pages_revisions_numbered_changes' => 'Revizyon #:id Değişiklikleri',
+    'pages_revisions_changelog' => 'Değişim Günlüğü',
+    'pages_revisions_changes' => 'Değişiklikler',
+    'pages_revisions_current' => 'Mevcut Versiyon',
+    'pages_revisions_preview' => 'Önizleme',
+    'pages_revisions_restore' => 'Kurtar',
+    'pages_revisions_none' => 'Bu sayfaya ait revizyon yok',
+    'pages_copy_link' => 'Linki kopyala',
+    'pages_edit_content_link' => 'İçeriği Düzenle',
+    'pages_permissions_active' => 'Sayfa İzinleri Aktif',
+    'pages_initial_revision' => 'İlk Yayın',
+    'pages_initial_name' => 'Yeni Sayfa',
+    'pages_editing_draft_notification' => 'Şu anda :timeDiff tarhinde kaydedilmiş olan taslağı düzenlemektesiniz.',
+    'pages_draft_edited_notification' => 'Bu sayfa son girişinizden bu yana güncellendi. Değişiklikleri yoksayıp, kaydetmeden çıkmanız önerilir.',
+    'pages_draft_edit_active' => [
+        'start_a' => ':count kullanıcı bu sayfayı düzenlemeye başladı',
+        'start_b' => ':userName kullanıcısı bu sayfayı düzenlemeye başladı',
+        'time_a' => 'sayfa son güncellendiğinden beri',
+        'time_b' => 'son :minCount dakikada',
+        'message' => ':start :time. Birbirinizin düzenlemelerinin çakışmamasına dikkat edin!',
+    ],
+    'pages_draft_discarded' => 'Taslak yok sayıldı, editör mevcut sayfa içeriği ile güncellendi',
+    'pages_specific' => 'Özel Sayfa',
+
+    // Editor Sidebar
+    'page_tags' => 'Sayfa Etiketleri',
+    'chapter_tags' => 'Bölüm Etiketleri',
+    'book_tags' => 'Kitap Etiketleri',
+    'shelf_tags' => 'Kitaplık Etiketleri',
+    'tag' => 'Etiket',
+    'tags' =>  'Etiketler',
+    'tag_value' => 'Etiket İçeriği (Opsiyonel)',
+    'tags_explain' => "İçeriğini daha iyi kategorize etmek için bazı etiketler ekle. Etiketlere değer atayarak daha derin bir organizasyon yapısına sahip olabilirsin.",
+    'tags_add' => 'Başka etiket ekle',
+    'attachments' => 'Ekler',
+    'attachments_explain' => 'Sayfanızda göstermek için bazı dosyalar yükleyin veya bazı bağlantılar ekleyin. Bunlar sayfanın sidebarında görülebilir.',
+    'attachments_explain_instant_save' => 'Burada yapılan değişiklikler anında kaydedilir.',
+    'attachments_items' => 'Eklenmiş Ögeler',
+    'attachments_upload' => 'Dosya Yükle',
+    'attachments_link' => 'Link Ekle',
+    'attachments_set_link' => 'Link Düzenle',
+    'attachments_delete_confirm' => 'Eki gerçekten silmek istiyor musunuz?',
+    'attachments_dropzone' => 'Dosyaları buraya sürükle veya  eklemek için buraya tıkla',
+    'attachments_no_files' => 'Hiç bir dosya yüklenmedi',
+    'attachments_explain_link' => 'Eğer dosya yüklememeyi tercih ederseniz link ekleyebilirsiniz. Bu başka bir sayfaya veya buluttaki bir dosyanın linki olabilir.',
+    'attachments_link_name' => 'Bağlantı Adı',
+    'attachment_link' => 'Ek linki',
+    'attachments_link_url' => 'Dosya linki',
+    'attachments_link_url_hint' => 'Dosyanın veya sitenin url adres',
+    'attach' => 'Ekle',
+    'attachments_edit_file' => 'Dosyayı Düzenle',
+    'attachments_edit_file_name' => 'Dosya Adı',
+    'attachments_edit_drop_upload' => 'Dosyaları sürükle veya yüklemek için buraya tıkla',
+    'attachments_order_updated' => 'Ek sırası güncellendi',
+    'attachments_updated_success' => 'Ek detayları güncellendi',
+    'attachments_deleted' => 'Ek silindi',
+    'attachments_file_uploaded' => 'Dosya başarıyla yüklendi',
+    'attachments_file_updated' => 'Dosya başarıyla güncellendi',
+    'attachments_link_attached' => 'Link sayfaya başarıyla eklendi',
+
+    // Profile View
+    'profile_user_for_x' => 'Kullanıcı :time',
+    'profile_created_content' => 'Oluşturulan İçerik',
+    'profile_not_created_pages' => ':userName herhangi bir sayfa oluşturmadı',
+    'profile_not_created_chapters' => ':userName herhangi bir bölüm oluşturmadı',
+    'profile_not_created_books' => ':userName herhangi bir kitap oluşturmadı',
+    'profile_not_created_shelves' => ':userName herhangi bir kitaplık oluşturmadı',
+
+    // Comments
+    'comment' => 'Yorum',
+    'comments' => 'Yorumlar',
+    'comment_add' => 'Yorum Ekle',
+    'comment_placeholder' => 'Buraya yorum ekle',
+    'comment_count' => '{0} Yorum Yok|{1} 1 Yorum|[2,*] :count Yorun',
+    'comment_save' => 'Yorum Kaydet',
+    'comment_saving' => 'Yorum kaydediliyor...',
+    'comment_deleting' => 'Yorum siliniyor...',
+    'comment_new' => 'Yeni Yorum',
+    'comment_created' => 'yorum yaptı :createDiff',
+    'comment_updated' => ':username tarafından :updateDiff önce güncellendi',
+    'comment_deleted_success' => 'Yorum silindi',
+    'comment_created_success' => 'Yorum eklendi',
+    'comment_updated_success' => 'Yorum güncellendi',
+    'comment_delete_confirm' => 'Bu yorumu silmek istediğinizden emin misiniz?',
+    'comment_in_reply_to' => ':commentId yorumuna yanıt olarak',
+
+    // Revision
+    'revision_delete_confirm' => 'Bu revizyonu silmek istediğinizden emin misiniz?',
+    'revision_restore_confirm' => 'Bu revizyonu yeniden yüklemek istediğinizden emin misiniz? Mevcut sayfa içeriği değiştirilecektir.',
+    'revision_delete_success' => 'Revizyon silindi',
+    'revision_cannot_delete_latest' => 'Son revizyon silinemez.'
+];
\ No newline at end of file
diff --git a/resources/lang/tr/errors.php b/resources/lang/tr/errors.php
new file mode 100644 (file)
index 0000000..962057e
--- /dev/null
@@ -0,0 +1,84 @@
+<?php
+/**
+ * Text shown in error messaging.
+ */
+return [
+
+    // Permissions
+    'permission' => 'Bu sayfaya erişme yetkiniz yok.',
+    'permissionJson' => 'Bu işlemi yapmak için yetkiniz yo.',
+
+    // Auth
+    'error_user_exists_different_creds' => ':email adresi farklı kullanıcı bilgileri ile zaten kullanımda.',
+    'email_already_confirmed' => 'E-mail halihazırda onaylanmış, giriş yapmayı dene.',
+    'email_confirmation_invalid' => 'Bu doğrulama tokenı daha önce kullanılmış veya geçerli değil, lütfen tekrar kayıt olmayı deneyin.',
+    'email_confirmation_expired' => 'Doğrulama token\'ının süresi geçmiş, yeni bir mail gönderildi.',
+    'ldap_fail_anonymous' => 'Anonim LDAP girişi başarısız oldu',
+    'ldap_fail_authed' => 'Verdiğiniz bilgiler ile LDAP girişi başarısız oldu.',
+    'ldap_extension_not_installed' => 'LDAP PHP eklentisi yüklenmedi',
+    'ldap_cannot_connect' => 'LDAP sunucusuna bağlanılamadı, ilk bağlantı başarısız oldu',
+    'social_no_action_defined' => 'Bir aksiyon tanımlanmadı',
+    'social_login_bad_response' => ":socialAccount girişi sırasında hata oluştu: \n:error",
+    'social_account_in_use' => 'Bu :socialAccount zaten kullanımda, :socialAccount hesabıyla giriş yapmayı deneyin.',
+    'social_account_email_in_use' => ':email adresi zaten kullanımda. Eğer zaten bir hesabınız varsa :socialAccount hesabınızı profil ayarları kısmından bağlayabilirsiniz.',
+    'social_account_existing' => 'Bu :socialAccount zaten profilinize eklenmiş.',
+    'social_account_already_used_existing' => 'Bu :socialAccount başka bir kullanıcı tarafından kullanılıyor.',
+    'social_account_not_used' => 'Bu :socialAccount hesabı hiç bir kullanıcıya bağlı değil. Lütfen profil ayarlarına gidiniz ve bağlayınız. ',
+    'social_account_register_instructions' => 'Hala bir hesabınız yoksa :socialAccount ile kayıt olabilirsiniz.',
+    'social_driver_not_found' => 'Social driver bulunamadı',
+    'social_driver_not_configured' => ':socialAccount ayarlarınız doğru bir şekilde ayarlanmadı.',
+
+    // System
+    'path_not_writable' => ':filePath dosya yolu yüklenemedi. Sunucuya yazılabilir olduğundan emin olun.',
+    'cannot_get_image_from_url' => ':url\'den görsel alınamadı',
+    'cannot_create_thumbs' => 'Sunucu küçük resimleri oluşturamadı. Lütfen GD PHP eklentisinin yüklü olduğundan emin olun.',
+    'server_upload_limit' => 'Sunucu bu boyutta dosya yüklemenize izin vermiyor. Lütfen daha küçük boyutta dosya yüklemeyi deneyiniz.',
+    'uploaded'  => 'Sunucu bu boyutta dosya yüklemenize izin vermiyor. Lütfen daha küçük boyutta dosya yüklemeyi deneyiniz.',
+    'image_upload_error' => 'Görsel yüklenirken bir hata oluştu',
+    'image_upload_type_error' => 'Yüklemeye çalıştığınız dosya türü geçerli değildir',
+    'file_upload_timeout' => 'Dosya yüklemesi zaman aşımına uğradı',
+
+    // Attachments
+    'attachment_page_mismatch' => 'Ek güncellemesi sırasında sayfa uyuşmazlığı yaşandı',
+    'attachment_not_found' => 'Ek bulunamadı',
+
+    // Pages
+    'page_draft_autosave_fail' => 'Taslak kaydetme başarısız. Sayfanızı kaydetmeden önce internet bağlantınız olduğundan emin olun',
+    'page_custom_home_deletion' => 'Bu sayfa anasayfa olarak ayarlandığı için silinemez',
+
+    // Entities
+    'entity_not_found' => 'Eleman bulunamadı',
+    'bookshelf_not_found' => 'Kitaplık bulunamadı',
+    'book_not_found' => 'Kitap bulunamadı',
+    'page_not_found' => 'Sayfa bulunamadı',
+    'chapter_not_found' => 'Bölüm bulunamadı',
+    'selected_book_not_found' => 'Seçilen kitap bulunamadı',
+    'selected_book_chapter_not_found' => 'Seçilen kitap veya bölüm bulunamadı',
+    'guests_cannot_save_drafts' => 'Misafirler taslak kaydedemezler',
+
+    // Users
+    'users_cannot_delete_only_admin' => 'Tek olan yöneticiyi silemezsiniz',
+    'users_cannot_delete_guest' => 'Misafir kullanıyıcıyı silemezsiniz',
+
+    // Roles
+    'role_cannot_be_edited' => 'Bu rol düzenlenemez',
+    'role_system_cannot_be_deleted' => 'Bu bir yönetici rolüdür ve silinemez',
+    'role_registration_default_cannot_delete' => 'Bu rol varsayılan yönetici rolü olarak atandığı için silinemez ',
+    'role_cannot_remove_only_admin' => 'Bu kullanıcı yönetici rolü olan tek kullanıcı olduğu için silinemez. Bu kullanıcıyı silmek için önce başka bir kullanıcıya yönetici rolü atayın.',
+
+    // Comments
+    'comment_list' => 'Yorumlar yüklenirken bir hata oluştu.',
+    'cannot_add_comment_to_draft' => 'Taslaklara yorum ekleyemezsiniz.',
+    'comment_add' => 'Yorum eklerken/güncellerken bir hata olıuştu.',
+    'comment_delete' => 'Yorum silinirken bir hata oluştu.',
+    'empty_comment' => 'Boş bir yorum eklenemez.',
+
+    // Error pages
+    '404_page_not_found' => 'Sayfa Bulunamadı',
+    'sorry_page_not_found' => 'Üzgünüz, aradığınız sayfa bulunamıyor.',
+    'return_home' => 'Anasayfaya dön',
+    'error_occurred' => 'Bir Hata Oluştu',
+    'app_down' => ':appName şu anda inaktif',
+    'back_soon' => 'En kısa zamanda aktif hale gelecek.',
+
+];
diff --git a/resources/lang/tr/pagination.php b/resources/lang/tr/pagination.php
new file mode 100644 (file)
index 0000000..8550a18
--- /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; Önceki',
+    'next'     => 'Sonraki &raquo;',
+
+];
diff --git a/resources/lang/tr/passwords.php b/resources/lang/tr/passwords.php
new file mode 100644 (file)
index 0000000..42e9ef2
--- /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' => 'Parolanız en az 6 karakterden oluşmalı ve doğrulama parolası ile eşleşmelidir. ',
+    'user' => "Bu e-mail adresi ile ilişkilendirilmiş bir kullanıcı bulamadık.",
+    'token' => 'Parola yenileme tokeni geçerli değil.',
+    'sent' => 'Parola sıfırlanma bağlantısını e-mail adresinize gönderdik!',
+    'reset' => 'Parolanız sıfırlandı!',
+
+];
diff --git a/resources/lang/tr/settings.php b/resources/lang/tr/settings.php
new file mode 100755 (executable)
index 0000000..cf20b06
--- /dev/null
@@ -0,0 +1,134 @@
+<?php
+/**
+ * Settings text strings
+ * Contains all text strings used in the general settings sections of BookStack
+ * including users and roles.
+ */
+return [
+
+    // Common Messages
+    'settings' => 'Ayarlar',
+    'settings_save' => 'Ayarları Kaydet',
+    'settings_save_success' => 'Ayarlar Kaydedildi',
+
+    // App Settings
+    'app_customization' => 'Özelleştirme',
+    'app_features_security' => 'Özellikler & Güvenlik',
+    'app_name' => 'Uygulama Adı',
+    'app_name_desc' => 'Bu isim başlıkta ve sistem tarafında gönderilen tüm mesajlarda gösterilecektir.',
+    'app_name_header' => 'İsmi başlıkta göster',
+    'app_public_access' => 'Açık Erişim',
+    'app_public_access_desc' => 'Bu özelliği aktif etmek giriş yapmamış misafir kullanıcıların sizin BookStack uygulamanıza erişmesini sağlar',
+    'app_public_access_desc_guest' => 'Kayıtlı olmayan kullanıcılar için erişim yetkisi "Guest" kullanıcısı üzerinden düzenlenebilir.',
+    'app_public_access_toggle' => 'Açık erişime izin ver',
+    'app_public_viewing' => 'Herkese açık görüntülenmeye izin verilsin mi?',
+    'app_secure_images' => 'Daha Yüksek Güvenlikli Görsel Yüklemeleri',
+    'app_secure_images_toggle' => 'Daha yüksek güveblikli görsel yüklemelerine izin ver',
+    'app_secure_images_desc' => 'Performans sebepleri nedeniyle bütün görseller halka açık. Bu opsiyon rastgele ve tahmin edilmesi zor dizileri görsel linklerinin önüne ekler. Dizin indexlerinin kapalı olduğundan emin olun.',
+    'app_editor' => 'Sayfa Editörü',
+    'app_editor_desc' => 'Sayfa düzenlemesi yapılırken hangi editörün kullanılacağını seçin.',
+    'app_custom_html' => 'Özel HTML Head İçeriği',
+    'app_custom_html_desc' => 'Buraya eklenecek olan içerik <head> taginin en sonuna eklenecektir. Bu stilleri override ederken veya analytics eklerken faydalı bir kullanım şeklidir.',
+    'app_custom_html_disabled_notice' => 'Yapılan hatalı değişikliklerin geriye alınabilmesi için bu sayfada özel HTML head içeriği kapalı.',
+    'app_logo' => 'Uygulama Logosu',
+    'app_logo_desc' => 'Bu görsel 43px yüksekliğinde olmalı. <br>Büyük görseller ölçeklenecektir.',
+    'app_primary_color' => 'Uygulamanın Birincil Rengi',
+    'app_primary_color_desc' => 'Bu bir hex değeri olmalıdır. <br>Varsayılan rengi seçmek için boş bırakın.',
+    'app_homepage' => 'Uygulama Anasayfası',
+    'app_homepage_desc' => 'Anasayfada görünmesi için bir view seçin. Sayfa izinleri seçili sayfalar için yok sayılacaktır.',
+    'app_homepage_select' => 'Sayfa seçiniz',
+    'app_disable_comments' => 'Yorumları Engelle',
+    'app_disable_comments_toggle' => 'Yorumları engelle',
+    'app_disable_comments_desc' => 'Yorumları uygulamadaki bütün sayfalar için engelle. <br> Mevcut yorumlar gösterilmeyecektir.',
+
+    // Registration Settings
+    'reg_settings' => 'Kayıt',
+    'reg_enable' => 'Kaydolmaya İzin Ver',
+    'reg_enable_toggle' => 'Kaydolmaya izin ver',
+    'reg_enable_desc' => 'Kayıt olmaya izin verdiğinizde kullanıcılar kendilerini uygulamaya kaydedebilecekler. Kayıt olduktan sonra kendilerine varsayılan kullanıcı rolü atanacaktır.',
+    'reg_default_role' => 'Kayıt olduktan sonra varsayılan kullanıcı rolü',
+    'reg_email_confirmation' => 'Email Doğrulama',
+    'reg_email_confirmation_toggle' => 'E-mail onayı gerektir',
+    'reg_confirm_email_desc' => 'Eğer domain kısıtlaması kullanılıyorsa o zaman email doğrulaması gereklidir ve bu seçenek yok sayılacaktır.',
+    'reg_confirm_restrict_domain' => 'Domain Kısıtlaması',
+    'reg_confirm_restrict_domain_desc' => 'Kısıtlamak istediğiniz email domainlerini vigül ile ayırarak yazınız. Kullanıcılara uygulamaya erişmeden önce adreslerini doğrulamak için bir mail gönderilecektir. <br> Kullanıcılar başarıyla kaydolduktan sonra email adreslerini değiştiremeyeceklerdir.',
+    'reg_confirm_restrict_domain_placeholder' => 'Hiçbir kısıtlama tanımlanmamış',
+
+    // Maintenance settings
+    'maint' => 'Bakım',
+    'maint_image_cleanup' => 'Görsel Temizliği',
+    'maint_image_cleanup_desc' => "Sayfaları ve revizyon içeriklerini tarayarak hangi gösel ve çizimlerin kullanımda olduğunu ve hangilerinin gereksiz olduğunu tespit eder. Bunu başlatmadan veritabanı ve görsellerin tam bir yedeğinin alındığından emin olun.",
+    'maint_image_cleanup_ignore_revisions' => 'Revizyonlardaki görselleri yoksay',
+    'maint_image_cleanup_run' => 'Temizliği Başlat',
+    'maint_image_cleanup_warning' => ':count potansiyel kullanılmayan görsel bulundu. Bu görselleri silmek istediğinizden emin misiniz?',
+    'maint_image_cleanup_success' => ':count potanisyel kullanılmayan görsel bulundu ve silindi!',
+    'maint_image_cleanup_nothing_found' => 'Kullanılmayan görsel bulunamadı ve birşey silinmedi!',
+
+    // Role Settings
+    'roles' => 'Roller',
+    'role_user_roles' => 'Kullanıcı Rolleri',
+    'role_create' => 'Yeni Rol Oluştur',
+    'role_create_success' => 'Rol Başarıyla Oluşturuldu',
+    'role_delete' => 'Rolü Sil',
+    'role_delete_confirm' => 'Bu işlem \':roleName\' rolünü silecektir.',
+    'role_delete_users_assigned' => 'Bu role atanmış :userCount adet kullanıcı var. Eğer bu kullanıcıların rollerini değiştirmek istiyorsanız aşağıdan yeni bir rol seçin.',
+    'role_delete_no_migration' => "Kullanıcıları taşıma",
+    'role_delete_sure' => 'Bu rolü silmek istediğinizden emin misiniz?',
+    'role_delete_success' => 'Rol başarıyla silindi',
+    'role_edit' => 'Rolü Düzenle',
+    'role_details' => 'Rol Detayları',
+    'role_name' => 'Rol Adı',
+    'role_desc' => 'Rolün Kısa Tanımı',
+    'role_external_auth_id' => 'Harici Authentication ID\'leri',
+    'role_system' => 'Sistem Yetkileri',
+    'role_manage_users' => 'Kullanıcıları yönet',
+    'role_manage_roles' => 'Rolleri ve rol izinlerini yönet',
+    'role_manage_entity_permissions' => 'Bütün kitap, bölüm ve sayfa izinlerini yönet',
+    'role_manage_own_entity_permissions' => 'Sahip olunan kitap, bölüm ve sayfaların izinlerini yönet',
+    'role_manage_settings' => 'Uygulama ayarlarını yönet',
+    'role_asset' => 'Asset Yetkileri',
+    'role_asset_desc' => 'Bu izinleri assetlere sistem içinden varsayılan erişimi kontrol eder. Kitaplar, bölümler ve sayfaların izinleri bu izinleri override eder.',
+    'role_asset_admins' => 'Yöneticilere otomatik olarak bütün içeriğe erişim yetkisi verilir fakat bu opsiyonlar UI özelliklerini gösterir veya gizler.',
+    'role_all' => 'Hepsi',
+    'role_own' => 'Sahip Olunan',
+    'role_controlled_by_asset' => 'Yükledikleri asset tarafından kontrol ediliyor',
+    'role_save' => 'Rolü Kaydet',
+    'role_update_success' => 'Rol başarıyla güncellendi',
+    'role_users' => 'Bu roldeki kullanıcılar',
+    'role_users_none' => 'Bu role henüz bir kullanıcı atanmadı',
+
+    // Users
+    'users' => 'Kullanıcılar',
+    'user_profile' => 'Kullanıcı Profili',
+    'users_add_new' => 'Yeni Kullanıcı Ekle',
+    'users_search' => 'Kullanıcıları Ara',
+    'users_details' => 'Kullanıcı Detayları',
+    'users_details_desc' => 'Bu kullanıcı için gösterilecek bir isim ve mail adresi belirleyin. Bu e-mail adresi kullanıcı tarafından giriş yaparken kullanılacak.',
+    'users_details_desc_no_email' => 'Diğer kullanıcılar tarafından tanınabilmesi için bir isim belirleyin.',
+    'users_role' => 'Kullanıcı Rolleri',
+    'users_role_desc' => 'Bu kullanıcının hangi rollere atanabileceğini belirleyin. Eğer bir kullanıcıya birden fazla rol atanırsa, kullanıcı bütün rollerin özelliklerini kullanabilir.',
+    'users_password' => 'Kullanıcı Parolası',
+    'users_password_desc' => 'Kullanıcının giriş yaparken kullanacağı bir parola belirleyin. Parola en az 5 karakter olmalıdır.',
+    'users_external_auth_id' => 'Harici Authentication ID\'si',
+    'users_external_auth_id_desc' => 'Bu ID kullanıcı LDAP sunucu ile bağlantı kurarken kullanılır.',
+    'users_password_warning' => 'Sadece parolanızı değiştirmek istiyorsanız aşağıyı doldurunuz.',
+    'users_system_public' => 'Bu kullanıcı sizin uygulamanızı ziyaret eden bütün misafir kullanıcıları temsil eder. Giriş yapmak için kullanılamaz, otomatik olarak atanır.',
+    'users_delete' => 'Kullanıcı Sil',
+    'users_delete_named' => ':userName kullanıcısını sil ',
+    'users_delete_warning' => 'Bu işlem \':userName\' kullanıcısını sistemden tamamen silecektir.',
+    'users_delete_confirm' => 'Bu kullanıcıyı tamamen silmek istediğinize emin misiniz?',
+    'users_delete_success' => 'Kullanıcılar başarıyla silindi.',
+    'users_edit' => 'Kullanıcıyı Güncelle',
+    'users_edit_profile' => 'Profili Düzenle',
+    'users_edit_success' => 'Kullanıcı başarıyla güncellendi',
+    'users_avatar' => 'Kullanıcı Avatarı',
+    'users_avatar_desc' => 'Bu kullanıcıyı temsil eden bir görsel seçin. Yaklaşık 256px kare olmalıdır.',
+    'users_preferred_language' => 'Tercih Edilen Dil',
+    'users_preferred_language_desc' => 'Bu seçenek kullanıcı arayüzünün dilini değiştirecektir. Herhangi bir kullanıcı içeriğini etkilemeyecektir.',
+    'users_social_accounts' => 'Sosyal Hesaplar',
+    'users_social_accounts_info' => 'Burada diğer hesaplarınızı ekleyerek daha hızlı ve kolay giriş sağlayabilirsiniz. Bir hesabın bağlantısını kesmek daha önce edilnilen erişiminizi kaldırmaz. Profil ayarlarınızdan bağlı sosyal hesabınızın erişimini kaldırınız.',
+    'users_social_connect' => 'Hesap Bağla',
+    'users_social_disconnect' => 'Hesabın Bağlantısını Kes',
+    'users_social_connected' => ':socialAccount hesabı profilinize başarıyla bağlandı.',
+    'users_social_disconnected' => ':socialAccount hesabınızın profilinizle ilişiği başarıyla kesildi.',
+];
diff --git a/resources/lang/tr/validation.php b/resources/lang/tr/validation.php
new file mode 100644 (file)
index 0000000..e7683a0
--- /dev/null
@@ -0,0 +1,85 @@
+<?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 kabul edilmelidir.',
+    'active_url'           => ':attribute geçerli bir URL adresi değildir.',
+    'after'                => ':attribute :date tarihinden sonra bir tarih olmalıdır.',
+    'alpha'                => ':attribute sadece harflerden oluşabilir.',
+    'alpha_dash'           => ':attribute sadece harf, rakam ve tirelerden oluşabilir.',
+    'alpha_num'            => ':attribute sadece harf ve rakam oluşabilir.',
+    'array'                => ':attribute array olmalıdır..',
+    'before'               => ':attribute :date tarihinden önce bir tarih olmalıdır.',
+    'between'              => [
+        'numeric' => ':attribute, :min ve :max değerleri arasında olmalıdır.',
+        'file'    => ':attribute, :min ve :max kilobyte boyutları arasında olmalıdır.',
+        'string'  => ':attribute, :min ve :max karakter arasında olmalıdır.',
+        'array'   => ':attribute :min ve :max öge arasında olmalıdır.',
+    ],
+    'boolean'              => ':attribute true veya false olmalıdır.',
+    'confirmed'            => ':attribute doğrulaması eşleşmiyor.',
+    'date'                 => ':attribute geçerli bir tarih değil.',
+    'date_format'          => ':attribute formatı :format\'ına uymuyor.',
+    'different'            => ':attribute be :other birbirinden farklı olmalıdır.',
+    'digits'               => ':attribute :digits basamaklı olmalıdır.',
+    'digits_between'       => ':attribute :min ve :max basamaklı olmalıdır.',
+    'email'                => ':attribute geçerli bir e-mail adresi olmalıdır.',
+    'filled'               => ':attribute gerekli bir alandır.',
+    'exists'               => 'Seçilen :attribute geçerli bir alan değildir.',
+    'image'                => ':attribute bir görsel olmalıdır.',
+    'image_extension'      => ':attribute geçerli ve desteklenen bir görsel uzantısı değildir.',
+    'in'                   => 'Seçilen :attribute geçerli değildir.',
+    'integer'              => ':attribute bir integer değeri olmalıdır.',
+    'ip'                   => ':attribute geçerli bir IP adresi olmalıdır.',
+    'max'                  => [
+        'numeric' => ':attribute, :max değerinden büyük olmamalıdır.',
+        'file'    => ':attribute, :max kilobyte boyutundan büyük olmamalıdır.',
+        'string'  => ':attribute, :max karakter boyutundan büyük olmamalıdır.',
+        'array'   => ':attribute, en fazla :max öge içermelidir.',
+    ],
+    'mimes'                => ':attribute :values dosya tipinde olmalıdır.',
+    'min'                  => [
+        'numeric' => ':attribute, :min değerinden az olmamalıdır.',
+        'file'    => ':attribute, :min kilobyte boyutundan küçük olmamalıdır.',
+        'string'  => ':attribute, :min karakter boyutundan küçük olmamalıdır.',
+        'array'   => ':attribute, en az :min öge içermelidir.',
+    ],
+    'no_double_extension'  => ':attribute sadece tek bir dosya tipinde olmalıdır.',
+    'not_in'               => 'Seçili :attribute geçerli değildir.',
+    'numeric'              => ':attribute rakam olmalıdır.',
+    'regex'                => ':attribute formatı geçerli değildir.',
+    'required'             => 'The :attribute field is required. :attribute alanı gereklidir.',
+    'required_if'          => ':other alanı :value değerinde ise :attribute alanı gereklidir.',
+    'required_with'        => 'Eğer :values değeri geçerli ise :attribute alanı gereklidir.',
+    'required_with_all'    => 'Eğer :values değeri geçerli ise :attribute alanı gereklidir. ',
+    'required_without'     => 'Eğer :values değeri geçerli değil ise :attribute alanı gereklidir.',
+    'required_without_all' => 'Eğer :values değerlerinden hiçbiri geçerli değil ise :attribute alanı gereklidir.',
+    'same'                 => ':attribute ve :other eşleşmelidir.',
+    'size'                 => [
+        'numeric' => ':attribute, :size boyutunda olmalıdır.',
+        'file'    => ':attribute, :size kilobyte boyutunda olmalıdır.',
+        'string'  => ':attribute, :size karakter uzunluğunda olmalıdır.',
+        'array'   => ':attribute, :size sayıda öge içermelidir.',
+    ],
+    'string'               => ':attribute string olmalıdır.',
+    'timezone'             => ':attribute geçerli bir alan olmalıdır.',
+    'unique'               => ':attribute daha önce alınmış.',
+    'url'                  => ':attribute formatı geçerli değil.',
+    'uploaded'             => 'Dosya yüklemesi başarısız oldu. Server bu boyutta dosyaları kabul etmiyor olabilir.',
+
+    // Custom validation lines
+    'custom' => [
+        'password-confirm' => [
+            'required_with' => 'Parola onayı gereklidir.',
+        ],
+    ],
+
+    // Custom validation attributes
+    'attributes' => [],
+];
index cd73f92db239428f4361720c61c38a7f80a56bf9..32b35a156aa52adcb5cbc1ef87bb257c984d2319 100644 (file)
@@ -21,7 +21,7 @@ return [
     'email' => 'Email',
     'password' => 'Пароль',
     'password_confirm' => 'Підтвердження пароля',
-    'password_hint' => 'Має бути більше 5 символів',
+    'password_hint' => 'Має бути більше 7 символів',
     'forgot_password' => 'Забули пароль?',
     'remember_me' => 'Запам’ятати мене',
     'ldap_email_hint' => 'Введіть email для цього облікового запису.',
index 046f2360b6c85b6b05c021adfaa287af36eaae77..f4159826c52f1aed4351e4d2004980fb7e6ecf28 100644 (file)
@@ -27,7 +27,7 @@ return [
     'email' => 'Email地址',
     'password' => '密码',
     'password_confirm' => '确认密码',
-    'password_hint' => '必须超过5个字符',
+    'password_hint' => '必须超过7个字符',
     'forgot_password' => '忘记密码?',
     'remember_me' => '记住我',
     'ldap_email_hint' => '请输入用于此帐户的电子邮件。',
index f44ac8af0560dfd217f241be6c5bd5b2350c6727..9e7fcd036e92f627beb83bdc85e9be9338acecc5 100644 (file)
@@ -27,7 +27,7 @@ return [
     'email' => 'Email位址',
     'password' => '密碼',
     'password_confirm' => '確認密碼',
-    'password_hint' => '必須超過5個字元',
+    'password_hint' => '必須超過7個字元',
     'forgot_password' => '忘記密碼?',
     'remember_me' => '記住我',
     'ldap_email_hint' => '請輸入用於此帳號的電子郵件。',
similarity index 97%
rename from resources/assets/sass/_blocks.scss
rename to resources/sass/_blocks.scss
index c43ff7f78c4f3c3b290bf40561e4e4eb06e6cfe0..2cb17a18db90e8f7185ffde7e52973ae1dd249b3 100644 (file)
   line-height: 1;
 }
 
+.card.border-card {
+  border: 1px solid #DDD;
+}
+
 .card.drag-card {
   border: 1px solid #DDD;
   border-radius: 4px;
   }
 }
 
-.bookshelf-grid-item .grid-card-content h2 a  {
-  color: $color-bookshelf;
-  fill: $color-bookshelf;
-}
-
 .book-grid-item .grid-card-footer {
   p.small {
     font-size: .8em;
   margin-left: auto;
   margin-right: auto;
   margin-bottom: $-xl;
-  overflow: auto;
+  overflow: initial;
   min-height: 60vh;
   &.auto-height {
     min-height: 0;
 }
 @include smaller-than($s) {
   .content-wrap.card {
-    padding: $-m $-s;
+    padding: $-m $-m;
   }
 }
 
similarity index 60%
rename from resources/assets/sass/_buttons.scss
rename to resources/sass/_buttons.scss
index eb7a09342ad60079503534da6294393af6a75fbc..e3d9e17cad825beec15792dac617ce3914c45d9b 100644 (file)
@@ -1,29 +1,9 @@
 button {
+  background-color: transparent;
+  border: 0;
   font-size: 100%;
 }
 
-@mixin generate-button-colors($textColor, $backgroundColor) {
-  background-color: $backgroundColor;
-  color: $textColor;
-  fill: $textColor;
-  border: 1px solid $backgroundColor;
-  &:hover {
-    background-color: lighten($backgroundColor, 8%);
-    color: $textColor;
-  }
-  &:active {
-    background-color: darken($backgroundColor, 8%);
-  }
-  &:focus {
-    background-color: lighten($backgroundColor, 4%);
-    box-shadow: $bs-light;
-    color: $textColor;
-  }
-}
-
-// Button Specific Variables
-$button-border-radius: 2px;
-
 .button  {
   text-decoration: none;
   font-size: 0.85rem;
@@ -34,34 +14,54 @@ $button-border-radius: 2px;
   display: inline-block;
   font-weight: 400;
   outline: 0;
-  border-radius: $button-border-radius;
+  border-radius: 2px;
   cursor: pointer;
-  transition: background-color ease-in-out 120ms, box-shadow ease-in-out 120ms;
+  transition: background-color ease-in-out 120ms,
+    filter ease-in-out 120ms,
+    box-shadow ease-in-out 120ms;
   box-shadow: none;
-  background-color: $primary;
+  background-color: var(--color-primary);
   color: #FFF;
   fill: #FFF;
   text-transform: uppercase;
-  border: 1px solid $primary;
+  border: 1px solid var(--color-primary);
   vertical-align: top;
-  &:hover, &:focus {
+  &:hover, &:focus, &:active {
+    background-color: var(--color-primary);
     text-decoration: none;
+    color: #FFFFFF;
+  }
+  &:hover {
+    box-shadow: $bs-light;
+    filter: brightness(110%);
+  }
+  &:focus {
+    outline: 1px dotted currentColor;
+    outline-offset: -$-xs;
+    box-shadow: none;
+    filter: brightness(90%);
   }
   &:active {
-    background-color: darken($primary, 8%);
+    outline: 0;
   }
 }
-.button.primary {
-  @include generate-button-colors(#FFFFFF, $primary);
-}
+
 .button.outline {
   background-color: transparent;
-  color: #888;
-  fill: #888;
-  border: 1px solid #DDD;
+  color: #666;
+  fill: currentColor;
+  border: 1px solid #CCC;
   &:hover, &:focus, &:active {
+    border: 1px solid #CCC;
     box-shadow: none;
-    background-color: #EEE;
+    background-color: #F2F2F2;
+    filter: none;
+  }
+  &:active {
+    border-color: #BBB;
+    background-color: #DDD;
+    color: #666;
+    box-shadow: inset 0 0 2px rgba(0, 0, 0, 0.1);
   }
 }
 
@@ -83,12 +83,18 @@ $button-border-radius: 2px;
   user-select: none;
   font-size: 0.75rem;
   line-height: 1.4em;
-  &:focus, &:active {
+  color: var(--color-primary);
+  fill: var(--color-primary);
+  &:active {
     outline: 0;
   }
   &:hover {
     text-decoration: none;
   }
+  &:hover, &:focus {
+    color: var(--color-primary);
+    fill: var(--color-primary);
+  }
 }
 
 .button.block {
@@ -118,6 +124,7 @@ $button-border-radius: 2px;
 .button[disabled] {
   background-color: #BBB;
   cursor: default;
+  border-color: #CCC;
   &:hover {
     background-color: #BBB;
     cursor: default;
diff --git a/resources/sass/_colors.scss b/resources/sass/_colors.scss
new file mode 100644 (file)
index 0000000..8623d37
--- /dev/null
@@ -0,0 +1,81 @@
+/**
+ * Background colors
+ */
+
+.primary-background {
+  background-color: var(--color-primary) !important;
+}
+.primary-background-light {
+  background-color: var(--color-primary-light);
+}
+
+/*
+ * Status text colors
+ */
+.text-pos, .text-pos:hover, .text-pos-hover:hover {
+  color: $positive !important;
+  fill: $positive !important;
+}
+
+.text-warn, .text-warn:hover, .text-warn-hover:hover {
+  color: $warning !important;
+  fill: $warning !important;
+}
+
+.text-neg, .text-neg:hover, .text-neg-hover:hover  {
+  color: $negative !important;
+  fill: $negative !important;
+}
+
+/*
+ * Style text colors
+ */
+.text-primary, .text-primary:hover, .text-primary-hover:hover  {
+  color: var(--color-primary) !important;
+  fill: var(--color-primary) !important;
+}
+
+.text-muted {
+  color: #575757 !important;
+  fill: #575757 !important;
+}
+
+/*
+ * Entity text colors
+ */
+.text-bookshelf, .text-bookshelf:hover {
+  color: var(--color-bookshelf);
+  fill: var(--color-bookshelf);
+}
+.text-book, .text-book:hover {
+  color: var(--color-book);
+  fill: var(--color-book);
+}
+.text-page, .text-page:hover {
+  color: var(--color-page);
+  fill: var(--color-page);
+}
+.text-page.draft, .text-page.draft:hover {
+  color: var(--color-page-draft);
+  fill: var(--color-page-draft);
+}
+.text-chapter, .text-chapter:hover {
+  color: var(--color-chapter);
+  fill: var(--color-chapter);
+}
+
+/*
+ * Standard & Entity background colors
+ */
+.bg-white {
+  background-color: #FFFFFF;
+}
+.bg-book {
+  background-color: var(--color-book);
+}
+.bg-chapter {
+  background-color: var(--color-chapter);
+}
+.bg-shelf {
+  background-color: var(--color-bookshelf);
+}
\ No newline at end of file
similarity index 93%
rename from resources/assets/sass/_components.scss
rename to resources/sass/_components.scss
index 039ac4dc8d8c3580c7f0374025ac6aa43a0692e3..0172956a76ee48c79ea0b69499e97180b6d16859 100644 (file)
   .popup-content {
     overflow-y: auto;
   }
+  &:focus {
+    outline: 0;
+  }
 }
 
 .popup-footer button, .popup-header-close {
     padding: 8px $-m;
   }
 }
-.popup-footer {
-  margin-top: 1px;
-}
 body.flexbox-support #entity-selector-wrap .popup-body .form-group {
   height: 444px;
   min-height: 444px;
@@ -579,6 +579,20 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
   }
 }
 
+.nav-tabs {
+  text-align: center;
+  a, .tab-item {
+    padding: $-m;
+    display: inline-block;
+    color: #666;
+    fill: #666;
+    cursor: pointer;
+    &.selected {
+      border-bottom: 2px solid var(--color-primary);
+    }
+  }
+}
+
 .image-picker .none {
   display: none;
 }
@@ -620,7 +634,7 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
     opacity: 0;
     transition: opacity ease-in-out 120ms;
   }
-  &:hover .actions {
+  &:hover .actions, &:focus-within .actions {
     opacity: 1;
   }
 }
@@ -637,7 +651,6 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
     }
     a { color: #666; }
     span {
-      color: #888;
       padding-left: $-xxs;
     }
   }
@@ -655,4 +668,32 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
 }
 .permissions-table tr:hover [permissions-table-toggle-all-in-row] {
   display: inline;
+}
+
+.template-item {
+  cursor: pointer;
+  position: relative;
+  &:hover, .template-item-actions button:hover {
+    background-color: #F2F2F2;
+  }
+  .template-item-actions {
+    position: absolute;
+    top: 0;
+    right: 0;
+    width: 50px;
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+    border-left: 1px solid #DDD;
+  }
+  .template-item-actions button {
+    cursor: pointer;
+    flex: 1;
+    background: #FFF;
+    border: 0;
+    border-top: 1px solid #DDD;
+  }
+  .template-item-actions button:first-child {
+    border-top: 0;
+  }
 }
\ No newline at end of file
similarity index 88%
rename from resources/assets/sass/_forms.scss
rename to resources/sass/_forms.scss
index a177129efec1a91544291674cb8c163a30f8a275..64308b29e725bbe0470189b0e259e31630ee0b73 100644 (file)
@@ -20,7 +20,8 @@
     background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAYAAADEUlfTAAAAMUlEQVQIW2NkwAGuXbv2nxGbHEhCS0uLEUMSJgHShCKJLIEiiS4Bl8QmAZbEJQGSBAC62BuJ+tt7zgAAAABJRU5ErkJggg==);
   }
   &:focus {
-    outline: 0;
+    border-color: var(--color-primary);
+    outline: 1px solid var(--color-primary);
   }
 }
 
 }
 
 .markdown-display {
-  padding: 0 $-m 0;
   margin-left: -1px;
-  overflow-y: scroll;
-  &.page-content {
-    margin: 0 auto;
-    width: 100%;
-    max-width: 100%;
+}
+
+.markdown-editor-display {
+  background-color: #FFFFFF;
+  body {
+    background-color: #FFFFFF;
+    padding-left: 16px;
+    padding-right: 16px;
   }
   [drawio-diagram]:hover {
-    outline: 2px solid $primary;
+    outline: 2px solid var(--color-primary);
   }
 }
 
@@ -158,7 +161,8 @@ table.form-table {
   }
 }
 
-input[type="text"], input[type="number"], input[type="email"], input[type="date"], input[type="search"], input[type="url"], input[type="password"], select, textarea {
+input[type="text"], input[type="number"], input[type="email"], input[type="date"], input[type="search"], input[type="url"],
+input[type="color"], input[type="password"], select, textarea {
   @extend .input-base;
 }
 
@@ -166,6 +170,10 @@ input[type=date] {
   width: 190px;
 }
 
+input[type=color] {
+  height: 60px;
+}
+
 .toggle-switch {
   user-select: none;
   display: inline-grid;
@@ -264,6 +272,9 @@ input[type=date] {
     margin-left: -$-m;
     margin-right: -$-m;
     padding: $-s $-m;
+    display: block;
+    width: calc(100% + 32px);
+    text-align: left;
   }
   .collapse-title, .collapse-title label {
     cursor: pointer;
@@ -337,16 +348,16 @@ div[editor-type="markdown"] .title-input.page-title input[type="text"] {
   button {
     background-color: transparent;
     border: none;
-    color: $primary;
+    fill: #666;
     padding: 0;
     cursor: pointer;
     position: absolute;
     left: 8px;
-    top: 9.5px;
+    top: 9px;
   }
   input {
     display: block;
-    padding-left: $-l;
+    padding-left: $-l + 4px;
     width: 300px;
     max-width: 100%;
   }
@@ -375,3 +386,18 @@ div[editor-type="markdown"] .title-input.page-title input[type="text"] {
   background-color: #BBB;
   max-width: 100%;
 }
+
+.custom-file-input {
+  overflow: hidden;
+  padding: 0;
+  position: absolute;
+  white-space: nowrap;
+  width: 1px;
+  height: 1px;
+  border: 0;
+  clip: rect(0, 0, 0, 0);
+}
+.custom-file-input:focus + label {
+  border-color: var(--color-primary);
+  outline: 1px solid var(--color-primary);
+}
\ No newline at end of file
similarity index 93%
rename from resources/assets/sass/_header.scss
rename to resources/sass/_header.scss
index c4ca4607adf76a641477d51f31952d1452527db1..687ddd8d2259c83dcf0cb94530e7b34b7d5e9b9d 100644 (file)
@@ -16,9 +16,8 @@ header .grid {
 header {
   position: relative;
   display: block;
-  z-index: 6;
+  z-index: 11;
   top: 0;
-  background-color: $primary-dark;
   color: #fff;
   fill: #fff;
   border-bottom: 1px solid #DDD;
@@ -47,9 +46,7 @@ header {
   }
   .user-name {
     vertical-align: top;
-    padding-top: $-m;
     position: relative;
-    top: -3px;
     display: inline-block;
     cursor: pointer;
     > * {
@@ -73,6 +70,9 @@ header {
   }
 }
 
+.header *, .primary-background * {
+  outline-color: #FFF;
+}
 
 
 .header-search {
@@ -88,6 +88,10 @@ header .search-box {
     color: #EEE;
     z-index: 2;
     padding-left: 40px;
+    &:focus {
+      outline: none;
+      border: 1px solid rgba(255, 255, 255, 0.6);
+    }
   }
   button {
     fill: #EEE;
@@ -103,12 +107,6 @@ header .search-box {
   ::-moz-placeholder { /* Firefox 19+ */
     color: #DDD;
   }
-  :-ms-input-placeholder { /* IE 10+ */
-    color: #DDD;
-  }
-  :-moz-placeholder { /* Firefox 18- */
-    color: #DDD;
-  }
   @include between($l, $xl) {
     max-width: 200px;
   }
@@ -243,7 +241,7 @@ header .search-box {
     line-height: 0.8;
     margin: -2px 0 0;
   }
-  &:hover {
+  &:hover, &:focus-within {
     opacity: 1;
   }
 }
@@ -372,18 +370,4 @@ header .search-box {
   .action-buttons .dropdown-container:last-child a {
     padding-left: $-xs;
   }
-}
-
-.nav-tabs {
-  text-align: center;
-  a, .tab-item {
-    padding: $-m;
-    display: inline-block;
-    color: #666;
-    fill: #666;
-    cursor: pointer;
-    &.selected {
-      border-bottom: 2px solid $primary;
-    }
-  }
 }
\ No newline at end of file
similarity index 81%
rename from resources/assets/sass/_html.scss
rename to resources/sass/_html.scss
index 7c3a3c49b41179aaf673b211ed834a399ca8a54e..de48c8ed1bbe8eac685067c399c583faafe0abdc 100644 (file)
@@ -1,5 +1,10 @@
 * {
   box-sizing: border-box;
+  outline-color: #444444;
+}
+
+*:focus {
+  outline-style: dotted;
 }
 
 html {
similarity index 92%
rename from resources/assets/sass/_layout.scss
rename to resources/sass/_layout.scss
index 4be6edb56f3632b2b2d76f9cc1627f4b8328c188..1a7ff2cab029e615c9f7ad7025b4b4fcc8bc2b9a 100644 (file)
@@ -116,6 +116,7 @@ body.flexbox {
   min-height: 0;
   max-width: 100%;
   position: relative;
+  overflow-y: hidden;
 }
 
 .flex {
@@ -219,12 +220,19 @@ body.flexbox {
 @include smaller-than($xxl) {
   .tri-layout-container {
     grid-template-areas:  "c b b"
-    "a b b";
+    "a b b"
+    ". b b";
     grid-template-columns: 1fr 3fr;
-    grid-template-rows: max-content min-content;
+    grid-template-rows: min-content min-content 1fr;
     padding-right: $-l;
   }
 }
+@include between($l, $xxl) {
+  .tri-layout-left {
+    position: sticky;
+    top: $-m;
+  }
+}
 @include larger-than($xxl) {
   .tri-layout-left-contents, .tri-layout-right-contents {
     padding: $-m;
@@ -257,10 +265,6 @@ body.flexbox {
       padding-left: $-m;
       padding-right: $-m;
     }
-    .tri-layout-right-contents > div, .tri-layout-left-contents > div {
-      opacity: 0.6;
-      z-index: 0;
-    }
     .tri-layout-left > *, .tri-layout-right > * {
       display: none;
       pointer-events: none;
@@ -298,6 +302,17 @@ body.flexbox {
   .tri-layout-mobile-tabs {
     display: none;
   }
+  .tri-layout-left-contents > *, .tri-layout-right-contents > * {
+    opacity: 0.6;
+    transition: opacity ease-in-out 120ms;
+    &:hover {
+      opacity: 1;
+    }
+    &:focus-within {
+      opacity: 1;
+    }
+  }
+
 }
 
 @include smaller-than($m) {
@@ -305,12 +320,4 @@ body.flexbox {
     margin-left: 0;
     margin-right: 0;
   }
-}
-
-.tri-layout-left-contents > div, .tri-layout-right-contents > div {
-  opacity: 0.6;
-  transition: opacity ease-in-out 120ms;
-  &:hover {
-    opacity: 1;
-  }
 }
\ No newline at end of file
similarity index 91%
rename from resources/assets/sass/_lists.scss
rename to resources/sass/_lists.scss
index 9c141232ea25fd9813d4333fcec3631747f73198..2e8fa257aebcf270e87bf438b00659ca85efcebd 100644 (file)
@@ -59,6 +59,8 @@
   .chapter-expansion-toggle {
     border-radius: 0 4px 4px 0;
     padding: $-xs $-m;
+    width: 100%;
+    text-align: left;
   }
   .chapter-expansion-toggle:hover {
     background-color: rgba(0, 0, 0, 0.06);
     padding-left: $nav-indent;
   }
   .h2 {
-    padding-left: $nav-indent;
+    padding-left: $nav-indent * 1.5;
   }
   .h3 {
     padding-left: $nav-indent * 2;
     padding-left: 1rem;
     padding-right: 0;
   }
+
   .entity-list-item {
     padding-top: $-xxs;
     padding-bottom: $-xxs;
+    background-clip: content-box;
+    border-radius: 0 3px 3px 0;
     .content {
       padding-top: $-xs;
       padding-bottom: $-xs;
       max-width: calc(100% - 20px);
     }
   }
+  .entity-list-item.selected {
+    background-color: rgba(0, 0, 0, 0.08);
+  }
   .entity-list-item.no-hover {
     margin-top: -$-xs;
     padding-right: 0;
 }
 .sort-box {
   margin-bottom: $-m;
-  border: 2px solid rgba($color-book, 0.6);
   padding: $-m $-xl;
-  border-radius: 4px;
+  position: relative;
+  &::before {
+    pointer-events: none;
+    content: '';
+    border-radius: 4px;
+    opacity: 0.5;
+    border: 2px solid var(--color-book);
+    display: block;
+    top: 0;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    position: absolute;
+  }
 }
 .sort-box-options {
   display: flex;
     border: 1px solid #DDD;
     margin-top: -1px;
     min-height: 38px;
-    &.text-chapter {
-      border-left: 2px solid $color-chapter;
-    }
-    &.text-page {
-      border-left: 2px solid $color-page;
-    }
+  }
+  li.text-page, li.text-chapter {
+    border-left: 2px solid currentColor;
   }
   li:first-child {
     margin-top: $-xs;
   display: grid;
   grid-template-columns: min-content 1fr;
   grid-column-gap: $-m;
-  color: #888;
-  fill: #888;
   font-size: 0.9em;
 }
 .card .activity-list-item {
@@ -355,8 +370,8 @@ ul.pagination {
     margin-top: 0;
   }
   .page.draft .text-page {
-    color: $color-page-draft;
-    fill: $color-page-draft;
+    color: var(--color-page-draft);
+    fill: var(--color-page-draft);
   }
   > .dropdown-container {
     display: block;
@@ -410,6 +425,11 @@ ul.pagination {
     background-color: transparent;
     border-color: rgba(0, 0, 0, 0.1);
   }
+  &:focus {
+    background-color: #eee;
+    outline: 1px dotted #666;
+    outline-offset: -2px;
+  }
 }
 
 .entity-list-item-path-sep {
@@ -545,11 +565,16 @@ ul.pagination {
     display: block;
     padding: $-xs $-m;
     color: #555;
-    fill: #555;
+    fill: currentColor;
     white-space: nowrap;
-    &:hover {
+    &:hover, &:focus {
       text-decoration: none;
-      background-color: #EEE;
+      background-color: var(--color-primary-light);
+      color: var(--color-primary);
+    }
+    &:focus {
+      outline: 1px solid var(--color-primary);
+      outline-offset: -2px;
     }
     svg {
       margin-right: $-s;
similarity index 95%
rename from resources/assets/sass/_pages.scss
rename to resources/sass/_pages.scss
index c58f6ef476e55cac1498e5dc41ba09b4ac21ecc7..709b1a7efe845b85dd6c55027d6d7be2fd5d3f37 100755 (executable)
   }
 }
 
+body.mce-fullscreen .page-editor .edit-area {
+  z-index: 12;
+}
+
 @include smaller-than($s) {
   .page-edit-toolbar {
     overflow-x: scroll;
 }
 .pointer {
   border: 1px solid #CCC;
-  display: inline-block;
+  display: flex;
+  align-items: center;
+  justify-items: center;
   padding: $-s $-s;
   border-radius: 4px;
-  box-shadow: 0 0 8px 1px rgba(212, 209, 209, 0.35);
+  box-shadow: 0 0 12px 1px rgba(212, 209, 209, 0.3);
   position: absolute;
   top: -60px;
   background-color:#FFF;
     border-right: 1px solid #CCC;
     z-index: 56;
   }
-  input {
-    background-color: #FFF;
-    border: 1px solid #DDD;
-    color: #666;
-    width: 172px;
-    z-index: 40;
-  }
   input, button, a {
     position: relative;
     border-radius: 0;
     vertical-align: top;
     padding: 5px 16px;
   }
-  > i {
-    color: #888;
-    font-size: 18px;
-    padding-top: 4px;
+  input {
+    background-color: #FFF;
+    border: 1px solid #DDD;
+    color: #666;
+    width: 172px;
+    z-index: 40;
+    padding: 5px 10px;
   }
   span.icon {
+    fill: #444;
     cursor: pointer;
     user-select: none;
+    display: inline-block;
+    line-height: 1;
   }
   .input-group .button {
     line-height: 1;
     box-shadow: none;
   }
   a.button {
-    margin: 0 0 0 0;
-
-    &:hover {
-      fill: #fff;
-    }
+    margin: 0;
   }
   .svg-icon {
     width: 1.2em;
     padding: 0;
     margin: 0;
   }
-  .tabs > span {
+  .tabs > button {
     display: block;
     cursor: pointer;
     padding: $-s $-m;
-    font-size: 13.5px;
+    font-size: 16px;
     line-height: 1.6;
     border-bottom: 1px solid rgba(255, 255, 255, 0.3);
   }
-  &.open .tabs > span.active {
+  &.open .tabs > button.active {
     fill: #444;
     background-color: rgba(0, 0, 0, 0.1);
   }
similarity index 88%
rename from resources/assets/sass/_text.scss
rename to resources/sass/_text.scss
index 41c99bbe594eabde42ca80f97db763a201817cc8..cf78c162b95c72fc66e9643982bbc4c5aec90ec3 100644 (file)
@@ -90,14 +90,14 @@ h2.list-heading {
  * Link styling
  */
 a {
-  color: $primary;
+  color: var(--color-primary);
+  fill: var(--color-primary);
   cursor: pointer;
   text-decoration: none;
-  transition: color ease-in-out 80ms;
+  transition: filter ease-in-out 80ms;
   line-height: 1.6;
   &:hover {
     text-decoration: underline;
-    color: darken($primary, 20%);
   }
   &.icon {
     display: inline-block;
@@ -106,6 +106,10 @@ a {
     position: relative;
     display: inline-block;
   }
+  &:focus img:only-child {
+    outline: 2px dashed var(--color-primary);
+    outline-offset: 2px;
+  }
 }
 
 .blended-links a {
@@ -195,7 +199,7 @@ pre {
 blockquote {
   display: block;
   position: relative;
-  border-left: 4px solid $primary;
+  border-left: 4px solid var(--color-primary);
   background-color: #F8F8F8;
   padding: $-s $-m $-s $-xl;
   &:before {
@@ -220,7 +224,7 @@ code {
   @extend .code-base;
   display: inline;
   padding: 1px 3px;
-  white-space:pre;
+  white-space:pre-wrap;
   line-height: 1.2em;
   margin-bottom: 1.2em;
 }
@@ -239,7 +243,6 @@ pre code {
 }
 
 span.highlight {
-  //background-color: rgba($primary, 0.2);
   font-weight: bold;
   padding: 2px 4px;
 }
@@ -291,15 +294,27 @@ li.checkbox-item, li.task-list-item {
 .text-center {
   text-align: center;
 }
-
 .text-left {
   text-align: left;
 }
-
 .text-right {
   text-align: right;
 }
 
+@each $sizeLetter, $size in $screen-sizes {
+  @include larger-than($size) {
+    .text-#{$sizeLetter}-center {
+      text-align: center;
+    }
+    .text-#{$sizeLetter}-left {
+      text-align: left;
+    }
+    .text-#{$sizeLetter}-right {
+      text-align: right;
+    }
+  }
+}
+
 .text-bigger {
   font-size: 1.1em;
 }
@@ -317,6 +332,12 @@ li.checkbox-item, li.task-list-item {
   overflow-wrap: break-word;
 }
 
+.limit-text {
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
 /**
  * Grouping
  */
@@ -348,4 +369,3 @@ span.sep {
   margin-right: $-xs;
   pointer-events: none;
 }
-
similarity index 94%
rename from resources/assets/sass/_tinymce.scss
rename to resources/sass/_tinymce.scss
index 1596feb76d91a014a7683221359142cffef639f1..27c3b28d01b50874e035ce055362e3784f64967b 100644 (file)
@@ -47,6 +47,8 @@
       display: flex !important;
       flex-direction: column;
       align-items: stretch;
+      -webkit-overflow-scrolling:touch;
+      overflow:auto;
       iframe {
         flex: 1;
       }
@@ -59,6 +61,7 @@
 
 .page-content.mce-content-body {
   padding-top: 16px;
+  outline: none;
 }
 
 // Fix to prevent 'No color' option from not being clickable.
similarity index 79%
rename from resources/assets/sass/_variables.scss
rename to resources/sass/_variables.scss
index 041b70edfdb4c6a9946dc6aa0705fac46e0b30b1..2d4d3970af2d028b98f825cf2b5833395db81fb1 100644 (file)
@@ -41,21 +41,21 @@ $fs-m: 14px;
 $fs-s: 12px;
 
 // Colours
-$primary: #0288D1;
-$primary-dark: #0288D1;
-$secondary: #cf4d03;
+:root {
+  --color-primary: #206ea7;
+  --color-primary-light: rgba(32,110,167,0.15);
+
+  --color-page: #206ea7;
+  --color-page-draft: #7e50b1;
+  --color-chapter: #af4d0d;
+  --color-book: #077b70;
+  --color-bookshelf: #a94747;
+}
+
 $positive: #0f7d15;
 $negative: #ab0f0e;
-$info: $primary;
-$warning: $secondary;
-$primary-faded: rgba(21, 101, 192, 0.15);
-
-// Item Colors
-$color-bookshelf: #af5a5a;
-$color-book: #009688;
-$color-chapter: #d7804a;
-$color-page: $primary;
-$color-page-draft: #9A60DA;
+$info: #0288D1;
+$warning: #cf4d03;
 
 // Text colours
 $text-dark: #444;
similarity index 97%
rename from resources/assets/sass/export-styles.scss
rename to resources/sass/export-styles.scss
index 4cc782dc0e9d4881d533c55ebc518bef42dd4a6e..958b788075cb50b788685345bfbe9d9b40b111ff 100644 (file)
 @import "lists";
 @import "pages";
 
+
+html, body {
+  background-color: #FFF;
+}
+
 body {
   font-family: 'DejaVu Sans', -apple-system, BlinkMacSystemFont, "Segoe UI", "Oxygen", "Ubuntu", "Roboto", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
-  background-color: #FFF;
   margin: 0;
   padding: 0;
 }
diff --git a/resources/sass/print-styles.scss b/resources/sass/print-styles.scss
new file mode 100644 (file)
index 0000000..296afbe
--- /dev/null
@@ -0,0 +1,35 @@
+@import "variables";
+
+header {
+  display: none;
+}
+
+html, body {
+  font-size: 12px;
+  background-color: #FFF;
+}
+
+.page-content {
+  margin: 0 auto;
+}
+
+.print-hidden {
+  display: none !important;
+}
+
+.tri-layout-container {
+  grid-template-columns: 1fr;
+  grid-template-areas: "b";
+  margin-left: 0;
+  margin-right: 0;
+  display: block;
+}
+
+.card {
+  box-shadow: none;
+}
+
+.content-wrap.card {
+  padding-left: 0;
+  padding-right: 0;
+}
\ No newline at end of file
similarity index 88%
rename from resources/assets/sass/styles.scss
rename to resources/sass/styles.scss
index 3fbbcf1da949f7ba54e21914b3f07ac768a01765..1f4d00f6b9606b1ebe15d94b0c5b8788dd747b6e 100644 (file)
@@ -75,17 +75,17 @@ $loadingSize: 10px;
     animation-iteration-count: infinite;
     animation-timing-function: cubic-bezier(.62, .28, .23, .99);
     margin-right: 4px;
-    background-color: $color-page;
+    background-color: var(--color-page);
     animation-delay: 0.3s;
   }
   > div:first-child {
       left: -($loadingSize+$-xs);
-      background-color: $color-book;
+      background-color: var(--color-book);
       animation-delay: 0s;
   }
   > div:last-of-type {
     left: $loadingSize+$-xs;
-    background-color: $color-chapter;
+    background-color: var(--color-chapter);
     animation-delay: 0.6s;
   }
   > span {
@@ -99,7 +99,7 @@ $loadingSize: 10px;
 // Back to top link
 $btt-size: 40px;
 [back-to-top] {
-  background-color: $primary;
+  background-color: var(--color-primary);
   position: fixed;
   bottom: $-m;
   right: $-l;
@@ -187,7 +187,7 @@ $btt-size: 40px;
     margin-bottom: 0;
   }
   .entity-list-item.selected {
-    background-color: rgba(0, 0, 0, 0.15) !important;
+    background-color: rgba(0, 0, 0, 0.05) !important;
   }
   .loading {
     height: 400px;
@@ -216,12 +216,23 @@ $btt-size: 40px;
   .scroll-box-item {
     padding: $-xs $-m;
     border-bottom: 1px solid #DDD;
+    border-top: 1px solid #DDD;
+    margin-top: -1px;
     &:last-child {
       border-bottom: 0;
     }
   }
 }
 
+.scroll-box[data-instruction]:before {
+  content: attr(data-instruction);
+  padding: $-xs $-m;
+  border-bottom: 1px solid #DDD;
+  display: block;
+  font-size: 0.75rem;
+  color: #666;
+}
+
 .fullscreen {
   border:0;
   position:fixed;
@@ -242,14 +253,15 @@ $btt-size: 40px;
   .list-sort {
     display: inline-grid;
     margin-left: $-s;
-    grid-template-columns: 120px 40px;
+    grid-template-columns: minmax(120px, max-content) 40px;
+    font-size: 0.9rem;
     border: 2px solid #DDD;
     border-radius: 4px;
   }
   .list-sort-label {
     font-weight: bold;
     display: inline-block;
-    color: #888;
+    color: #555;
   }
   .list-sort-type {
     text-align: left;
index f7903e18c72de72fd979356726142b91afff1a92..2699fda00ac59801e454f748dd339eb8b6c7fac8 100644 (file)
@@ -1,12 +1,12 @@
 <div class="form-group">
     <label for="username">{{ trans('auth.username') }}</label>
-    @include('form.text', ['name' => 'username', 'tabindex' => 1])
+    @include('form.text', ['name' => 'username', 'autofocus' => true])
 </div>
 
 @if(session('request-email', false) === true)
     <div class="form-group">
         <label for="email">{{ trans('auth.email') }}</label>
-        @include('form.text', ['name' => 'email', 'tabindex' => 1])
+        @include('form.text', ['name' => 'email'])
         <span class="text-neg">
             {{ trans('auth.ldap_email_hint') }}
         </span>
@@ -15,5 +15,5 @@
 
 <div class="form-group">
     <label for="password">{{ trans('auth.password') }}</label>
-    @include('form.password', ['name' => 'password', 'tabindex' => 1])
+    @include('form.password', ['name' => 'password'])
 </div>
\ No newline at end of file
index ea63cf7ac1ba764725e7442e5ccd5a5de5534448..52fae3750ad38dbd9cccacedeea82328d63fd0b1 100644 (file)
@@ -1,12 +1,12 @@
 <div class="form-group">
     <label for="email">{{ trans('auth.email') }}</label>
-    @include('form.text', ['name' => 'email', 'tabindex' => 1])
+    @include('form.text', ['name' => 'email', 'autofocus' => true])
 </div>
 
 <div class="form-group">
     <label for="password">{{ trans('auth.password') }}</label>
-    @include('form.password', ['name' => 'password', 'tabindex' => 1])
+    @include('form.password', ['name' => 'password'])
     <span class="block small mt-s">
-        <a href="{{ baseUrl('/password/email') }}">{{ trans('auth.forgot_password') }}</a>
+        <a href="{{ url('/password/email') }}">{{ trans('auth.forgot_password') }}</a>
     </span>
 </div>
diff --git a/resources/views/auth/invite-set-password.blade.php b/resources/views/auth/invite-set-password.blade.php
new file mode 100644 (file)
index 0000000..fbe62f2
--- /dev/null
@@ -0,0 +1,27 @@
+@extends('simple-layout')
+
+@section('content')
+
+    <div class="container very-small mt-xl">
+        <div class="card content-wrap auto-height">
+            <h1 class="list-heading">{{ trans('auth.user_invite_page_welcome', ['appName' => setting('app-name')]) }}</h1>
+            <p>{{ trans('auth.user_invite_page_text', ['appName' => setting('app-name')]) }}</p>
+
+            <form action="{{ url('/register/invite/' . $token) }}" method="POST" class="stretch-inputs">
+                {!! csrf_field() !!}
+
+                <div class="form-group">
+                    <label for="password">{{ trans('auth.password') }}</label>
+                    @include('form.password', ['name' => 'password', 'placeholder' => trans('auth.password_hint')])
+                </div>
+
+                <div class="text-right">
+                    <button class="button">{{ trans('auth.user_invite_page_confirm_button') }}</button>
+                </div>
+
+            </form>
+
+        </div>
+    </div>
+
+@stop
index 51b47f5c7b7aa44c45d54c6d9e7ea3c3a4346022..fbf540d7105fd6f0969956d7291413471d820b53 100644 (file)
@@ -7,9 +7,9 @@
         <div class="my-l">&nbsp;</div>
 
         <div class="card content-wrap auto-height">
-            <h1 class="list-heading">{{ title_case(trans('auth.log_in')) }}</h1>
+            <h1 class="list-heading">{{ Str::title(trans('auth.log_in')) }}</h1>
 
-            <form action="{{ baseUrl('/login') }}" method="POST" id="login-form" class="mt-l">
+            <form action="{{ url('/login') }}" method="POST" id="login-form" class="mt-l">
                 {!! csrf_field() !!}
 
                 <div class="stretch-inputs">
@@ -27,7 +27,7 @@
                     </div>
 
                     <div class="text-right">
-                        <button class="button primary" tabindex="1">{{ title_case(trans('auth.log_in')) }}</button>
+                        <button class="button">{{ Str::title(trans('auth.log_in')) }}</button>
                     </div>
                 </div>
 
@@ -37,7 +37,7 @@
                 <hr class="my-l">
                 @foreach($socialDrivers as $driver => $name)
                     <div>
-                        <a id="social-login-{{$driver}}" class="button outline block svg" href="{{ baseUrl("/login/service/" . $driver) }}">
+                        <a id="social-login-{{$driver}}" class="button outline block svg" href="{{ url("/login/service/" . $driver) }}">
                             @icon('auth/' . $driver)
                             {{ trans('auth.log_in_with', ['socialDriver' => $name]) }}
                         </a>
@@ -48,7 +48,7 @@
             @if(setting('registration-enabled', false))
                 <div class="text-center pb-s">
                     <hr class="my-l">
-                    <a href="{{ baseUrl('/register') }}">{{ trans('auth.dont_have_account') }}</a>
+                    <a href="{{ url('/register') }}">{{ trans('auth.dont_have_account') }}</a>
                 </div>
             @endif
         </div>
index de4edff0a8e479da8999a802727d7d76c6f1cb72..8273ed2356bf931f7fbe142fd6e4ad1e4841baec 100644 (file)
@@ -7,7 +7,7 @@
 
             <p class="text-muted small">{{ trans('auth.reset_password_send_instructions') }}</p>
 
-            <form action="{{ baseUrl("/password/email") }}" method="POST" class="stretch-inputs">
+            <form action="{{ url("/password/email") }}" method="POST" class="stretch-inputs">
                 {!! csrf_field() !!}
 
                 <div class="form-group">
@@ -16,7 +16,7 @@
                 </div>
 
                 <div class="from-group text-right mt-m">
-                    <button class="button primary">{{ trans('auth.reset_password_send_button') }}</button>
+                    <button class="button">{{ trans('auth.reset_password_send_button') }}</button>
                 </div>
             </form>
 
index fa6ad5b9a327b6cd0945709f27c31616a21913f8..930544cde40e870c788c1793afeb70661bc010a2 100644 (file)
@@ -6,7 +6,7 @@
         <div class="card content-wrap auto-height">
             <h1 class="list-heading">{{ trans('auth.reset_password') }}</h1>
 
-            <form action="{{ baseUrl("/password/reset") }}" method="POST" class="stretch-inputs">
+            <form action="{{ url("/password/reset") }}" method="POST" class="stretch-inputs">
                 {!! csrf_field() !!}
                 <input type="hidden" name="token" value="{{ $token }}">
 
@@ -26,7 +26,7 @@
                 </div>
 
                 <div class="from-group text-right mt-m">
-                    <button class="button primary">{{ trans('auth.reset_password') }}</button>
+                    <button class="button">{{ trans('auth.reset_password') }}</button>
                 </div>
             </form>
 
index 38904f63bb7ce880d3b5a80ae0731227f63bc83d..0e996a00d2300e1d27ecba57976260af441e6ee0 100644 (file)
@@ -6,9 +6,9 @@
         <div class="my-l">&nbsp;</div>
 
         <div class="card content-wrap auto-height">
-            <h1 class="list-heading">{{ title_case(trans('auth.sign_up')) }}</h1>
+            <h1 class="list-heading">{{ Str::title(trans('auth.sign_up')) }}</h1>
 
-            <form action="{{ baseUrl("/register") }}" method="POST" class="mt-l stretch-inputs">
+            <form action="{{ url("/register") }}" method="POST" class="mt-l stretch-inputs">
                 {!! csrf_field() !!}
 
                 <div class="form-group">
 
                 <div class="grid half collapse-xs gap-xl v-center mt-m">
                     <div class="text-small">
-                        <a href="{{ baseUrl('/login') }}">{{ trans('auth.already_have_account') }}</a>
+                        <a href="{{ url('/login') }}">{{ trans('auth.already_have_account') }}</a>
                     </div>
                     <div class="from-group text-right">
-                        <button class="button primary">{{ trans('auth.create_account') }}</button>
+                        <button class="button">{{ trans('auth.create_account') }}</button>
                     </div>
                 </div>
 
@@ -42,7 +42,7 @@
                 <hr class="my-l">
                 @foreach($socialDrivers as $driver => $name)
                     <div>
-                        <a id="social-register-{{$driver}}" class="button block outline svg" href="{{ baseUrl("/register/service/" . $driver) }}">
+                        <a id="social-register-{{$driver}}" class="button block outline svg" href="{{ url("/register/service/" . $driver) }}">
                             @icon('auth/' . $driver)
                             {{ trans('auth.sign_up_with', ['socialDriver' => $name]) }}
                         </a>
index 54bf6eda3314ac73d4b07b3de18d645e1f40dfe1..85473685b96207e108d07d3093a2c23fa4f43bcd 100644 (file)
@@ -13,7 +13,7 @@
                 {{ trans('auth.email_not_confirmed_resend') }}
             </p>
 
-            <form action="{{ baseUrl("/register/confirm/resend") }}" method="POST" class="stretch-inputs">
+            <form action="{{ url("/register/confirm/resend") }}" method="POST" class="stretch-inputs">
                 {!! csrf_field() !!}
                 <div class="form-group">
                     <label for="email">{{ trans('auth.email') }}</label>
@@ -24,7 +24,7 @@
                     @endif
                 </div>
                 <div class="form-group text-right mt-m">
-                    <button type="submit" class="button primary">{{ trans('auth.email_not_confirmed_resend_button') }}</button>
+                    <button type="submit" class="button">{{ trans('auth.email_not_confirmed_resend_button') }}</button>
                 </div>
             </form>
 
index 367a2cd8b9ba69536d985dc85cbf1a14681c7395..07548162067404fa82061695c6753fe5bd15e27c 100644 (file)
@@ -1,37 +1,40 @@
 <!DOCTYPE html>
-<html class="@yield('body-class')">
+<html lang="{{ config('app.lang') }}" class="@yield('body-class')">
 <head>
     <title>{{ isset($pageTitle) ? $pageTitle . ' | ' : '' }}{{ setting('app-name') }}</title>
 
     <!-- Meta -->
     <meta name="viewport" content="width=device-width">
     <meta name="token" content="{{ csrf_token() }}">
-    <meta name="base-url" content="{{ baseUrl('/') }}">
+    <meta name="base-url" content="{{ url('/') }}">
     <meta charset="utf-8">
 
     <!-- Styles and Fonts -->
     <link rel="stylesheet" href="{{ versioned_asset('dist/styles.css') }}">
     <link rel="stylesheet" media="print" href="{{ versioned_asset('dist/print-styles.css') }}">
 
-    <!-- Scripts -->
-    <script src="{{ baseUrl('/translations') }}"></script>
-
     @yield('head')
+
+    <!-- Custom Styles & Head Content -->
     @include('partials.custom-styles')
     @include('partials.custom-head')
 
     @stack('head')
+
+    <!-- Translations for JS -->
+    @stack('translations')
+
 </head>
 <body class="@yield('body-class')">
 
     @include('partials.notifications')
     @include('common.header')
 
-    <section id="content" class="block">
+    <div id="content" class="block">
         @yield('content')
-    </section>
+    </div>
 
-    <div back-to-top class="primary-background">
+    <div back-to-top class="primary-background print-hidden">
         <div class="inner">
             @icon('chevron-up') <span>{{ trans('common.back_to_top') }}</span>
         </div>
index 40b781441ddda044dc8f59c6f6a4079fe3321277..6de81cd462db648bcf11e294eff640c1c67c6f91 100644 (file)
             @endif
         </div>
 
-        <div class="content-wrap card">
+        <main class="content-wrap card">
             <h1 class="list-heading">{{ trans('entities.books_create') }}</h1>
-            <form action="{{ isset($bookshelf) ? $bookshelf->getUrl('/create-book') : baseUrl('/books') }}" method="POST" enctype="multipart/form-data">
+            <form action="{{ isset($bookshelf) ? $bookshelf->getUrl('/create-book') : url('/books') }}" method="POST" enctype="multipart/form-data">
                 @include('books.form')
             </form>
-        </div>
+        </main>
     </div>
 
 @stop
\ No newline at end of file
index 2860e8bcdf15a63dbb2dec9cee399c4f39020ea0..be3f742cba5d13f5bc45c01864d3122ba4043283 100644 (file)
@@ -23,7 +23,7 @@
                 {!! csrf_field() !!}
                 <input type="hidden" name="_method" value="DELETE">
                 <a href="{{$book->getUrl()}}" class="button outline">{{ trans('common.cancel') }}</a>
-                <button type="submit" class="button primary">{{ trans('common.confirm') }}</button>
+                <button type="submit" class="button">{{ trans('common.confirm') }}</button>
             </form>
         </div>
 
index 2e51ed6e956fb947b905b97282d895ef38f1137c..400fd6e817a0a3dc6bdd5d7289abf59c0386f0c8 100644 (file)
             ]])
         </div>
 
-        <div class="content-wrap card">
+        <main class="content-wrap card">
             <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])
             </form>
-        </div>
+        </main>
     </div>
 @stop
\ No newline at end of file
index e1fabd800bd1c42c6cd42257bb6e81e2a28fc23c..1cf91046df1b5ab2043c5e7072ed265c9a9c5a89 100644 (file)
@@ -1,5 +1,5 @@
 <!doctype html>
-<html lang="en">
+<html lang="{{ config('app.lang') }}">
 <head>
     <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
     <title>{{ $book->name }}</title>
index 4edec240a03e9a961bca643f45c5ba5a518058c0..8960b41359584ceb2cdec3b41039a90f0b1bfdde 100644 (file)
 </div>
 
 <div class="form-group" collapsible id="logo-control">
-    <div class="collapse-title text-primary" collapsible-trigger>
-        <label for="user-avatar">{{ trans('common.cover_image') }}</label>
-    </div>
+    <button type="button" class="collapse-title text-primary" collapsible-trigger aria-expanded="false">
+        <label>{{ trans('common.cover_image') }}</label>
+    </button>
     <div class="collapse-content" collapsible-content>
         <p class="small">{{ trans('common.cover_image_description') }}</p>
 
         @include('components.image-picker', [
-            'defaultImage' => baseUrl('/book_default_cover.png'),
-            'currentImage' => (isset($model) && $model->cover) ? $model->getBookCover() : baseUrl('/book_default_cover.png') ,
+            'defaultImage' => url('/book_default_cover.png'),
+            'currentImage' => (isset($model) && $model->cover) ? $model->getBookCover() : url('/book_default_cover.png') ,
             'name' => 'image',
             'imageClass' => 'cover'
         ])
 </div>
 
 <div class="form-group" collapsible id="tags-control">
-    <div class="collapse-title text-primary" collapsible-trigger>
+    <button type="button" class="collapse-title text-primary" collapsible-trigger aria-expanded="false">
         <label for="tag-manager">{{ trans('entities.book_tags') }}</label>
-    </div>
+    </button>
     <div class="collapse-content" collapsible-content>
         @include('components.tag-manager', ['entity' => isset($book)?$book:null, 'entityType' => 'chapter'])
     </div>
 </div>
 
 <div class="form-group text-right">
-    <a href="{{ isset($book) ? $book->getUrl() : baseUrl('/books') }}" class="button outline">{{ trans('common.cancel') }}</a>
-    <button type="submit" class="button primary">{{ trans('entities.books_save') }}</button>
+    <a href="{{ isset($book) ? $book->getUrl() : url('/books') }}" class="button outline">{{ trans('common.cancel') }}</a>
+    <button type="submit" class="button">{{ trans('entities.books_save') }}</button>
 </div>
\ No newline at end of file
index 38286c2016983e8bd5033cf1c1b62af42762414e..b9bd987a9c723224eddd35272a5f2ea2a61aa900 100644 (file)
@@ -1,7 +1,5 @@
 @extends('tri-layout')
 
-@section('container-classes', 'mt-xl')
-
 @section('body')
     @include('books.list', ['books' => $books, 'view' => $view])
 @stop
@@ -39,7 +37,7 @@
         <h5>{{ trans('common.actions') }}</h5>
         <div class="icon-list text-primary">
             @if($currentUser->can('book-create-all'))
-                <a href="{{ baseUrl("/create-book") }}" class="icon-list-item">
+                <a href="{{ url("/create-book") }}" class="icon-list-item">
                     <span>@icon('add')</span>
                     <span>{{ trans('entities.books_create') }}</span>
                 </a>
index 91a2c716e275bd1cefe9dd1bc6fd7a6b3c86191d..42a2757f94e0e949d536791f6dd79f4daa5ea38a 100644 (file)
@@ -1,10 +1,14 @@
 
-<div class="content-wrap mt-m card">
-    <div class="grid half v-center">
+<main class="content-wrap mt-m card">
+    <div class="grid half v-center no-row-gap">
         <h1 class="list-heading">{{ trans('entities.books') }}</h1>
-        <div class="text-right">
+        <div class="text-m-right my-m">
 
-            @include('partials.sort', ['options' => $sortOptions, 'order' => $order, 'sort' => $sort, 'type' => 'books'])
+            @include('partials.sort', ['options' => [
+                'name' => trans('common.sort_name'),
+                'created_at' => trans('common.sort_created_at'),
+                'updated_at' => trans('common.sort_updated_at'),
+            ], 'order' => $order, 'sort' => $sort, 'type' => 'books'])
 
         </div>
     </div>
@@ -28,7 +32,7 @@
     @else
         <p class="text-muted">{{ trans('entities.books_empty') }}</p>
         @if(userCan('books-create-all'))
-            <a href="{{ baseUrl("/create-book") }}" class="text-pos">@icon('edit'){{ trans('entities.create_now') }}</a>
+            <a href="{{ url("/create-book") }}" class="text-pos">@icon('edit'){{ trans('entities.create_now') }}</a>
         @endif
     @endif
-</div>
\ No newline at end of file
+</main>
\ No newline at end of file
index 64322cf859696af63971a57b76abad784ac14be9..b387ed6c7c94b082800b1a72979c61668308a2f7 100644 (file)
             ]])
         </div>
 
-        <div class="card content-wrap">
+        <main class="card content-wrap">
             <h1 class="list-heading">{{ trans('entities.books_permissions') }}</h1>
             @include('form.entity-permissions', ['model' => $book])
-        </div>
+        </main>
     </div>
 
 @stop
index b709b29dcd7ac6ac2d1e0512ec62f0d69845c745..cbafdb4364b0d18e350369d83e6457775edba5e5 100644 (file)
@@ -14,7 +14,7 @@
         ]])
     </div>
 
-    <div class="content-wrap card">
+    <main class="content-wrap card">
         <h1 class="break-text" v-pre>{{$book->name}}</h1>
         <div class="book-content" v-show="!searching">
             <p class="text-muted" v-pre>{!! nl2br(e($book->description)) !!}</p>
@@ -53,7 +53,7 @@
         </div>
 
         @include('partials.entity-dashboard-search-results')
-    </div>
+    </main>
 
 @stop
 
 
             <hr class="primary-background">
 
-            <div dropdown class="dropdown-container">
-                <div dropdown-toggle class="icon-list-item">
-                    <span>@icon('export')</span>
-                    <span>{{ trans('entities.export') }}</span>
-                </div>
-                <ul class="wide dropdown-menu">
-                    <li><a href="{{ $book->getUrl('/export/html') }}" target="_blank">{{ trans('entities.export_html') }} <span class="text-muted float right">.html</span></a></li>
-                    <li><a href="{{ $book->getUrl('/export/pdf') }}" target="_blank">{{ trans('entities.export_pdf') }} <span class="text-muted float right">.pdf</span></a></li>
-                    <li><a href="{{ $book->getUrl('/export/plaintext') }}" target="_blank">{{ trans('entities.export_text') }} <span class="text-muted float right">.txt</span></a></li>
-                </ul>
-            </div>
+            @include('partials.entity-export-menu', ['entity' => $book])
         </div>
     </div>
 
index 23259a593a57bcd9b4e05e0996c9f56ab1cfb500..642b88c873a1c442451494903a5db83db76c5c33 100644 (file)
 
         <div class="grid left-focus gap-xl">
             <div>
-                <div class="card content-wrap">
+                <div book-sort class="card content-wrap">
                     <h1 class="list-heading mb-l">{{ trans('entities.books_sort') }}</h1>
-                    <div id="sort-boxes">
+                    <div book-sort-boxes>
                         @include('books.sort-box', ['book' => $book, 'bookChildren' => $bookChildren])
                     </div>
 
                     <form action="{{ $book->getUrl('/sort') }}" method="POST">
                         {!! csrf_field() !!}
                         <input type="hidden" name="_method" value="PUT">
-                        <input type="hidden" id="sort-tree-input" name="sort-tree">
+                        <input book-sort-input type="hidden" name="sort-tree">
                         <div class="list text-right">
                             <a href="{{ $book->getUrl() }}" class="button outline">{{ trans('common.cancel') }}</a>
-                            <button class="button primary" type="submit">{{ trans('entities.books_sort_save') }}</button>
+                            <button class="button" type="submit">{{ trans('entities.books_sort_save') }}</button>
                         </div>
                     </form>
                 </div>
             </div>
 
             <div>
-                <div class="card content-wrap">
+                <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])
 
-                </div>
+                </main>
             </div>
         </div>
 
     </div>
 
 @stop
-
-@section('scripts')
-    <script src="{{ baseUrl("/libs/jquery-sortable/jquery-sortable.min.js") }}"></script>
-    <script>
-        $(document).ready(function() {
-
-            const $container = $('#sort-boxes');
-
-            // Sortable options
-            const sortableOptions = {
-                group: 'serialization',
-                containerSelector: 'ul',
-                itemPath: '',
-                itemSelector: 'li',
-                onDrop: function ($item, container, _super) {
-                    updateMapInput();
-                    _super($item, container);
-                },
-                isValidTarget: function ($item, container) {
-                    // Prevent nested chapters
-                    return !($item.is('[data-type="chapter"]') && container.target.closest('li').attr('data-type') === 'chapter');
-                }
-            };
-
-            // Create our sortable group
-            let group = $('.sort-list').sortable(sortableOptions);
-
-            // Add book on selection confirm
-            window.$events.listen('entity-select-confirm', function(entityInfo) {
-                const alreadyAdded = $container.find(`[data-type="book"][data-id="${entityInfo.id}"]`).length > 0;
-                if (alreadyAdded) return;
-
-                const entitySortItemUrl = entityInfo.link + '/sort-item';
-                window.$http.get(entitySortItemUrl).then(resp => {
-                    $container.append(resp.data);
-                    group.sortable("destroy");
-                    group = $('.sort-list').sortable(sortableOptions);
-                });
-            });
-
-            /**
-             * Update the input with our sort data.
-             */
-            function updateMapInput() {
-                const pageMap = buildEntityMap();
-                $('#sort-tree-input').val(JSON.stringify(pageMap));
-            }
-
-            /**
-             * Build up a mapping of entities with their ordering and nesting.
-             * @returns {Array}
-             */
-            function buildEntityMap() {
-                const entityMap = [];
-                const $lists = $('.sort-list');
-                $lists.each(function(listIndex) {
-                    const $list = $(this);
-                    const bookId = $list.closest('[data-type="book"]').attr('data-id');
-                    const $directChildren = $list.find('> [data-type="page"], > [data-type="chapter"]');
-                    $directChildren.each(function(directChildIndex) {
-                        const $childElem = $(this);
-                        const type = $childElem.attr('data-type');
-                        const parentChapter = false;
-                        const childId = $childElem.attr('data-id');
-
-                        entityMap.push({
-                            id: childId,
-                            sort: directChildIndex,
-                            parentChapter: parentChapter,
-                            type: type,
-                            book: bookId
-                        });
-
-                        $childElem.find('[data-type="page"]').each(function(pageIndex) {
-                            const $chapterChild = $(this);
-                            entityMap.push({
-                                id: $chapterChild.attr('data-id'),
-                                sort: pageIndex,
-                                parentChapter: childId,
-                                type: 'page',
-                                book: bookId
-                            });
-                        });
-
-                    });
-                });
-                return entityMap;
-            }
-
-
-            // Auto sort control
-            const sortOperations = {
-                name: function(a, b) {
-                    const aName = a.getAttribute('data-name').trim().toLowerCase();
-                    const bName = b.getAttribute('data-name').trim().toLowerCase();
-                    return aName.localeCompare(bName);
-                },
-                created: function(a, b) {
-                    const aTime = Number(a.getAttribute('data-created'));
-                    const bTime = Number(b.getAttribute('data-created'));
-                    return bTime - aTime;
-                },
-                updated: function(a, b) {
-                    const aTime = Number(a.getAttribute('data-updated'));
-                    const bTime = Number(b.getAttribute('data-updated'));
-                    return bTime - aTime;
-                },
-                chaptersFirst: function(a, b) {
-                    const aType = a.getAttribute('data-type');
-                    const bType = b.getAttribute('data-type');
-                    if (aType === bType) {
-                        return 0;
-                    }
-                    return (aType === 'chapter' ? -1 : 1);
-                },
-                chaptersLast: function(a, b) {
-                    const aType = a.getAttribute('data-type');
-                    const bType = b.getAttribute('data-type');
-                    if (aType === bType) {
-                        return 0;
-                    }
-                    return (aType === 'chapter' ? 1 : -1);
-                },
-            };
-
-            let lastSort = '';
-            let reverse = false;
-            const reversibleTypes = ['name', 'created', 'updated'];
-
-            $container.on('click', '.sort-box-options [data-sort]', function(event) {
-                event.preventDefault();
-                const $sortLists = $(this).closest('.sort-box').find('ul');
-                const sort = $(this).attr('data-sort');
-
-                reverse = (lastSort === sort) ? !reverse : false;
-                let sortFunction = sortOperations[sort];
-                if (reverse && reversibleTypes.includes(sort)) {
-                   sortFunction = function(a, b) {
-                       return 0 - sortOperations[sort](a, b)
-                   };
-                }
-
-                $sortLists.each(function() {
-                    const $list = $(this);
-                    $list.children('li').sort(sortFunction).appendTo($list);
-                });
-
-                lastSort = sort;
-                updateMapInput();
-            });
-
-        });
-    </script>
-@stop
index 36c7f9a243cbc0717a0a5da7637f650e578340f1..6137c34e8fce357653db2583e3a21cb413f01aba 100644 (file)
@@ -1,10 +1,11 @@
 <div class="chapter-child-menu">
-    <p chapter-toggle class="text-muted @if($bookChild->matchesOrContains($current)) open @endif">
+    <button chapter-toggle type="button" aria-expanded="{{ $isOpen ? 'true' : 'false' }}"
+            class="text-muted @if($isOpen) open @endif">
         @icon('caret-right') @icon('page') <span>{{ trans_choice('entities.x_pages', $bookChild->pages->count()) }}</span>
-    </p>
-    <ul class="sub-menu inset-list @if($bookChild->matchesOrContains($current)) open @endif">
+    </button>
+    <ul class="sub-menu inset-list @if($isOpen) open @endif" @if($isOpen) style="display: block;" @endif role="menu">
         @foreach($bookChild->pages as $childPage)
-            <li class="list-item-page {{ $childPage->isA('page') && $childPage->draft ? 'draft' : '' }}">
+            <li class="list-item-page {{ $childPage->isA('page') && $childPage->draft ? 'draft' : '' }}" role="presentation">
                 @include('partials.entity-list-item-basic', ['entity' => $childPage, 'classes' => $current->matches($childPage)? 'selected' : '' ])
             </li>
         @endforeach
index fd2c82b46563e507f4b9647ddfb15fcc9ba8ac59..c9787e6348991073a5c3e265097e0926b8179338 100644 (file)
             ]])
         </div>
 
-        <div class="content-wrap card">
+        <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')
             </form>
-        </div>
+        </main>
 
     </div>
 @stop
\ No newline at end of file
index 3444ee0fb19271f242352621d4c5ea3cd5799607..60f8c99339022535b48019d4232b3a764c831574 100644 (file)
@@ -27,7 +27,7 @@
 
                 <div class="text-right">
                     <a href="{{ $chapter->getUrl() }}" class="button outline">{{ trans('common.cancel') }}</a>
-                    <button type="submit" class="button primary">{{ trans('common.confirm') }}</button>
+                    <button type="submit" class="button">{{ trans('common.confirm') }}</button>
                 </div>
             </form>
         </div>
index d282fe1ddc9b614d5d40aede5f093dcacefa6792..d8bb056f632ce8d172da67ebb8b2e50ad7c092cb 100644 (file)
             ]])
         </div>
 
-        <div class="content-wrap card">
+        <main class="content-wrap card">
             <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])
             </form>
-        </div>
+        </main>
 
     </div>
 
index 2830855b4f8a0fc53a132ebda8cc9c19b5e818e9..580c123ccf6d7901491f80e943ff03a264ae3bb6 100644 (file)
@@ -1,5 +1,5 @@
 <!doctype html>
-<html lang="en">
+<html lang="{{ config('app.lang') }}">
 <head>
     <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
     <title>{{ $chapter->name }}</title>
index 31d8597de44c60681153908ed9f834bd837056d4..cd240e685dd651f5501a4659e74d8e145c0c1760 100644 (file)
@@ -12,9 +12,9 @@
 </div>
 
 <div class="form-group" collapsible id="logo-control">
-    <div class="collapse-title text-primary" collapsible-trigger>
-        <label for="user-avatar">{{ trans('entities.chapter_tags') }}</label>
-    </div>
+    <button type="button" class="collapse-title text-primary" collapsible-trigger aria-expanded="false">
+        <label for="tags">{{ trans('entities.chapter_tags') }}</label>
+    </button>
     <div class="collapse-content" collapsible-content>
         @include('components.tag-manager', ['entity' => isset($chapter)?$chapter:null, 'entityType' => 'chapter'])
     </div>
@@ -22,5 +22,5 @@
 
 <div class="form-group text-right">
     <a href="{{ isset($chapter) ? $chapter->getUrl() : $book->getUrl() }}" class="button outline">{{ trans('common.cancel') }}</a>
-    <button type="submit" class="button primary">{{ trans('entities.chapters_save') }}</button>
+    <button type="submit" class="button">{{ trans('entities.chapters_save') }}</button>
 </div>
index fd463e07a4fbfc51945c45678f2761e22cd8ef9f..7e2e0e1c539c9dca666540a245c5cf2342ea9384 100644 (file)
@@ -11,7 +11,9 @@
     <div class="chapter chapter-expansion">
         <span class="icon text-chapter">@icon('page')</span>
         <div class="content">
-            <div chapter-toggle class="text-muted chapter-expansion-toggle">@icon('caret-right') <span>{{ trans_choice('entities.x_pages', $chapter->pages->count()) }}</span></div>
+            <button type="button" chapter-toggle
+                    aria-expanded="false"
+                    class="text-muted chapter-expansion-toggle">@icon('caret-right') <span>{{ trans_choice('entities.x_pages', $chapter->pages->count()) }}</span></button>
             <div class="inset-list">
                 <div class="entity-list-item-children">
                     @include('partials.entity-list', ['entities' => $chapter->pages])
index 7f3de1322bdcaf94200ef5dfbeddbb358f190bf3..8663dca5050e53fb0dbbc9ab6b369d720bbab59a 100644 (file)
@@ -15,7 +15,7 @@
             ]])
         </div>
 
-        <div class="card content-wrap">
+        <main class="card content-wrap">
             <h1 class="list-heading">{{ trans('entities.chapters_move') }}</h1>
 
             <form action="{{ $chapter->getUrl('/move') }}" method="POST">
 
                 <div class="form-group text-right">
                     <a href="{{ $chapter->getUrl() }}" class="button outline">{{ trans('common.cancel') }}</a>
-                    <button type="submit" class="button primary">{{ trans('entities.chapters_move') }}</button>
+                    <button type="submit" class="button">{{ trans('entities.chapters_move') }}</button>
                 </div>
             </form>
 
-        </div>
+        </main>
 
 
 
index cb5808e7d5070f9fa0c3e2b0dfc1fa273248c24b..48c954dc96871b0f23154772ec2ba6e79a600d80 100644 (file)
             ]])
         </div>
 
-        <div class="card content-wrap">
+        <main class="card content-wrap">
             <h1 class="list-heading">{{ trans('entities.chapters_permissions') }}</h1>
             @include('form.entity-permissions', ['model' => $chapter])
-        </div>
+        </main>
     </div>
 
 @stop
index 0915f9a16120afd97a9a9af1e4f0f0ed278fd36d..105cda760ff496c909aadf723c274fd12b64637b 100644 (file)
@@ -8,14 +8,14 @@
 
 @section('body')
 
-    <div class="mb-m">
+    <div class="mb-m print-hidden">
         @include('partials.breadcrumbs', ['crumbs' => [
             $chapter->book,
             $chapter,
         ]])
     </div>
 
-    <div class="content-wrap card">
+    <main class="content-wrap card">
         <h1 class="break-text" v-pre>{{ $chapter->name }}</h1>
         <div class="chapter-content" v-show="!searching">
             <p v-pre class="text-muted break-text">{!! nl2br(e($chapter->description)) !!}</p>
@@ -50,7 +50,7 @@
         </div>
 
         @include('partials.entity-dashboard-search-results')
-    </div>
+    </main>
 
 @stop
 
 
             <hr class="primary-background"/>
 
-            <div dropdown class="dropdown-container">
-                <div dropdown-toggle class="icon-list-item">
-                    <span>@icon('export')</span>
-                    <span>{{ trans('entities.export') }}</span>
-                </div>
-                <ul class="wide dropdown-menu">
-                    <li><a href="{{ $chapter->getUrl('/export/html') }}" target="_blank">{{ trans('entities.export_html') }} <span class="text-muted float right">.html</span></a></li>
-                    <li><a href="{{ $chapter->getUrl('/export/pdf') }}" target="_blank">{{ trans('entities.export_pdf') }} <span class="text-muted float right">.pdf</span></a></li>
-                    <li><a href="{{ $chapter->getUrl('/export/plaintext') }}" target="_blank">{{ trans('entities.export_text') }} <span class="text-muted float right">.txt</span></a></li>
-                </ul>
-            </div>
+            @include('partials.entity-export-menu', ['entity' => $chapter])
         </div>
     </div>
 @stop
index a855031784dc2c181b36691467564064386078c9..ea96a9250dd24a22f2bf274fc093f52edcd6454e 100644 (file)
@@ -1,8 +1,8 @@
 <div class="comment-box mb-m" comment="{{ $comment->id }}" local-id="{{$comment->local_id}}" parent-id="{{$comment->parent_id}}" id="comment{{$comment->local_id}}">
     <div class="header p-s">
-        <div class="grid half no-gap v-center">
-            <div class="meta">
-                <a href="#comment{{$comment->local_id}}" class="text-muted">#{{$comment->local_id}}</a>
+        <div class="grid half left-focus no-gap v-center">
+            <div class="meta text-muted text-small">
+                <a href="#comment{{$comment->local_id}}">#{{$comment->local_id}}</a>
                 &nbsp;&nbsp;
                 @if ($comment->createdBy)
                     <img width="50" src="{{ $comment->createdBy->getAvatar(50) }}" class="avatar" alt="{{ $comment->createdBy->name }}">
             </div>
             <div class="actions text-right">
                 @if(userCan('comment-update', $comment))
-                    <button type="button" class="text-button" action="edit" title="{{ trans('common.edit') }}">@icon('edit')</button>
+                    <button type="button" class="text-button" action="edit" aria-label="{{ trans('common.edit') }}" title="{{ trans('common.edit') }}">@icon('edit')</button>
                 @endif
                 @if(userCan('comment-create-all'))
-                    <button type="button" class="text-button" action="reply" title="{{ trans('common.reply') }}">@icon('reply')</button>
+                    <button type="button" class="text-button" action="reply" aria-label="{{ trans('common.reply') }}" title="{{ trans('common.reply') }}">@icon('reply')</button>
                 @endif
                 @if(userCan('comment-delete', $comment))
                     <div dropdown class="dropdown-container">
-                        <button type="button" dropdown-toggle class="text-button" title="{{ trans('common.delete') }}">@icon('delete')</button>
-                        <ul class="dropdown-menu">
+                        <button type="button" dropdown-toggle aria-haspopup="true" aria-expanded="false" class="text-button" title="{{ trans('common.delete') }}">@icon('delete')</button>
+                        <ul class="dropdown-menu" role="menu">
                             <li class="px-m text-small text-muted pb-s">{{trans('entities.comment_delete_confirm')}}</li>
-                            <li><a action="delete" class="text-button text-neg" >@icon('delete'){{ trans('common.delete') }}</a></li>
+                            <li><button action="delete" type="button" class="text-button text-neg" >@icon('delete'){{ trans('common.delete') }}</button></li>
                         </ul>
                     </div>
                 @endif
@@ -61,7 +61,7 @@
                 </div>
                 <div class="form-group text-right">
                     <button type="button" class="button outline" action="closeUpdateForm">{{ trans('common.cancel') }}</button>
-                    <button type="submit" class="button primary">{{ trans('entities.comment_save') }}</button>
+                    <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')])
index 4848977c9cd16c55a527c91354cc61c3c61c1fd0..fc81f13ee73053e4c4e208f64cc64e9eb8183826 100644 (file)
@@ -1,8 +1,16 @@
-<div page-comments page-id="{{ $page->id }}" class="comments-list">
-    <div comment-count-bar class="grid half left-focus v-center">
+<section page-comments page-id="{{ $page->id }}" class="comments-list" aria-label="{{ trans('entities.comments') }}">
+
+    @exposeTranslations([
+        'entities.comment_updated_success',
+        'entities.comment_deleted_success',
+        'entities.comment_created_success',
+        'entities.comment_count',
+    ])
+
+    <div comment-count-bar class="grid half left-focus v-center no-row-gap">
         <h5 comments-title>{{ trans_choice('entities.comment_count', count($page->comments), ['count' => count($page->comments)]) }}</h5>
-        @if (count($page->comments) === 0)
-            <div class="text-right" comment-add-button-container>
+        @if (count($page->comments) === 0 && userCan('comment-create-all'))
+            <div class="text-m-right" comment-add-button-container>
                 <button type="button" action="addComment"
                         class="button outline">{{ trans('entities.comment_add') }}</button>
             </div>
 
     @if(userCan('comment-create-all'))
         @include('comments.create')
-    @endif
 
-    @if (count($page->comments) > 0)
-        <div class="text-right" comment-add-button-container>
-            <button type="button" action="addComment"
-                    class="button outline">{{ trans('entities.comment_add') }}</button>
-        </div>
+        @if (count($page->comments) > 0)
+            <div class="text-right" comment-add-button-container>
+                <button type="button" action="addComment"
+                        class="button outline">{{ trans('entities.comment_add') }}</button>
+            </div>
+        @endif
     @endif
 
-</div>
\ No newline at end of file
+</section>
\ No newline at end of file
index abd95f008a1e5be44d550f7f1438f1890bdc54ea..61e41a354fab3214883296238e9ee55ac7c9a130 100644 (file)
@@ -19,7 +19,7 @@
             <div class="form-group text-right">
                 <button type="button" class="button outline"
                         action="hideForm">{{ trans('common.cancel') }}</button>
-                <button type="submit" class="button primary">{{ trans('entities.comment_save') }}</button>
+                <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')])
index 734789899b1e804f65efc8ea2fecbbbc040fbef4..19299695042e98ded5b55735865c31f6fee9528b 100644 (file)
@@ -2,9 +2,9 @@
     <div class="grid mx-l">
 
         <div>
-            <a href="{{ baseUrl('/') }}" class="logo">
+            <a href="{{ url('/') }}" class="logo">
                 @if(setting('app-logo', '') !== 'none')
-                    <img class="logo-image" src="{{ setting('app-logo', '') === '' ? baseUrl('/logo.png') : baseUrl(setting('app-logo', '')) }}" alt="Logo">
+                    <img class="logo-image" src="{{ setting('app-logo', '') === '' ? url('/logo.png') : url(setting('app-logo', '')) }}" alt="Logo">
                 @endif
                 @if (setting('app-name-header'))
                     <span class="logo-text">{{ setting('app-name') }}</span>
 
         <div class="header-search hide-under-l">
             @if (hasAppAccess())
-            <form action="{{ baseUrl('/search') }}" method="GET" class="search-box">
-                <button id="header-search-box-button" type="submit">@icon('search') </button>
-                <input id="header-search-box-input" type="text" name="term" tabindex="2" placeholder="{{ trans('common.search') }}" value="{{ isset($searchTerm) ? $searchTerm : '' }}">
+            <form action="{{ url('/search') }}" method="GET" class="search-box" role="search">
+                <button id="header-search-box-button" type="submit" aria-label="{{ trans('common.search') }}" tabindex="-1">@icon('search') </button>
+                <input id="header-search-box-input" type="text" name="term"
+                       aria-label="{{ trans('common.search') }}" placeholder="{{ trans('common.search') }}"
+                       value="{{ isset($searchTerm) ? $searchTerm : '' }}">
             </form>
             @endif
         </div>
 
         <div class="text-right">
-            <div class="header-links">
+            <nav class="header-links" >
                 <div class="links text-center">
                     @if (hasAppAccess())
-                        <a class="hide-over-l" href="{{ baseUrl('/search') }}">@icon('search'){{ trans('common.search') }}</a>
+                        <a class="hide-over-l" href="{{ url('/search') }}">@icon('search'){{ trans('common.search') }}</a>
                         @if(userCanOnAny('view', \BookStack\Entities\Bookshelf::class) || userCan('bookshelf-view-all') || userCan('bookshelf-view-own'))
-                            <a href="{{ baseUrl('/shelves') }}">@icon('bookshelf'){{ trans('entities.shelves') }}</a>
+                            <a href="{{ url('/shelves') }}">@icon('bookshelf'){{ trans('entities.shelves') }}</a>
                         @endif
-                        <a href="{{ baseUrl('/books') }}">@icon('books'){{ trans('entities.books') }}</a>
+                        <a href="{{ url('/books') }}">@icon('books'){{ trans('entities.books') }}</a>
                         @if(signedInUser() && userCan('settings-manage'))
-                            <a href="{{ baseUrl('/settings') }}">@icon('settings'){{ trans('settings.settings') }}</a>
+                            <a href="{{ url('/settings') }}">@icon('settings'){{ trans('settings.settings') }}</a>
                         @endif
                         @if(signedInUser() && userCan('users-manage') && !userCan('settings-manage'))
-                            <a href="{{ baseUrl('/settings/users') }}">@icon('users'){{ trans('settings.users') }}</a>
+                            <a href="{{ url('/settings/users') }}">@icon('users'){{ trans('settings.users') }}</a>
                         @endif
                     @endif
 
                     @if(!signedInUser())
                         @if(setting('registration-enabled', false))
-                            <a href="{{ baseUrl('/register') }}">@icon('new-user') {{ trans('auth.sign_up') }}</a>
+                            <a href="{{ url('/register') }}">@icon('new-user') {{ trans('auth.sign_up') }}</a>
                         @endif
-                        <a href="{{ baseUrl('/login') }}">@icon('login') {{ trans('auth.log_in') }}</a>
+                        <a href="{{ url('/login') }}">@icon('login') {{ trans('auth.log_in') }}</a>
                     @endif
                 </div>
                 @if(signedInUser())
                     <?php $currentUser = user(); ?>
                     <div class="dropdown-container" dropdown>
-                        <span class="user-name hide-under-l" dropdown-toggle>
+                        <span class="user-name py-s hide-under-l" dropdown-toggle
+                              aria-haspopup="true" aria-expanded="false" aria-label="{{ trans('common.profile_menu') }}" tabindex="0">
                             <img class="avatar" src="{{$currentUser->getAvatar(30)}}" alt="{{ $currentUser->name }}">
                             <span class="name">{{ $currentUser->getShortName(9) }}</span> @icon('caret-down')
                         </span>
-                        <ul class="dropdown-menu">
+                        <ul class="dropdown-menu" role="menu">
                             <li>
-                                <a href="{{ baseUrl("/user/{$currentUser->id}") }}">@icon('user'){{ trans('common.view_profile') }}</a>
+                                <a href="{{ url("/user/{$currentUser->id}") }}">@icon('user'){{ trans('common.view_profile') }}</a>
                             </li>
                             <li>
-                                <a href="{{ baseUrl("/settings/users/{$currentUser->id}") }}">@icon('edit'){{ trans('common.edit_profile') }}</a>
+                                <a href="{{ url("/settings/users/{$currentUser->id}") }}">@icon('edit'){{ trans('common.edit_profile') }}</a>
                             </li>
                             <li>
-                                <a href="{{ baseUrl('/logout') }}">@icon('logout'){{ trans('auth.logout') }}</a>
+                                <a href="{{ url('/logout') }}">@icon('logout'){{ trans('auth.logout') }}</a>
                             </li>
                         </ul>
                     </div>
                 @endif
-            </div>
+            </nav>
         </div>
 
     </div>
index 0efaa32ec7305c94526a201e55d28b3832b9d0b9..7644eeb8871278e6b0618d911a5a935c5fd5f948 100644 (file)
@@ -1,23 +1,19 @@
-@extends('simple-layout')
+@extends('tri-layout')
 
 @section('body')
-    <div class="container mt-m">
-        <div class="grid right-focus gap-xl">
-            <div>
+    @include('books.list', ['books' => $books, 'view' => $view])
+@stop
 
-                <div class="actions mb-xl">
-                    <h5>{{ trans('common.actions') }}</h5>
-                    <div class="icon-list text-primary">
-                        @include('partials.view-toggle', ['view' => $view, 'type' => 'book'])
-                        @include('components.expand-toggle', ['target' => '.entity-list.compact .entity-item-snippet', 'key' => 'home-details'])
-                    </div>
-                </div>
+@section('left')
+    @include('common.home-sidebar')
+@stop
 
-                @include('common.home-sidebar')
-            </div>
-            <div>
-                @include('books.list', ['books' => $books, 'view' => $view])
-            </div>
+@section('right')
+    <div class="actions mb-xl">
+        <h5>{{ trans('common.actions') }}</h5>
+        <div class="icon-list text-primary">
+            @include('partials.view-toggle', ['view' => $view, 'type' => 'book'])
+            @include('components.expand-toggle', ['target' => '.entity-list.compact .entity-item-snippet', 'key' => 'home-details'])
         </div>
     </div>
 @stop
\ No newline at end of file
index f1133a60755afa3ade5697d537dc017d5e54a9d8..56e281dcb7fd107c52317ba0f856ce9c4310a103 100644 (file)
@@ -1,26 +1,24 @@
-@extends('simple-layout')
+@extends('tri-layout')
 
 @section('body')
-    <div class="container mt-l">
-        <div class="grid right-focus gap-xl">
-            <div>
+    <div class="mt-m">
+        <main class="content-wrap card">
+            <div class="page-content" page-display="{{ $customHomepage->id }}">
+                @include('pages.page-display', ['page' => $customHomepage])
+            </div>
+        </main>
+    </div>
+@stop
 
-                <div class="actions mb-xl">
-                    <h5>{{ trans('common.actions') }}</h5>
-                    <div class="icon-list text-primary">
-                        @include('components.expand-toggle', ['target' => '.entity-list.compact .entity-item-snippet', 'key' => 'home-details'])
-                    </div>
-                </div>
+@section('left')
+    @include('common.home-sidebar')
+@stop
 
-                @include('common.home-sidebar')
-            </div>
-            <div>
-                <div class="content-wrap card">
-                    <div class="page-content" page-display="{{ $customHomepage->id }}">
-                        @include('pages.page-display', ['page' => $customHomepage])
-                    </div>
-                </div>
-            </div>
+@section('right')
+    <div class="actions mb-xl">
+        <h5>{{ trans('common.actions') }}</h5>
+        <div class="icon-list text-primary">
+            @include('components.expand-toggle', ['target' => '.entity-list.compact .entity-item-snippet', 'key' => 'home-details'])
         </div>
     </div>
 @stop
\ No newline at end of file
index f09bfd4169659d136e54544bdcf620a82e67b114..a9c58589386df4ac996facacd32c3edc8c3c4753 100644 (file)
@@ -1,23 +1,19 @@
-@extends('simple-layout')
+@extends('tri-layout')
 
 @section('body')
-    <div class="container mt-m">
-        <div class="grid right-focus gap-xl">
-            <div>
+    @include('shelves.list', ['shelves' => $shelves, 'view' => $view])
+@stop
 
-                <div class="actions mb-xl">
-                    <h5>{{ trans('common.actions') }}</h5>
-                    <div class="icon-list text-primary">
-                        @include('partials.view-toggle', ['view' => $view, 'type' => 'shelf'])
-                        @include('components.expand-toggle', ['target' => '.entity-list.compact .entity-item-snippet', 'key' => 'home-details'])
-                    </div>
-                </div>
+@section('left')
+    @include('common.home-sidebar')
+@stop
 
-                @include('common.home-sidebar')
-            </div>
-            <div>
-                @include('shelves.list', ['shelves' => $shelves, 'view' => $view])
-            </div>
+@section('right')
+    <div class="actions mb-xl">
+        <h5>{{ trans('common.actions') }}</h5>
+        <div class="icon-list text-primary">
+            @include('partials.view-toggle', ['view' => $view, 'type' => 'shelf'])
+            @include('components.expand-toggle', ['target' => '.entity-list.compact .entity-item-snippet', 'key' => 'home-details'])
         </div>
     </div>
 @stop
\ No newline at end of file
index 07eda2cff997ac848a1c7f63b1720d8ce6226212..12adda618905a59033b8b40cd6326ee2ab26cf05 100644 (file)
@@ -15,7 +15,7 @@
 </div>
 
 <div class="mb-xl">
-    <h5><a class="no-color" href="{{ baseUrl("/pages/recently-updated") }}">{{ trans('entities.recently_updated_pages') }}</a></h5>
+    <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', [
         'entities' => $recentlyUpdatedPages,
index 2f0189f872c3ef86f61f4c66a64c8db2bd60935a..cd27ff5687e006fffa598cb30f4e2243601e4243 100644 (file)
@@ -34,7 +34,7 @@
 
             <div>
                 <div id="recent-pages" class="card mb-xl">
-                    <h3 class="card-title"><a class="no-color" href="{{ baseUrl("/pages/recently-updated") }}">{{ trans('entities.recently_updated_pages') }}</a></h3>
+                    <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', [
                         'entities' => $recentlyUpdatedPages,
index 7636cd581b63c8c8923ed96ea3101039d54332b0..31a583182cde7bf0b9d55fb26df1f6e71e295b8a 100644 (file)
@@ -1,6 +1,6 @@
 <div id="code-editor">
     <div overlay ref="overlay" v-cloak @click="hide()">
-        <div class="popup-body" @click.stop>
+        <div class="popup-body" tabindex="-1" @click.stop>
 
             <div class="popup-header primary-background">
                 <div class="popup-title">{{ trans('components.code_editor') }}</div>
@@ -43,7 +43,7 @@
                 </div>
 
                 <div class="form-group">
-                    <button type="button" class="button primary" @click="save()">{{ trans('components.code_save') }}</button>
+                    <button type="button" class="button" @click="save()">{{ trans('components.code_save') }}</button>
                 </div>
 
             </div>
index 73b7496f8d9e2b62130f4a9c97fbfc96c25edeb3..2bf4e223201a5e04b8accd48b575f7b887f378c5 100644 (file)
@@ -4,8 +4,10 @@ $value
 $checked
 $label
 --}}
-<label class="toggle-switch @if($errors->has($name)) text-neg @endif">
+<label custom-checkbox class="toggle-switch @if($errors->has($name)) text-neg @endif">
     <input type="checkbox" name="{{$name}}" value="{{ $value }}" @if($checked) checked="checked" @endif>
-    <span class="custom-checkbox text-primary">@icon('check')</span>
+    <span tabindex="0" role="checkbox"
+          aria-checked="{{ $checked ? 'true' : 'false' }}"
+          class="custom-checkbox text-primary">@icon('check')</span>
     <span class="label">{{$label}}</span>
 </label>
\ No newline at end of file
index c497a16d594563db25c1eb6e0e902857d7814c57..0beee658d88334bd598d79b185dae116fe6aa838 100644 (file)
@@ -1,13 +1,13 @@
 <div id="entity-selector-wrap">
     <div overlay entity-selector-popup>
-        <div class="popup-body small">
+        <div class="popup-body small" tabindex="-1">
             <div class="popup-header primary-background">
                 <div class="popup-title">{{ trans('entities.entity_select') }}</div>
                 <button type="button" class="popup-header-close">x</button>
             </div>
             @include('components.entity-selector', ['name' => 'entity-selector'])
             <div class="popup-footer">
-                <button type="button" disabled="true" class="button entity-link-selector-confirm primary corner-button">{{ trans('common.select') }}</button>
+                <button type="button" disabled="true" class="button entity-link-selector-confirm corner-button">{{ trans('common.select') }}</button>
             </div>
         </div>
     </div>
index e8b2220e5cb981445a3666e629b9cfee0863968e..a24f9ac1e9ec79c7d1c55ed95250f177bf9f42ee 100644 (file)
@@ -3,13 +3,13 @@ $target - CSS selector of items to expand
 $key - Unique key for checking existing stored state.
 --}}
 <?php $isOpen = setting()->getForCurrentUser('section_expansion#'. $key); ?>
-<a expand-toggle="{{ $target }}"
-   expand-toggle-update-endpoint="{{ baseUrl('/settings/users/'. $currentUser->id .'/update-expansion-preference/' . $key) }}"
+<button type="button" expand-toggle="{{ $target }}"
+   expand-toggle-update-endpoint="{{ url('/settings/users/'. $currentUser->id .'/update-expansion-preference/' . $key) }}"
    expand-toggle-is-open="{{ $isOpen ? 'yes' : 'no' }}"
    class="text-muted icon-list-item text-primary">
     <span>@icon('expand-text')</span>
     <span>{{ trans('common.toggle_details') }}</span>
-</a>
+</button>
 @if($isOpen)
     @push('head')
         <style>
index 7c9084ad102fa1b2690517c950d413a5cc59be0a..0971c3ed95ed7705da9c476c790d05acea3bc71d 100644 (file)
@@ -1,6 +1,15 @@
 <div id="image-manager" image-type="{{ $imageType }}" uploaded-to="{{ $uploaded_to ?? 0 }}">
+
+    @exposeTranslations([
+        'components.image_delete_success',
+        'components.image_upload_success',
+        'errors.server_upload_limit',
+        'components.image_upload_remove',
+        'components.file_upload_timeout',
+    ])
+
     <div overlay v-cloak @click="hide">
-        <div class="popup-body" @click.stop="">
+        <div class="popup-body" tabindex="-1" @click.stop>
 
             <div class="popup-header primary-background">
                 <div class="popup-title">{{ trans('components.image_select') }}</div>
@@ -63,7 +72,7 @@
                                     <button type="button" class="button icon outline" @click="deleteImage">@icon('delete')</button>
 
                                 </div>
-                                <button class="button primary anim fadeIn float right" v-show="selectedImage" @click="callbackAndHide(selectedImage)">
+                                <button class="button anim fadeIn float right" v-show="selectedImage" @click="callbackAndHide(selectedImage)">
                                     {{ trans('components.image_select_image') }}
                                 </button>
                                 <div class="clearfix"></div>
index 73885aeb4548c54aa085c795caed53f081de72f7..9c2661cccbd3d19c412a39af93837ad0be8ab205 100644 (file)
@@ -8,8 +8,8 @@
         </div>
         <div class="text-center">
 
+            <input type="file" class="custom-file-input" accept="image/*" name="{{ $name }}" id="{{ $name }}">
             <label for="{{ $name }}" class="button outline">{{ trans('components.image_select_image') }}</label>
-            <input type="file" class="hidden" accept="image/*" name="{{ $name }}" id="{{ $name }}">
             <input type="hidden" data-reset-input name="{{ $name }}_reset" value="true" disabled="disabled">
             @if(isset($removeName))
                 <input type="hidden" data-remove-input name="{{ $removeName }}" value="{{ $removeValue }}" disabled="disabled">
index 7a3285fa76afe1215c1939c639056e6f53193b8f..e24ea49f1c82a7a374f8c8cf0c51392a40cac943 100644 (file)
@@ -3,7 +3,7 @@
 <div page-picker>
     <div class="input-base">
         <span @if($value) style="display: none" @endif page-picker-default class="text-muted italic">{{ $placeholder }}</span>
-        <a @if(!$value) style="display: none" @endif href="{{ baseUrl('/link/' . $value) }}" target="_blank" class="text-page" page-picker-display>#{{$value}}, {{$value ? \BookStack\Entities\Page::find($value)->name : '' }}</a>
+        <a @if(!$value) style="display: none" @endif href="{{ url('/link/' . $value) }}" target="_blank" class="text-page" page-picker-display>#{{$value}}, {{$value ? \BookStack\Entities\Page::find($value)->name : '' }}</a>
     </div>
     <br>
     <input type="hidden" value="{{$value}}" name="{{$name}}" id="{{$name}}">
index 1d1cc2d806153baa00d770e3ca05b4735fb5177b..f7a9c6c48623c36d92f9e0434532a1388f5753c7 100644 (file)
@@ -1,6 +1,6 @@
 @foreach($entity->tags as $tag)
     <div class="tag-item primary-background-light">
-        <div class="tag-name"><a href="{{ baseUrl('/search?term=%5B' . urlencode($tag->name) .'%5D') }}">@icon('tag'){{ $tag->name }}</a></div>
-        @if($tag->value) <div class="tag-value"><a href="{{ baseUrl('/search?term=%5B' . urlencode($tag->name) .'%3D' . urlencode($tag->value) . '%5D') }}">{{$tag->value}}</a></div> @endif
+        <div class="tag-name"><a href="{{ url('/search?term=%5B' . urlencode($tag->name) .'%5D') }}">@icon('tag'){{ $tag->name }}</a></div>
+        @if($tag->value) <div class="tag-value"><a href="{{ url('/search?term=%5B' . urlencode($tag->name) .'%3D' . urlencode($tag->value) . '%5D') }}">{{$tag->value}}</a></div> @endif
     </div>
 @endforeach
\ No newline at end of file
index 5ae3831986bc8aa8eeee34a09f72275f63f4811a..2878569374d6db6bd80928a3fe34d8477d982124 100644 (file)
@@ -2,19 +2,18 @@
     <div class="tags">
         <p class="text-muted small">{!! nl2br(e(trans('entities.tags_explain'))) !!}</p>
 
-
         <draggable :options="{handle: '.handle'}" :list="tags" element="div">
             <div v-for="(tag, i) in tags" :key="tag.key" class="card drag-card">
                 <div class="handle" >@icon('grip')</div>
                 <div>
-                    <autosuggest url="{{ baseUrl('/ajax/tags/suggest/names') }}" type="name" class="outline" :name="getTagFieldName(i, 'name')"
-                                 v-model="tag.name" @input="tagChange(tag)" @blur="tagBlur(tag)" placeholder="{{ trans('entities.tag') }}"/>
+                    <autosuggest url="{{ url('/ajax/tags/suggest/names') }}" type="name" class="outline" :name="getTagFieldName(i, 'name')"
+                                 v-model="tag.name" @input="tagChange(tag)" @blur="tagBlur(tag)" placeholder="{{ trans('entities.tag_name') }}"/>
                 </div>
                 <div>
-                    <autosuggest url="{{ baseUrl('/ajax/tags/suggest/values') }}" type="value" class="outline" :name="getTagFieldName(i, 'value')"
+                    <autosuggest url="{{ url('/ajax/tags/suggest/values') }}" type="value" class="outline" :name="getTagFieldName(i, 'value')"
                                  v-model="tag.value" @change="tagChange(tag)" @blur="tagBlur(tag)" placeholder="{{ trans('entities.tag_value') }}"/>
                 </div>
-                <div v-show="tags.length !== 1" class="text-center drag-card-action text-neg" @click="removeTag(tag)">@icon('close')</div>
+                <button type="button" aria-label="{{ trans('entities.tags_remove') }}" v-show="tags.length !== 1" class="text-center drag-card-action text-neg" @click="removeTag(tag)">@icon('close')</button>
             </div>
         </draggable>
 
index 84a8a3083716972b1a1a53a6302aec62d3ed6256..a5eec30051b32a9bcd9e983c0fd98a40c4356ca9 100644 (file)
@@ -1,6 +1,8 @@
-<label toggle-switch="{{$name}}" class="toggle-switch">
+<label toggle-switch="{{$name}}" custom-checkbox class="toggle-switch">
     <input type="hidden" name="{{$name}}" value="{{$value?'true':'false'}}"/>
     <input type="checkbox" @if($value) checked="checked" @endif>
-    <span class="custom-checkbox text-primary">@icon('check')</span>
+    <span tabindex="0" role="checkbox"
+          aria-checked="{{ $value ? 'true' : 'false' }}"
+          class="custom-checkbox text-primary">@icon('check')</span>
     <span class="label">{{ $label }}</span>
 </label>
\ No newline at end of file
index 228c51520e7d92c47a94cf1ef5bbebd3fb1af054..9c599307ed0279c22de797ea8790717aa91cadf8 100644 (file)
@@ -10,7 +10,7 @@
                 <h5>{{ trans('errors.sorry_page_not_found') }}</h5>
             </div>
             <div class="text-right">
-                <a href="{{ baseUrl('/') }}" class="button outline">{{ trans('errors.return_home') }}</a>
+                <a href="{{ url('/') }}" class="button outline">{{ trans('errors.return_home') }}</a>
             </div>
         </div>
 
@@ -22,7 +22,7 @@
                 <div class="card mb-xl">
                     <h3 class="card-title">{{ trans('entities.pages_popular') }}</h3>
                     <div class="px-m">
-                        @include('partials.entity-list', ['entities' => Views::getPopular(10, 0, 'page'), 'style' => 'compact'])
+                        @include('partials.entity-list', ['entities' => Views::getPopular(10, 0, ['page']), 'style' => 'compact'])
                     </div>
                 </div>
             </div>
@@ -30,7 +30,7 @@
                 <div class="card mb-xl">
                     <h3 class="card-title">{{ trans('entities.books_popular') }}</h3>
                     <div class="px-m">
-                        @include('partials.entity-list', ['entities' => Views::getPopular(10, 0, 'book'), 'style' => 'compact'])
+                        @include('partials.entity-list', ['entities' => Views::getPopular(10, 0, ['book']), 'style' => 'compact'])
                     </div>
                 </div>
             </div>
@@ -38,7 +38,7 @@
                 <div class="card mb-xl">
                     <h3 class="card-title">{{ trans('entities.chapters_popular') }}</h3>
                     <div class="px-m">
-                        @include('partials.entity-list', ['entities' => Views::getPopular(10, 0, 'chapter'), 'style' => 'compact'])
+                        @include('partials.entity-list', ['entities' => Views::getPopular(10, 0, ['chapter']), 'style' => 'compact'])
                     </div>
                 </div>
             </div>
index 3745f2292f733ce4c18f1af01d6f9e0130c7c288..8c6822767a1f823e67782a3d0a0fb96ad89cd8e2 100644 (file)
@@ -7,7 +7,7 @@
             <h3 class="text-muted">{{ trans('errors.error_occurred') }}</h3>
             <div class="body">
                 <h5>{{ $message ?? 'An unknown error occurred' }}</h5>
-                <p><a href="{{ baseUrl('/') }}" class="button outline">{{ trans('errors.return_home') }}</a></p>
+                <p><a href="{{ url('/') }}" class="button outline">{{ trans('errors.return_home') }}</a></p>
             </div>
         </div>
     </div>
index 6bb4b51ada62824b103fce9604b43209db24ebc4..3581a545b1c96f6b082b11c3a379747251f13ad1 100644 (file)
@@ -1,4 +1,4 @@
-<form action="{{ $model->getUrl('/permissions') }}" method="POST">
+<form action="{{ $model->getUrl('/permissions') }}" method="POST" entity-permissions-editor>
     {!! csrf_field() !!}
     <input type="hidden" name="_method" value="PUT">
 
@@ -11,7 +11,7 @@
         ])
     </div>
 
-    <table permissions-table class="table permissions-table toggle-switch-list">
+    <table permissions-table class="table permissions-table toggle-switch-list" style="{{ !$model->restricted ? 'display: none' : '' }}">
         <tr>
             <th>{{ trans('common.role') }}</th>
             <th @if($model->isA('page')) colspan="3" @else colspan="4" @endif>
@@ -19,7 +19,7 @@
                 <a href="#" permissions-table-toggle-all class="text-small ml-m text-primary">{{ trans('common.toggle_all') }}</a>
             </th>
         </tr>
-        @foreach($roles as $role)
+        @foreach(\BookStack\Auth\Role::restrictable() as $role)
             <tr>
                 <td width="33%" class="pt-m">
                     {{ $role->display_name }}
@@ -37,6 +37,6 @@
 
     <div class="text-right">
         <a href="{{ $model->getUrl() }}" class="button outline">{{ trans('common.cancel') }}</a>
-        <button type="submit" class="button primary">{{ trans('entities.permissions_save') }}</button>
+        <button type="submit" class="button">{{ trans('entities.permissions_save') }}</button>
     </div>
 </form>
\ No newline at end of file
index 8f7c4ee01e5c8384596884f7e390f9d306c293c0..a3c868a8dc08ba293e624f27aa8f1229aa5aa039 100644 (file)
@@ -1,7 +1,6 @@
 <input type="password" id="{{ $name }}" name="{{ $name }}"
        @if($errors->has($name)) class="text-neg" @endif
        @if(isset($placeholder)) placeholder="{{$placeholder}}" @endif
-       @if(isset($tabindex)) tabindex="{{$tabindex}}" @endif
        @if(old($name)) value="{{ old($name)}}" @endif>
 @if($errors->has($name))
     <div class="text-neg text-small">{{ $errors->first($name) }}</div>
index 948a55cbc10a2b6e3f68e16626f8a0ff785394a8..4b3631a06566158d2286df4f0e662b0b4dfd376a 100644 (file)
@@ -1,8 +1,9 @@
 <input type="text" id="{{ $name }}" name="{{ $name }}"
        @if($errors->has($name)) class="text-neg" @endif
        @if(isset($placeholder)) placeholder="{{$placeholder}}" @endif
-       @if(isset($tabindex)) tabindex="{{$tabindex}}" @endif
+       @if($autofocus ?? false) autofocus @endif
+       @if($disabled ?? false) disabled="disabled" @endif
        @if(isset($model) || old($name)) value="{{ old($name) ? old($name) : $model->$name}}" @endif>
 @if($errors->has($name))
     <div class="text-neg text-small">{{ $errors->first($name) }}</div>
-@endif
\ No newline at end of file
+@endif
diff --git a/resources/views/pages/attachment-manager.blade.php b/resources/views/pages/attachment-manager.blade.php
new file mode 100644 (file)
index 0000000..dd00678
--- /dev/null
@@ -0,0 +1,102 @@
+<div toolbox-tab-content="files" id="attachment-manager" page-id="{{ $page->id ?? 0 }}">
+
+    @exposeTranslations([
+    'entities.attachments_file_uploaded',
+    'entities.attachments_file_updated',
+    'entities.attachments_link_attached',
+    'entities.attachments_updated_success',
+    'errors.server_upload_limit',
+    'components.image_upload_remove',
+    'components.file_upload_timeout',
+    ])
+
+    <h4>{{ trans('entities.attachments') }}</h4>
+    <div class="px-l files">
+
+        <div id="file-list" v-show="!fileToEdit">
+            <p class="text-muted small">{{ trans('entities.attachments_explain') }} <span class="text-warn">{{ trans('entities.attachments_explain_instant_save') }}</span></p>
+
+            <div class="tab-container">
+                <div class="nav-tabs">
+                    <button type="button" @click="tab = 'list'" :class="{selected: tab === 'list'}"
+                            class="tab-item">{{ trans('entities.attachments_items') }}</button>
+                    <button type="button" @click="tab = 'file'" :class="{selected: tab === 'file'}"
+                            class="tab-item">{{ trans('entities.attachments_upload') }}</button>
+                    <button type="button" @click="tab = 'link'" :class="{selected: tab === 'link'}"
+                            class="tab-item">{{ trans('entities.attachments_link') }}</button>
+                </div>
+                <div v-show="tab === 'list'">
+                    <draggable style="width: 100%;" :options="{handle: '.handle'}" @change="fileSortUpdate" :list="files" element="div">
+                        <div v-for="(file, index) in files" :key="file.id" class="card drag-card">
+                            <div class="handle">@icon('grip')</div>
+                            <div class="py-s">
+                                <a :href="getFileUrl(file)" target="_blank" v-text="file.name"></a>
+                                <div v-if="file.deleting">
+                                    <span class="text-neg small">{{ trans('entities.attachments_delete_confirm') }}</span>
+                                    <br>
+                                    <button type="button" class="text-primary small" @click="file.deleting = false;">{{ trans('common.cancel') }}</button>
+                                </div>
+                            </div>
+                            <button type="button" @click="startEdit(file)" class="drag-card-action text-center text-primary">@icon('edit')</button>
+                            <button type="button" @click="deleteFile(file)" class="drag-card-action text-center text-neg">@icon('close')</button>
+                        </div>
+                    </draggable>
+                    <p class="small text-muted" v-if="files.length === 0">
+                        {{ trans('entities.attachments_no_files') }}
+                    </p>
+                </div>
+                <div v-show="tab === 'file'">
+                    <dropzone placeholder="{{ trans('entities.attachments_dropzone') }}" :upload-url="getUploadUrl()" :uploaded-to="pageId" @success="uploadSuccess"></dropzone>
+                </div>
+                <div v-show="tab === 'link'" @keypress.enter.prevent="attachNewLink(file)">
+                    <p class="text-muted small">{{ trans('entities.attachments_explain_link') }}</p>
+                    <div class="form-group">
+                        <label for="attachment-via-link">{{ trans('entities.attachments_link_name') }}</label>
+                        <input type="text" placeholder="{{ trans('entities.attachments_link_name') }}" v-model="file.name">
+                        <p class="small text-neg" v-for="error in errors.link.name" v-text="error"></p>
+                    </div>
+                    <div class="form-group">
+                        <label for="attachment-via-link">{{ trans('entities.attachments_link_url') }}</label>
+                        <input type="text"  placeholder="{{ trans('entities.attachments_link_url_hint') }}" v-model="file.link">
+                        <p class="small text-neg" v-for="error in errors.link.link" v-text="error"></p>
+                    </div>
+                    <button @click.prevent="attachNewLink(file)" class="button">{{ trans('entities.attach') }}</button>
+
+                </div>
+            </div>
+
+        </div>
+
+        <div id="file-edit" v-if="fileToEdit" @keypress.enter.prevent="updateFile(fileToEdit)">
+            <h5>{{ trans('entities.attachments_edit_file') }}</h5>
+
+            <div class="form-group">
+                <label for="attachment-name-edit">{{ trans('entities.attachments_edit_file_name') }}</label>
+                <input type="text" id="attachment-name-edit" placeholder="{{ trans('entities.attachments_edit_file_name') }}" v-model="fileToEdit.name">
+                <p class="small text-neg" v-for="error in errors.edit.name" v-text="error"></p>
+            </div>
+
+            <div class="tab-container">
+                <div class="nav-tabs">
+                    <button type="button" @click="editTab = 'file'" :class="{selected: editTab === 'file'}" class="tab-item">{{ trans('entities.attachments_upload') }}</button>
+                    <button type="button" @click="editTab = 'link'" :class="{selected: editTab === 'link'}" class="tab-item">{{ trans('entities.attachments_set_link') }}</button>
+                </div>
+                <div v-if="editTab === 'file'">
+                    <dropzone :upload-url="getUploadUrl(fileToEdit)" :uploaded-to="pageId" placeholder="{{ trans('entities.attachments_edit_drop_upload') }}" @success="uploadSuccessUpdate"></dropzone>
+                    <br>
+                </div>
+                <div v-if="editTab === 'link'">
+                    <div class="form-group">
+                        <label for="attachment-link-edit">{{ trans('entities.attachments_link_url') }}</label>
+                        <input type="text" id="attachment-link-edit" placeholder="{{ trans('entities.attachment_link') }}" v-model="fileToEdit.link">
+                        <p class="small text-neg" v-for="error in errors.edit.link" v-text="error"></p>
+                    </div>
+                </div>
+            </div>
+
+            <button type="button" class="button outline" @click="cancelEdit">{{ trans('common.back') }}</button>
+            <button @click.enter.prevent="updateFile(fileToEdit)" class="button">{{ trans('common.save') }}</button>
+        </div>
+
+    </div>
+</div>
\ No newline at end of file
index f197421725874c893ac27a0c4dd95352346a06b1..0f2af0476e17143b5f8a48df42fccd75d0892f2a 100644 (file)
@@ -29,9 +29,9 @@
                 </div>
 
                 <div class="form-group" collapsible>
-                    <div class="collapse-title text-primary" collapsible-trigger>
+                    <button type="button" class="collapse-title text-primary" collapsible-trigger aria-expanded="false">
                         <label for="entity_selection">{{ trans('entities.pages_copy_desination') }}</label>
-                    </div>
+                    </button>
                     <div class="collapse-content" collapsible-content>
                         @include('components.entity-selector', ['name' => 'entity_selection', 'selectorSize' => 'large', 'entityTypes' => 'book,chapter', 'entityPermission' => 'page-create'])
                     </div>
@@ -39,7 +39,7 @@
 
                 <div class="form-group text-right">
                     <a href="{{ $page->getUrl() }}" class="button outline">{{ trans('common.cancel') }}</a>
-                    <button type="submit" class="button primary">{{ trans('entities.pages_copy') }}</button>
+                    <button type="submit" class="button">{{ trans('entities.pages_copy') }}</button>
                 </div>
             </form>
 
index a72157a83c45ef6a3dab3d7ab467dc786df2fe10..2ec046fa03bf1bf39b4a89ff45bfefa8ebf58be6 100644 (file)
@@ -34,7 +34,7 @@
                         <input type="hidden" name="_method" value="DELETE">
                         <div class="form-group text-right">
                             <a href="{{ $page->getUrl() }}" class="button outline">{{ trans('common.cancel') }}</a>
-                            <button type="submit" class="button primary">{{ trans('common.confirm') }}</button>
+                            <button type="submit" class="button">{{ trans('common.confirm') }}</button>
                         </div>
                     </form>
                 </div>
index eb2fab94cb3076602b909499ee5a29656c7fe257..c2bbdb53711629d86bc7a3272f8fcab12ff6ba20 100644 (file)
@@ -2,7 +2,7 @@
 
 @section('body')
     <div class="container small pt-xl">
-        <div class="card content-wrap">
+        <main class="card content-wrap">
             <h1 class="list-heading">{{ $title }}</h1>
 
             <div class="book-contents">
@@ -12,6 +12,6 @@
             <div class="text-center">
                 {!! $pages->links() !!}
             </div>
-        </div>
+        </main>
     </div>
 @stop
\ No newline at end of file
index c12bd6b4d2b78e769c1b000b1c9df8d94b0e988f..cfb66fdd0e34ad87827be468764670b230bc2a3b 100644 (file)
@@ -1,7 +1,7 @@
 @extends('base')
 
 @section('head')
-    <script src="{{ baseUrl('/libs/tinymce/tinymce.min.js?ver=4.9.4') }}"></script>
+    <script src="{{ url('/libs/tinymce/tinymce.min.js?ver=4.9.4') }}"></script>
 @stop
 
 @section('body-class', 'flexbox')
 
     <div class="flex-fill flex">
         <form action="{{ $page->getUrl() }}" autocomplete="off" data-page-id="{{ $page->id }}" method="POST" class="flex flex-fill">
+            {{ csrf_field() }}
+
             @if(!isset($isDraft))
                 <input type="hidden" name="_method" value="PUT">
             @endif
             @include('pages.form', ['model' => $page])
-            @include('pages.form-toolbox')
+            @include('pages.editor-toolbox')
         </form>
     </div>
     
diff --git a/resources/views/pages/editor-toolbox.blade.php b/resources/views/pages/editor-toolbox.blade.php
new file mode 100644 (file)
index 0000000..6ea6518
--- /dev/null
@@ -0,0 +1,32 @@
+<div editor-toolbox class="floating-toolbox">
+
+    <div class="tabs primary-background-light">
+        <button type="button" toolbox-toggle aria-expanded="false">@icon('caret-left-circle')</button>
+        <button type="button" toolbox-tab-button="tags" title="{{ trans('entities.page_tags') }}" class="active">@icon('tag')</button>
+        @if(userCan('attachment-create-all'))
+            <button type="button" toolbox-tab-button="files" title="{{ trans('entities.attachments') }}">@icon('attach')</button>
+        @endif
+        <button type="button" toolbox-tab-button="templates" title="{{ trans('entities.templates') }}">@icon('template')</button>
+    </div>
+
+    <div toolbox-tab-content="tags">
+        <h4>{{ trans('entities.page_tags') }}</h4>
+        <div class="px-l">
+            @include('components.tag-manager', ['entity' => $page, 'entityType' => 'page'])
+        </div>
+    </div>
+
+    @if(userCan('attachment-create-all'))
+        @include('pages.attachment-manager', ['page' => $page])
+    @endif
+
+    <div toolbox-tab-content="templates">
+        <h4>{{ trans('entities.templates') }}</h4>
+
+        <div class="px-l">
+            @include('pages.template-manager', ['page' => $page, 'templates' => $templates])
+        </div>
+
+    </div>
+
+</div>
index e40643c256ff5346124a36dc717c5dada02fadde..4746a56f37842a5f54dfe13cc1c4a1a4d6586b96 100644 (file)
@@ -1,5 +1,5 @@
 <!doctype html>
-<html lang="en">
+<html lang="{{ config('app.lang') }}">
 <head>
     <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
     <title>{{ $page->name }}</title>
diff --git a/resources/views/pages/form-toolbox.blade.php b/resources/views/pages/form-toolbox.blade.php
deleted file mode 100644 (file)
index e515c0b..0000000
+++ /dev/null
@@ -1,110 +0,0 @@
-
-<div editor-toolbox class="floating-toolbox">
-
-    <div class="tabs primary-background-light">
-        <span toolbox-toggle>@icon('caret-left-circle')</span>
-        <span toolbox-tab-button="tags" title="{{ trans('entities.page_tags') }}" class="active">@icon('tag')</span>
-        @if(userCan('attachment-create-all'))
-            <span toolbox-tab-button="files" title="{{ trans('entities.attachments') }}">@icon('attach')</span>
-        @endif
-    </div>
-
-    <div toolbox-tab-content="tags">
-        <h4>{{ trans('entities.page_tags') }}</h4>
-        <div class="px-l">
-            @include('components.tag-manager', ['entity' => $page, 'entityType' => 'page'])
-        </div>
-    </div>
-
-    @if(userCan('attachment-create-all'))
-        <div toolbox-tab-content="files" id="attachment-manager" page-id="{{ $page->id ?? 0 }}">
-            <h4>{{ trans('entities.attachments') }}</h4>
-            <div class="px-l files">
-
-                <div id="file-list" v-show="!fileToEdit">
-                    <p class="text-muted small">{{ trans('entities.attachments_explain') }} <span class="text-warn">{{ trans('entities.attachments_explain_instant_save') }}</span></p>
-
-                    <div class="tab-container">
-                        <div class="nav-tabs">
-                            <div @click="tab = 'list'" :class="{selected: tab === 'list'}" class="tab-item">{{ trans('entities.attachments_items') }}</div>
-                            <div @click="tab = 'file'" :class="{selected: tab === 'file'}" class="tab-item">{{ trans('entities.attachments_upload') }}</div>
-                            <div @click="tab = 'link'" :class="{selected: tab === 'link'}" class="tab-item">{{ trans('entities.attachments_link') }}</div>
-                        </div>
-                        <div v-show="tab === 'list'">
-                            <draggable style="width: 100%;" :options="{handle: '.handle'}" @change="fileSortUpdate" :list="files" element="div">
-                                <div v-for="(file, index) in files" :key="file.id" class="card drag-card">
-                                    <div class="handle">@icon('grip')</div>
-                                    <div class="py-s">
-                                        <a :href="getFileUrl(file)" target="_blank" v-text="file.name"></a>
-                                        <div v-if="file.deleting">
-                                            <span class="text-neg small">{{ trans('entities.attachments_delete_confirm') }}</span>
-                                            <br>
-                                            <span class="text-primary small" @click="file.deleting = false;">{{ trans('common.cancel') }}</span>
-                                        </div>
-                                    </div>
-                                    <div @click="startEdit(file)" class="drag-card-action text-center text-primary">@icon('edit')</div>
-                                    <div @click="deleteFile(file)" class="drag-card-action text-center text-neg">@icon('close')</div>
-                                </div>
-                            </draggable>
-                            <p class="small text-muted" v-if="files.length === 0">
-                                {{ trans('entities.attachments_no_files') }}
-                            </p>
-                        </div>
-                        <div v-show="tab === 'file'">
-                            <dropzone placeholder="{{ trans('entities.attachments_dropzone') }}" :upload-url="getUploadUrl()" :uploaded-to="pageId" @success="uploadSuccess"></dropzone>
-                        </div>
-                        <div v-show="tab === 'link'" @keypress.enter.prevent="attachNewLink(file)">
-                            <p class="text-muted small">{{ trans('entities.attachments_explain_link') }}</p>
-                            <div class="form-group">
-                                <label for="attachment-via-link">{{ trans('entities.attachments_link_name') }}</label>
-                                <input type="text" placeholder="{{ trans('entities.attachments_link_name') }}" v-model="file.name">
-                                <p class="small text-neg" v-for="error in errors.link.name" v-text="error"></p>
-                            </div>
-                            <div class="form-group">
-                                <label for="attachment-via-link">{{ trans('entities.attachments_link_url') }}</label>
-                                <input type="text"  placeholder="{{ trans('entities.attachments_link_url_hint') }}" v-model="file.link">
-                                <p class="small text-neg" v-for="error in errors.link.link" v-text="error"></p>
-                            </div>
-                            <button @click.prevent="attachNewLink(file)" class="button primary">{{ trans('entities.attach') }}</button>
-
-                        </div>
-                    </div>
-
-                </div>
-
-                <div id="file-edit" v-if="fileToEdit" @keypress.enter.prevent="updateFile(fileToEdit)">
-                    <h5>{{ trans('entities.attachments_edit_file') }}</h5>
-
-                    <div class="form-group">
-                        <label for="attachment-name-edit">{{ trans('entities.attachments_edit_file_name') }}</label>
-                        <input type="text" id="attachment-name-edit" placeholder="{{ trans('entities.attachments_edit_file_name') }}" v-model="fileToEdit.name">
-                        <p class="small text-neg" v-for="error in errors.edit.name" v-text="error"></p>
-                    </div>
-
-                    <div class="tab-container">
-                        <div class="nav-tabs">
-                            <div @click="editTab = 'file'" :class="{selected: editTab === 'file'}" class="tab-item">{{ trans('entities.attachments_upload') }}</div>
-                            <div @click="editTab = 'link'" :class="{selected: editTab === 'link'}" class="tab-item">{{ trans('entities.attachments_set_link') }}</div>
-                        </div>
-                        <div v-if="editTab === 'file'">
-                            <dropzone :upload-url="getUploadUrl(fileToEdit)" :uploaded-to="pageId" placeholder="{{ trans('entities.attachments_edit_drop_upload') }}" @success="uploadSuccessUpdate"></dropzone>
-                            <br>
-                        </div>
-                        <div v-if="editTab === 'link'">
-                            <div class="form-group">
-                                <label for="attachment-link-edit">{{ trans('entities.attachments_link_url') }}</label>
-                                <input type="text" id="attachment-link-edit" placeholder="{{ trans('entities.attachment_link') }}" v-model="fileToEdit.link">
-                                <p class="small text-neg" v-for="error in errors.edit.link" v-text="error"></p>
-                            </div>
-                        </div>
-                    </div>
-
-                    <button type="button" class="button outline" @click="cancelEdit">{{ trans('common.back') }}</button>
-                    <button @click.enter.prevent="updateFile(fileToEdit)" class="button primary">{{ trans('common.save') }}</button>
-                </div>
-
-            </div>
-        </div>
-    @endif
-
-</div>
index 4a473e53636016c3bca1ae671d18d86ffe0131de..ffc286c2cadadc32f3f9834dfd72ffb6be54ee3c 100644 (file)
@@ -1,4 +1,3 @@
-
 <div class="page-editor flex-fill flex" id="page-editor"
      drafts-enabled="{{ $draftsEnabled ? 'true' : 'false' }}"
      drawio-enabled="{{ config('services.drawio') ? 'true' : 'false' }}"
@@ -8,7 +7,14 @@
      page-new-draft="{{ $model->draft ?? 0 }}"
      page-update-draft="{{ $model->isDraft ?? 0 }}">
 
-    {{ csrf_field() }}
+    @exposeTranslations([
+        'entities.pages_editing_draft',
+        'entities.pages_editing_page',
+        'errors.page_draft_autosave_fail',
+        'entities.pages_editing_page',
+        'entities.pages_draft_discarded',
+        'entities.pages_edit_set_changelog',
+    ])
 
     {{--Header Bar--}}
     <div class="primary-background-light toolbar page-edit-toolbar">
 
             <div class="action-buttons text-left px-m py-xs">
                 <a href="{{ back()->getTargetUrl() }}" class="text-button text-primary">@icon('back')<span class="hide-under-l">{{ trans('common.back') }}</span></a>
-                <a onclick="$('body>header').slideToggle();" class="text-button text-primary">@icon('swap-vertical')<span class="hide-under-l">{{ trans('entities.pages_edit_toggle_header') }}</span></a>
             </div>
 
             <div class="text-center px-m py-xs">
                 <div v-show="draftsEnabled" dropdown dropdown-move-menu class="dropdown-container draft-display text">
-                    <a dropdown-toggle  class="text-primary text-button"><span class="faded-text" v-text="draftText"></span>&nbsp; @icon('more')</a>
+                    <button type="button" dropdown-toggle aria-haspopup="true" aria-expanded="false" title="{{ trans('entities.pages_edit_draft_options') }}" class="text-primary text-button"><span class="faded-text" v-text="draftText"></span>&nbsp; @icon('more')</button>
                     @icon('check-circle', ['class' => 'text-pos draft-notification svg-icon', ':class' => '{visible: draftUpdated}'])
-                    <ul class="dropdown-menu">
+                    <ul class="dropdown-menu" role="menu">
                         <li>
-                            <a @click="saveDraft()" class="text-pos">@icon('save'){{ trans('entities.pages_edit_save_draft') }}</a>
+                            <button type="button" @click="saveDraft()" class="text-pos">@icon('save'){{ trans('entities.pages_edit_save_draft') }}</button>
                         </li>
                         <li v-if="isNewDraft">
                             <a href="{{ $model->getUrl('/delete') }}" class="text-neg">@icon('delete'){{ trans('entities.pages_edit_delete_draft') }}</a>
                         </li>
                         <li v-if="isUpdateDraft">
-                            <a type="button" @click="discardDraft" class="text-neg">@icon('cancel'){{ trans('entities.pages_edit_discard_draft') }}</a>
+                            <button type="button" @click="discardDraft" class="text-neg">@icon('cancel'){{ trans('entities.pages_edit_discard_draft') }}</button>
                         </li>
                     </ul>
                 </div>
@@ -39,7 +44,7 @@
 
             <div class="action-buttons px-m py-xs" v-cloak>
                 <div dropdown dropdown-move-menu class="dropdown-container">
-                    <a dropdown-toggle class="text-primary text-button">@icon('edit') <span v-text="changeSummaryShort"></span></a>
+                    <button type="button" dropdown-toggle aria-haspopup="true" aria-expanded="false" class="text-primary text-button">@icon('edit') <span v-text="changeSummaryShort"></span></button>
                     <ul class="wide dropdown-menu">
                         <li class="px-l py-m">
                             <p class="text-muted pb-s">{{ trans('entities.pages_edit_enter_changelog_desc') }}</p>
 
         {{--WYSIWYG Editor--}}
         @if(setting('app-editor') === 'wysiwyg')
-            <div wysiwyg-editor class="flex-fill flex">
-                <textarea id="html-editor"  name="html" rows="5" v-pre
-                    @if($errors->has('html')) class="text-neg" @endif>@if(isset($model) || old('html')){{htmlspecialchars( old('html') ? old('html') : $model->html)}}@endif</textarea>
-            </div>
-
-            @if($errors->has('html'))
-                <div class="text-neg text-small">{{ $errors->first('html') }}</div>
-            @endif
+            @include('pages.wysiwyg-editor', ['model' => $model])
         @endif
 
         {{--Markdown Editor--}}
         @if(setting('app-editor') === 'markdown')
-            <div v-pre id="markdown-editor" markdown-editor class="flex-fill flex code-fill">
-
-                <div class="markdown-editor-wrap active">
-                    <div class="editor-toolbar">
-                        <span class="float left editor-toolbar-label">{{ trans('entities.pages_md_editor') }}</span>
-                        <div class="float right buttons">
-                            @if(config('services.drawio'))
-                                <button class="text-button" type="button" data-action="insertDrawing">@icon('drawing'){{ trans('entities.pages_md_insert_drawing') }}</button>
-                                &nbsp;|&nbsp
-                            @endif
-                            <button class="text-button" type="button" data-action="insertImage">@icon('image'){{ trans('entities.pages_md_insert_image') }}</button>
-                            &nbsp;|&nbsp;
-                            <button class="text-button" type="button" data-action="insertLink">@icon('link'){{ trans('entities.pages_md_insert_link') }}</button>
-                        </div>
-                    </div>
-
-                    <div markdown-input class="flex flex-fill">
-                        <textarea  id="markdown-editor-input"  name="markdown" rows="5"
-                            @if($errors->has('markdown')) class="text-neg" @endif>@if(isset($model) || old('markdown')){{htmlspecialchars( old('markdown') ? old('markdown') : ($model->markdown === '' ? $model->html : $model->markdown))}}@endif</textarea>
-                    </div>
-
-                </div>
-
-                <div class="markdown-editor-wrap">
-                    <div class="editor-toolbar">
-                        <div class="editor-toolbar-label">{{ trans('entities.pages_md_preview') }}</div>
-                    </div>
-                    <div class="markdown-display page-content">
-                    </div>
-                </div>
-                <input type="hidden" name="html"/>
-
-            </div>
-
-
-
-            @if($errors->has('markdown'))
-                <div class="text-neg text-small">{{ $errors->first('markdown') }}</div>
-            @endif
+            @include('pages.markdown-editor', ['model' => $model])
         @endif
 
     </div>
index 4650f3a1c1377808f6e09357a5c2523b37f01b13..55db85144ae1f9ba147b4da17268c78cfc2589ec 100644 (file)
@@ -15,7 +15,7 @@
             ]])
         </div>
 
-        <div class="card content-wrap">
+        <main class="card content-wrap">
             <h1 class="list-heading">{{ trans('entities.pages_new') }}</h1>
             <form action="{{  $parent->getUrl('/create-guest-page') }}" method="POST">
                 {!! csrf_field() !!}
 
                 <div class="form-group text-right">
                     <a href="{{ $parent->getUrl() }}" class="button outline">{{ trans('common.cancel') }}</a>
-                    <button type="submit" class="button primary">{{ trans('common.continue') }}</button>
+                    <button type="submit" class="button">{{ trans('common.continue') }}</button>
                 </div>
 
             </form>
-        </div>
+        </main>
     </div>
 
 @stop
\ No newline at end of file
diff --git a/resources/views/pages/markdown-editor.blade.php b/resources/views/pages/markdown-editor.blade.php
new file mode 100644 (file)
index 0000000..5264411
--- /dev/null
@@ -0,0 +1,41 @@
+<div v-pre id="markdown-editor" markdown-editor class="flex-fill flex code-fill">
+    @exposeTranslations([
+        'errors.image_upload_error',
+    ])
+
+    <div class="markdown-editor-wrap active">
+        <div class="editor-toolbar">
+            <span class="float left editor-toolbar-label">{{ trans('entities.pages_md_editor') }}</span>
+            <div class="float right buttons">
+                @if(config('services.drawio'))
+                    <button class="text-button" type="button" data-action="insertDrawing">@icon('drawing'){{ trans('entities.pages_md_insert_drawing') }}</button>
+                    &nbsp;|&nbsp
+                @endif
+                <button class="text-button" type="button" data-action="insertImage">@icon('image'){{ trans('entities.pages_md_insert_image') }}</button>
+                &nbsp;|&nbsp;
+                <button class="text-button" type="button" data-action="insertLink">@icon('link'){{ trans('entities.pages_md_insert_link') }}</button>
+            </div>
+        </div>
+
+        <div markdown-input class="flex flex-fill">
+                        <textarea  id="markdown-editor-input"  name="markdown" rows="5"
+                                   @if($errors->has('markdown')) class="text-neg" @endif>@if(isset($model) || old('markdown')){{ old('markdown') ? old('markdown') : ($model->markdown === '' ? $model->html : $model->markdown) }}@endif</textarea>
+        </div>
+
+    </div>
+
+    <div class="markdown-editor-wrap">
+        <div class="editor-toolbar">
+            <div class="editor-toolbar-label">{{ trans('entities.pages_md_preview') }}</div>
+        </div>
+        <iframe srcdoc="" class="markdown-display" sandbox="allow-same-origin"></iframe>
+    </div>
+    <input type="hidden" name="html"/>
+
+</div>
+
+
+
+@if($errors->has('markdown'))
+    <div class="text-neg text-small">{{ $errors->first('markdown') }}</div>
+@endif
\ No newline at end of file
index 83421be934b4e169076285d8dc79721a7af23e51..3bf1db5e46671f98ead3e3cd8c879f0bb92887ec 100644 (file)
@@ -16,7 +16,7 @@
             ]])
         </div>
 
-        <div class="card content-wrap">
+        <main class="card content-wrap">
             <h1 class="list-heading">{{ trans('entities.pages_move') }}</h1>
 
             <form action="{{ $page->getUrl('/move') }}" method="POST">
 
                 <div class="form-group text-right">
                     <a href="{{ $page->getUrl() }}" class="button outline">{{ trans('common.cancel') }}</a>
-                    <button type="submit" class="button primary">{{ trans('entities.pages_move') }}</button>
+                    <button type="submit" class="button">{{ trans('entities.pages_move') }}</button>
                 </div>
             </form>
 
-        </div>
+        </main>
     </div>
 
 @stop
index 260f0e49f961eae313aa276653d982fd12ba82ad..de28137dbe02952d52f5cb563d52d95e992074f2 100644 (file)
             ]])
         </div>
 
-        <div class="card content-wrap">
+        <main class="card content-wrap">
             <h1 class="list-heading">{{ trans('entities.pages_permissions') }}</h1>
             @include('form.entity-permissions', ['model' => $page])
-        </div>
+        </main>
     </div>
 
 @stop
diff --git a/resources/views/pages/pointer.blade.php b/resources/views/pages/pointer.blade.php
new file mode 100644 (file)
index 0000000..b4b28ea
--- /dev/null
@@ -0,0 +1,13 @@
+<div class="pointer-container" id="pointer">
+    <div class="pointer anim {{ userCan('page-update', $page) ? 'is-page-editable' : ''}}" >
+        <span class="icon mr-xxs">@icon('link') @icon('include', ['style' => 'display:none;'])</span>
+        <div class="input-group inline block">
+            <input readonly="readonly" type="text" id="pointer-url" placeholder="url">
+            <button class="button outline icon" data-clipboard-target="#pointer-url" type="button" title="{{ trans('entities.pages_copy_link') }}">@icon('copy')</button>
+        </div>
+        @if(userCan('page-update', $page))
+            <a href="{{ $page->getUrl('/edit') }}" id="pointer-edit" data-edit-href="{{ $page->getUrl('/edit') }}"
+               class="button primary outline icon heading-edit-icon ml-s px-s" title="{{ trans('entities.pages_edit_content_link')}}">@icon('edit')</a>
+        @endif
+    </div>
+</div>
\ No newline at end of file
index 3ce5b349f1afc8c79645e3bcd16e500bc95e1ca3..0557b6b1cd79be45f57d51074bcbf32ab9753b86 100644 (file)
@@ -11,7 +11,7 @@
 
 @section('body')
 
-    <div class="mb-m">
+    <div class="mb-m print-hidden">
         @include('partials.breadcrumbs', ['crumbs' => [
             $page->$book,
             $page->chapter,
         ]])
     </div>
 
-    <div class="card content-wrap">
+    <main class="card content-wrap">
         <div class="page-content page-revision">
             @include('pages.page-display')
         </div>
-    </div>
+    </main>
 
 @stop
\ No newline at end of file
index f3fb048bc85ded30fe5fbd05ce2e3df452f8495e..feb3180775adaff018fe9dd739e21bc64354998c 100644 (file)
@@ -15,7 +15,7 @@
             ]])
         </div>
 
-        <div class="card content-wrap">
+        <main class="card content-wrap">
             <h1 class="list-heading">{{ trans('entities.pages_revisions') }}</h1>
             @if(count($page->revisions) > 0)
 
                                 @else
                                     <a href="{{ $revision->getUrl() }}" target="_blank">{{ trans('entities.pages_revisions_preview') }}</a>
                                     <span class="text-muted">&nbsp;|&nbsp;</span>
-                                    <a href="{{ $revision->getUrl('restore') }}"></a>
                                     <div dropdown class="dropdown-container">
-                                        <a dropdown-toggle>{{ trans('entities.pages_revisions_restore') }}</a>
-                                        <ul class="dropdown-menu">
+                                        <a dropdown-toggle href="#" aria-haspopup="true" aria-expanded="false">{{ trans('entities.pages_revisions_restore') }}</a>
+                                        <ul class="dropdown-menu" role="menu">
                                             <li class="px-m py-s"><small class="text-muted">{{trans('entities.revision_restore_confirm')}}</small></li>
                                             <li>
                                                 <form action="{{ $revision->getUrl('/restore') }}" method="POST">
@@ -66,8 +65,8 @@
                                     </div>
                                     <span class="text-muted">&nbsp;|&nbsp;</span>
                                     <div dropdown class="dropdown-container">
-                                        <a dropdown-toggle>{{ trans('common.delete') }}</a>
-                                        <ul class="dropdown-menu">
+                                        <a dropdown-toggle href="#" aria-haspopup="true" aria-expanded="false">{{ trans('common.delete') }}</a>
+                                        <ul class="dropdown-menu" role="menu">
                                             <li class="px-m py-s"><small class="text-muted">{{trans('entities.revision_delete_confirm')}}</small></li>
                                             <li>
                                                 <form action="{{ $revision->getUrl('/delete/') }}" method="POST">
@@ -87,7 +86,7 @@
             @else
                 <p>{{ trans('entities.pages_revisions_none') }}</p>
             @endif
-        </div>
+        </main>
 
     </div>
 
index ff4db2eec4a84df6b787c7f129e1f3cfe262b61e..51ab5bbbe53531e315e6ae314694123adcfc7281 100644 (file)
@@ -2,7 +2,7 @@
 
 @section('body')
 
-    <div class="mb-m">
+    <div class="mb-m print-hidden">
         @include('partials.breadcrumbs', ['crumbs' => [
             $page->book,
             $page->hasChapter() ? $page->chapter : null,
         ]])
     </div>
 
-    <div class="content-wrap card">
-        <div class="page-content flex" page-display="{{ $page->id }}">
-
-            <div class="pointer-container" id="pointer">
-                <div class="pointer anim {{ userCan('page-update', $page) ? 'is-page-editable' : ''}}" >
-                    <span class="icon text-primary">@icon('link') @icon('include', ['style' => 'display:none;'])</span>
-                    <span class="input-group">
-                    <input readonly="readonly" type="text" id="pointer-url" placeholder="url">
-                    <button class="button icon" data-clipboard-target="#pointer-url" type="button" title="{{ trans('entities.pages_copy_link') }}">@icon('copy')</button>
-                </span>
-                    @if(userCan('page-update', $page))
-                        <a href="{{ $page->getUrl('/edit') }}" id="pointer-edit" data-edit-href="{{ $page->getUrl('/edit') }}"
-                           class="button icon heading-edit-icon" title="{{ trans('entities.pages_edit_content_link')}}">@icon('edit')</a>
-                    @endif
-                </div>
-            </div>
-
+    <main class="content-wrap card">
+        <div class="page-content" page-display="{{ $page->id }}">
+            @include('pages.pointer', ['page' => $page])
             @include('pages.page-display')
         </div>
-    </div>
+    </main>
 
     @if ($commentsEnabled)
-        <div class="container small p-none comments-container mb-l">
+        <div class="container small p-none comments-container mb-l print-hidden">
             @include('comments.comments', ['page' => $page])
             <div class="clearfix"></div>
         </div>
     @endif
 
     @if (isset($pageNav) && count($pageNav))
-        <div id="page-navigation" class="mb-xl">
+        <nav id="page-navigation" class="mb-xl" aria-label="{{ trans('entities.pages_navigation') }}">
             <h5>{{ trans('entities.pages_navigation') }}</h5>
             <div class="body">
                 <div class="sidebar-page-nav menu">
                     @foreach($pageNav as $navItem)
                         <li class="page-nav-item h{{ $navItem['level'] }}">
-                            <a href="{{ $navItem['link'] }}">{{ $navItem['text'] }}</a>
+                            <a href="{{ $navItem['link'] }}" class="limit-text block">{{ $navItem['text'] }}</a>
                             <div class="primary-background sidebar-page-nav-bullet"></div>
                         </li>
                     @endforeach
                 </div>
             </div>
-        </div>
+        </nav>
     @endif
 
     @include('partials.book-tree', ['book' => $book, 'sidebarTree' => $sidebarTree])
                     @endif
                 </div>
             @endif
+
+            @if($page->template)
+                <div>
+                    @icon('template'){{ trans('entities.pages_is_template') }}
+                </div>
+            @endif
         </div>
     </div>
 
             <hr class="primary-background"/>
 
             {{--Export--}}
-            <div dropdown class="dropdown-container block">
-                <div dropdown-toggle class="icon-list-item">
-                    <span>@icon('export')</span>
-                    <span>{{ trans('entities.export') }}</span>
-                </div>
-                <ul class="dropdown-menu wide">
-                    <li><a href="{{ $page->getUrl('/export/html') }}" target="_blank">{{ trans('entities.export_html') }} <span class="text-muted float right">.html</span></a></li>
-                    <li><a href="{{ $page->getUrl('/export/pdf') }}" target="_blank">{{ trans('entities.export_pdf') }} <span class="text-muted float right">.pdf</span></a></li>
-                    <li><a href="{{ $page->getUrl('/export/plaintext') }}" target="_blank">{{ trans('entities.export_text') }} <span class="text-muted float right">.txt</span></a></li>
-                </ul>
-            </div>
+            @include('partials.entity-export-menu', ['entity' => $page])
         </div>
 
     </div>
diff --git a/resources/views/pages/template-manager-list.blade.php b/resources/views/pages/template-manager-list.blade.php
new file mode 100644 (file)
index 0000000..f2f70c1
--- /dev/null
@@ -0,0 +1,24 @@
+{{ $templates->links() }}
+
+@foreach($templates as $template)
+    <div class="card template-item border-card p-m mb-m" tabindex="0"
+         aria-label="{{ trans('entities.templates_replace_content') }} - {{ $template->name }}"
+         draggable="true" template-id="{{ $template->id }}">
+        <div class="template-item-content" title="{{ trans('entities.templates_replace_content') }}">
+            <div>{{ $template->name }}</div>
+            <div class="text-muted">{{ trans('entities.meta_updated', ['timeLength' => $template->updated_at->diffForHumans()]) }}</div>
+        </div>
+        <div class="template-item-actions">
+            <button type="button"
+                    title="{{ trans('entities.templates_prepend_content') }}"
+                    aria-label="{{ trans('entities.templates_prepend_content') }} - {{ $template->name }}"
+                    template-action="prepend">@icon('chevron-up')</button>
+            <button type="button"
+                    title="{{ trans('entities.templates_append_content') }}"
+                    aria-label="{{ trans('entities.templates_append_content') }} -- {{ $template->name }}"
+                    template-action="append">@icon('chevron-down')</button>
+        </div>
+    </div>
+@endforeach
+
+{{ $templates->links() }}
\ No newline at end of file
diff --git a/resources/views/pages/template-manager.blade.php b/resources/views/pages/template-manager.blade.php
new file mode 100644 (file)
index 0000000..fbdb70a
--- /dev/null
@@ -0,0 +1,25 @@
+<div template-manager>
+    @if(userCan('templates-manage'))
+        <p class="text-muted small mb-none">
+            {{ trans('entities.templates_explain_set_as_template') }}
+        </p>
+        @include('components.toggle-switch', [
+               'name' => 'template',
+               'value' => old('template', $page->template ? 'true' : 'false') === 'true',
+               'label' => trans('entities.templates_set_as_template')
+        ])
+        <hr>
+    @endif
+
+    @if(count($templates) > 0)
+        <div class="search-box flexible mb-m">
+            <input type="text" name="template-search" placeholder="{{ trans('common.search') }}">
+            <button type="button">@icon('search')</button>
+            <button class="search-box-cancel text-neg hidden" type="button">@icon('close')</button>
+        </div>
+    @endif
+
+    <div template-manager-list>
+        @include('pages.template-manager-list', ['templates' => $templates])
+    </div>
+</div>
\ No newline at end of file
diff --git a/resources/views/pages/wysiwyg-editor.blade.php b/resources/views/pages/wysiwyg-editor.blade.php
new file mode 100644 (file)
index 0000000..1a67ee7
--- /dev/null
@@ -0,0 +1,13 @@
+<div wysiwyg-editor class="flex-fill flex">
+
+    @exposeTranslations([
+        'errors.image_upload_error',
+    ])
+
+    <textarea id="html-editor"  name="html" rows="5" v-pre
+          @if($errors->has('html')) class="text-neg" @endif>@if(isset($model) || old('html')){{ old('html') ? old('html') : $model->html }}@endif</textarea>
+</div>
+
+@if($errors->has('html'))
+    <div class="text-neg text-small">{{ $errors->first('html') }}</div>
+@endif
\ No newline at end of file
index 73064dceb46b72b6d9f9f38b23c9aeb5e75d55af..c288e63674ff38b456a4b13ed530746510f11e29 100644 (file)
@@ -1,4 +1,4 @@
-<div id="book-tree" class="book-tree mb-xl" v-pre>
+<nav id="book-tree" class="book-tree mb-xl" v-pre aria-label="{{ trans('entities.books_navigation') }}">
     <h5>{{ trans('entities.books_navigation') }}</h5>
 
     <ul class="sidebar-page-list mt-xs menu entity-list">
 
                 @if($bookChild->isA('chapter') && count($bookChild->pages) > 0)
                     <div class="entity-list-item no-hover">
-                        <span class="icon text-chapter">
-
-                        </span>
+                        <span role="presentation" class="icon text-chapter"></span>
                         <div class="content">
-                            @include('chapters.child-menu', ['chapter' => $bookChild, 'current' => $current])
+                            @include('chapters.child-menu', [
+                                'chapter' => $bookChild,
+                                'current' => $current,
+                                'isOpen'  => $bookChild->matchesOrContains($current)
+                            ])
                         </div>
                     </div>
 
@@ -27,4 +29,4 @@
             </li>
         @endforeach
     </ul>
-</div>
\ No newline at end of file
+</nav>
\ No newline at end of file
index 3dea3202355b6d5953cc7d48bb51fc71269555d4..e53cb4c53b9cf59200427f67d6134d90ce1c5bbd 100644 (file)
@@ -1,11 +1,12 @@
 <div class="breadcrumb-listing" dropdown breadcrumb-listing="{{ $entity->getType() }}:{{ $entity->id }}">
-    <div class="breadcrumb-listing-toggle" dropdown-toggle>
+    <div class="breadcrumb-listing-toggle" dropdown-toggle
+         aria-haspopup="true" aria-expanded="false" tabindex="0">
         <div class="separator">@icon('chevron-right')</div>
     </div>
-    <div dropdown-menu class="breadcrumb-listing-dropdown card">
+    <div dropdown-menu class="breadcrumb-listing-dropdown card" role="menu">
         <div class="breadcrumb-listing-search">
             @icon('search')
-            <input autocomplete="off" type="text" name="entity-search">
+            <input autocomplete="off" type="text" name="entity-search" placeholder="{{ trans('common.search') }}" aria-label="{{ trans('common.search') }}">
         </div>
         @include('partials.loading-icon')
         <div class="breadcrumb-listing-entity-list px-m"></div>
index df5d1aa049fce72ba44a9e45a1a8ac73b826f8a6..58ccd51257e0338686e746762781d36b318b5d70 100644 (file)
@@ -1,9 +1,9 @@
-<div class="breadcrumbs text-center">
+<nav class="breadcrumbs text-center" aria-label="{{ trans('common.breadcrumb') }}">
     <?php $breadcrumbCount = 0; ?>
 
     {{-- Show top level books item --}}
-    @if (count($crumbs) > 0 && array_first($crumbs) instanceof  \BookStack\Entities\Book)
-        <a href="{{  baseUrl('/books')  }}" class="text-book icon-list-item outline-hover">
+    @if (count($crumbs) > 0 && ($crumbs[0] ?? null) instanceof  \BookStack\Entities\Book)
+        <a href="{{  url('/books')  }}" class="text-book icon-list-item outline-hover">
             <span>@icon('books')</span>
             <span>{{ trans('entities.books') }}</span>
         </a>
@@ -11,8 +11,8 @@
     @endif
 
     {{-- Show top level shelves item --}}
-    @if (count($crumbs) > 0 && array_first($crumbs) instanceof  \BookStack\Entities\Bookshelf)
-        <a href="{{  baseUrl('/shelves')  }}" class="text-bookshelf icon-list-item outline-hover">
+    @if (count($crumbs) > 0 && ($crumbs[0] ?? null) instanceof  \BookStack\Entities\Bookshelf)
+        <a href="{{  url('/shelves')  }}" class="text-bookshelf icon-list-item outline-hover">
             <span>@icon('bookshelf')</span>
             <span>{{ trans('entities.shelves') }}</span>
         </a>
         @endif
 
         @if (is_string($crumb))
-            <a href="{{  baseUrl($key)  }}">
+            <a href="{{  url($key)  }}">
                 {{ $crumb }}
             </a>
         @elseif (is_array($crumb))
-            <a href="{{  baseUrl($key)  }}" class="icon-list-item outline-hover">
+            <a href="{{  url($key)  }}" class="icon-list-item outline-hover">
                 <span>@icon($crumb['icon'])</span>
                 <span>{{ $crumb['text'] }}</span>
             </a>
@@ -51,4 +51,4 @@
         @endif
         <?php $breadcrumbCount++; ?>
     @endforeach
-</div>
\ No newline at end of file
+</nav>
\ No newline at end of file
index 2a293edc5f4b5f22209d13860e038508978eee67..9080790825f74c4014f53368458f7c3fe3705570 100644 (file)
@@ -1,24 +1,6 @@
 <style id="custom-styles" data-color="{{ setting('app-color') }}" data-color-light="{{ setting('app-color-light') }}">
-    .primary-background {
-        background-color: {{ setting('app-color') }} !important;
+    :root {
+        --color-primary: {{ setting('app-color') }};
+        --color-primary-light: {{ setting('app-color-light') }};
     }
-    .primary-background-light {
-        background-color: {{ setting('app-color-light') }};
-    }
-    .button.primary, .button.primary:hover, .button.primary:active, .button.primary:focus {
-        background-color: {{ setting('app-color') }};
-        border-color: {{ setting('app-color') }};
-    }
-    .nav-tabs a.selected, .nav-tabs .tab-item.selected {
-        border-bottom-color: {{ setting('app-color') }};
-    }
-    .text-primary, .text-primary-hover:hover, .text-primary:hover {
-        color: {{ setting('app-color') }} !important;
-        fill: {{ setting('app-color') }} !important;
-    }
-
-    a, a:hover, a:focus, .text-button, .text-button:hover, .text-button:focus {
-        color: {{ setting('app-color') }};
-        fill: {{ setting('app-color') }};
-    }
-</style>
+</style>
\ No newline at end of file
index 99d37c5f874bc390de36fbb595e5c9d750379b67..2e0395253b7680e5b21166228c59e8389118d4ce 100644 (file)
@@ -1,7 +1,8 @@
 <div class="mb-xl">
-    <form v-on:submit.prevent="searchBook" class="search-box flexible">
-        <input v-model="searchTerm" v-on:change="checkSearchForm" type="text" name="term" placeholder="{{ trans('entities.books_search_this') }}">
-        <button type="submit">@icon('search')</button>
-        <button v-if="searching" v-cloak class="search-box-cancel text-neg" v-on:click="clearSearch" type="button">@icon('close')</button>
+    <form v-on:submit.prevent="searchBook" class="search-box flexible" role="search">
+        <input v-model="searchTerm" v-on:change="checkSearchForm" type="text" aria-label="{{ trans('entities.books_search_this') }}" name="term" placeholder="{{ trans('entities.books_search_this') }}">
+        <button type="submit" aria-label="{{ trans('common.search') }}">@icon('search')</button>
+        <button v-if="searching" v-cloak class="search-box-cancel text-neg" v-on:click="clearSearch"
+                type="button" aria-label="{{ trans('common.search_clear') }}">@icon('close')</button>
     </form>
 </div>
\ No newline at end of file
diff --git a/resources/views/partials/entity-export-menu.blade.php b/resources/views/partials/entity-export-menu.blade.php
new file mode 100644 (file)
index 0000000..630d640
--- /dev/null
@@ -0,0 +1,12 @@
+<div dropdown class="dropdown-container" id="export-menu">
+    <div dropdown-toggle class="icon-list-item"
+         aria-haspopup="true" aria-expanded="false" aria-label="{{ trans('entities.export') }}" tabindex="0">
+        <span>@icon('export')</span>
+        <span>{{ trans('entities.export') }}</span>
+    </div>
+    <ul class="wide dropdown-menu" role="menu">
+        <li><a href="{{ $entity->getUrl('/export/html') }}" target="_blank">{{ trans('entities.export_html') }} <span class="text-muted float right">.html</span></a></li>
+        <li><a href="{{ $entity->getUrl('/export/pdf') }}" target="_blank">{{ trans('entities.export_pdf') }} <span class="text-muted float right">.pdf</span></a></li>
+        <li><a href="{{ $entity->getUrl('/export/plaintext') }}" target="_blank">{{ trans('entities.export_text') }} <span class="text-muted float right">.txt</span></a></li>
+    </ul>
+</div>
\ No newline at end of file
index c4942c71f2dd38af6bb2de09290a710bd08b7912..2ec4bee5cc07dbf13db6b0978effd5990551cdc4 100644 (file)
@@ -1,6 +1,6 @@
 <?php $type = $entity->getType(); ?>
 <a href="{{ $entity->getUrl() }}" class="{{$type}} {{$type === 'page' && $entity->draft ? 'draft' : ''}} {{$classes ?? ''}} entity-list-item" data-entity-type="{{$type}}" data-entity-id="{{$entity->id}}">
-    <span class="icon text-{{$type}}">@icon($type)</span>
+    <span role="presentation" class="icon text-{{$type}}">@icon($type)</span>
     <div class="content">
             <h4 class="entity-list-item-name break-text">{{ $entity->name }}</h4>
             {{ $slot ?? '' }}
index ac853a56cc01b79f7a1c7a681a9e0ec59da33158..52687149928b299777276912b6e1870c4d9c90e7 100644 (file)
@@ -1,11 +1,11 @@
-<div notification="success" style="display: none;" data-autohide class="pos" @if(session()->has('success')) data-show @endif>
+<div notification="success" style="display: none;" data-autohide class="pos" role="alert" @if(session()->has('success')) data-show @endif>
     @icon('check-circle') <span>{!! nl2br(htmlentities(session()->get('success'))) !!}</span>
 </div>
 
-<div notification="warning" style="display: none;" class="warning" @if(session()->has('warning')) data-show @endif>
+<div notification="warning" style="display: none;" class="warning" role="alert" @if(session()->has('warning')) data-show @endif>
     @icon('info') <span>{!! nl2br(htmlentities(session()->get('warning'))) !!}</span>
 </div>
 
-<div notification="error" style="display: none;" class="neg" @if(session()->has('error')) data-show @endif>
+<div notification="error" style="display: none;" class="neg" role="alert" @if(session()->has('error')) data-show @endif>
     @icon('danger') <span>{!! nl2br(htmlentities(session()->get('error'))) !!}</span>
 </div>
index 9544bcee1e3d828763d96b0870eb11ce088282a1..09c61d01383fa01f2d5e6f0c1af54b586dff75c7 100644 (file)
@@ -4,7 +4,7 @@
 ?>
 <div class="list-sort-container" list-sort-control>
     <div class="list-sort-label">{{ trans('common.sort') }}</div>
-    <form action="{{ baseUrl("/settings/users/{$currentUser->id}/change-sort/{$type}") }}" method="post">
+    <form action="{{ url("/settings/users/{$currentUser->id}/change-sort/{$type}") }}" method="post">
 
         {!! csrf_field() !!}
         {!! method_field('PATCH') !!}
 
         <div class="list-sort">
             <div class="list-sort-type dropdown-container" dropdown>
-                <div dropdown-toggle>{{ $options[$selectedSort] }}</div>
+                <div dropdown-toggle aria-haspopup="true" aria-expanded="false" aria-label="{{ trans('common.sort_options') }}" tabindex="0">{{ $options[$selectedSort] }}</div>
                 <ul class="dropdown-menu">
                     @foreach($options as $key => $label)
                         <li @if($key === $selectedSort) class="active" @endif><a href="#" data-sort-value="{{$key}}">{{ $label }}</a></li>
                     @endforeach
                 </ul>
             </div>
-            <div class="list-sort-dir" data-sort-dir>
+            <button href="#" class="list-sort-dir" type="button" data-sort-dir
+                    aria-label="{{ trans('common.sort_direction_toggle') }} - {{ $order === 'asc' ? trans('common.sort_ascending') : trans('common.sort_descending') }}" tabindex="0">
                 @icon($order === 'desc' ? 'sort-up' : 'sort-down')
-            </div>
+            </button>
         </div>
     </form>
 </div>
\ No newline at end of file
index 9eb00e1d95261d0b977ca44a8f900bd3d4fa8618..9f911c88231d1775366263e0e29766705690df94 100644 (file)
@@ -1,5 +1,5 @@
 <div>
-    <form action="{{ baseUrl("/settings/users/{$currentUser->id}/switch-${type}-view") }}" method="POST" class="inline">
+    <form action="{{ url("/settings/users/{$currentUser->id}/switch-${type}-view") }}" method="POST" class="inline">
         {!! csrf_field() !!}
         {!! method_field('PATCH') !!}
         <input type="hidden" value="{{ $view === 'list'? 'grid' : 'list' }}" name="view_type">
index 03c0b93e71a9acdc5fe11c4e5aa02fa06a377c9f..f19e560a2d5f633b01bf063c074d5396a5819419 100644 (file)
                         </table>
 
 
-                        <button type="submit" class="button primary">{{ trans('entities.search_update') }}</button>
+                        <button type="submit" class="button">{{ trans('entities.search_update') }}</button>
                     </form>
 
                 </div>
             <div>
                 <div v-pre class="card content-wrap">
                     <h1 class="list-heading">{{ trans('entities.search_results') }}</h1>
-                    <form action="{{ baseUrl('/search') }}" method="GET"  class="search-box flexible hide-over-l">
+                    <form action="{{ url('/search') }}" method="GET"  class="search-box flexible hide-over-l">
                         <input value="{{$searchTerm}}" type="text" name="term" placeholder="{{ trans('common.search') }}">
                         <button type="submit">@icon('search')</button>
                         <button v-if="searching" v-cloak class="search-box-cancel text-neg" v-on:click="clearSearch" type="button">@icon('close')</button>
index 2dabe9dec5830b29a293fc3535b62aa97930c320..ba2b92fe7006fd211809c2e50bdec02afe2882b9 100644 (file)
@@ -15,7 +15,7 @@
 
         <div class="card content-wrap auto-height">
             <h2 class="list-heading">{{ trans('settings.app_features_security') }}</h2>
-            <form action="{{ baseUrl("/settings") }}" method="POST">
+            <form action="{{ url("/settings") }}" method="POST">
                 {!! csrf_field() !!}
 
                 <div class="setting-list">
@@ -27,7 +27,7 @@
                             <p class="small">{!! trans('settings.app_public_access_desc') !!}</p>
                             @if(userCan('users-manage'))
                                 <p class="small mb-none">
-                                    <a href="{{ baseUrl($guestUser->getEditUrl()) }}">{!! trans('settings.app_public_access_desc_guest') !!}</a>
+                                    <a href="{{ url($guestUser->getEditUrl()) }}">{!! trans('settings.app_public_access_desc_guest') !!}</a>
                                 </p>
                             @endif
                         </div>
                 </div>
 
                 <div class="form-group text-right">
-                    <button type="submit" class="button primary">{{ trans('settings.settings_save') }}</button>
+                    <button type="submit" class="button">{{ trans('settings.settings_save') }}</button>
                 </div>
             </form>
         </div>
 
         <div class="card content-wrap auto-height">
             <h2 class="list-heading">{{ trans('settings.app_customization') }}</h2>
-            <form action="{{ baseUrl("/settings") }}" method="POST" enctype="multipart/form-data">
+            <form action="{{ url("/settings") }}" method="POST" enctype="multipart/form-data">
                 {!! csrf_field() !!}
 
                 <div class="setting-list">
                             @include('components.image-picker', [
                                      'removeName' => 'setting-app-logo',
                                      'removeValue' => 'none',
-                                     'defaultImage' => baseUrl('/logo.png'),
+                                     'defaultImage' => url('/logo.png'),
                                      'currentImage' => setting('app-logo'),
                                      'name' => 'app_logo',
                                      'imageClass' => 'logo-image',
                             <label class="setting-list-label">{{ trans('settings.app_primary_color') }}</label>
                             <p class="small">{!! trans('settings.app_primary_color_desc') !!}</p>
                         </div>
-                        <div>
-                            <input type="text" value="{{ setting('app-color') }}" name="setting-app-color" id="setting-app-color" placeholder="#0288D1">
+                        <div setting-app-color-picker class="text-m-right">
+                            <input type="color" value="{{ setting('app-color') }}" name="setting-app-color" id="setting-app-color" placeholder="#206ea7">
                             <input type="hidden" value="{{ setting('app-color-light') }}" name="setting-app-color-light" id="setting-app-color-light">
+                            <br>
+                            <button type="button" class="text-button text-muted mt-s mx-s" setting-app-color-picker-reset>{{ trans('common.reset') }}</button>
                         </div>
                     </div>
 
                         <label for="setting-app-custom-head" class="setting-list-label">{{ trans('settings.app_custom_html') }}</label>
                         <p class="small">{{ trans('settings.app_custom_html_desc') }}</p>
                         <textarea name="setting-app-custom-head" id="setting-app-custom-head" class="simple-code-input mt-m">{{ setting('app-custom-head', '') }}</textarea>
+                        <p class="small text-right">{{ trans('settings.app_custom_html_disabled_notice') }}</p>
                     </div>
 
 
                 </div>
 
                 <div class="form-group text-right">
-                    <button type="submit" class="button primary">{{ trans('settings.settings_save') }}</button>
+                    <button type="submit" class="button">{{ trans('settings.settings_save') }}</button>
                 </div>
             </form>
         </div>
 
         <div class="card content-wrap auto-height">
             <h2 class="list-heading">{{ trans('settings.reg_settings') }}</h2>
-            <form action="{{ baseUrl("/settings") }}" method="POST">
+            <form action="{{ url("/settings") }}" method="POST">
                 {!! csrf_field() !!}
 
                 <div class="setting-list">
                 </div>
 
                 <div class="form-group text-right">
-                    <button type="submit" class="button primary">{{ trans('settings.settings_save') }}</button>
+                    <button type="submit" class="button">{{ trans('settings.settings_save') }}</button>
                 </div>
             </form>
         </div>
 
     @include('components.image-manager', ['imageType' => 'system'])
     @include('components.entity-selector-popup', ['entityTypes' => 'page'])
-@stop
-
-@section('scripts')
-    <script src="{{ baseUrl("/libs/jq-color-picker/tiny-color-picker.min.js?version=1.0.0") }}"></script>
-    <script type="text/javascript">
-        $('#setting-app-color').colorPicker({
-            opacity: false,
-            renderCallback: function($elm, toggled) {
-                const hexVal = '#' + this.color.colors.HEX;
-                const rgb = this.color.colors.RND.rgb;
-                const rgbLightVal = 'rgba('+ [rgb.r, rgb.g, rgb.b, '0.15'].join(',') +')';
-
-                // Set textbox color to hex color code.
-                const isEmpty = $.trim($elm.val()).length === 0;
-                if (!isEmpty) $elm.val(hexVal);
-                $('#setting-app-color-light').val(isEmpty ? '' : rgbLightVal);
-
-                const customStyles = document.getElementById('custom-styles');
-                const oldColor = customStyles.getAttribute('data-color');
-                const oldColorLight = customStyles.getAttribute('data-color-light');
-
-                customStyles.innerHTML = customStyles.innerHTML.split(oldColor).join(hexVal);
-                customStyles.innerHTML = customStyles.innerHTML.split(oldColorLight).join(rgbLightVal);
-
-                customStyles.setAttribute('data-color', hexVal);
-                customStyles.setAttribute('data-color-light', rgbLightVal);
-            }
-        });
-    </script>
 @stop
\ No newline at end of file
index 8e60b65aa0ae7b37095d8de7596abadc6d0197a3..aa5bc39c49c4fdc94d26b7fce5eb75dfba391786 100644 (file)
@@ -22,7 +22,7 @@
                 <p class="small text-muted">{{ trans('settings.maint_image_cleanup_desc') }}</p>
             </div>
             <div>
-                <form method="POST" action="{{ baseUrl('/settings/maintenance/cleanup-images') }}">
+                <form method="POST" action="{{ url('/settings/maintenance/cleanup-images') }}">
                     {!! csrf_field()  !!}
                     <input type="hidden" name="_method" value="DELETE">
                     <div>
index ddbaa3f2a865db53fea0cfd47c0ce1d33133f9dc..896de9d97477c0c3e510ca806ed7e33cbf12e611 100644 (file)
@@ -1,13 +1,13 @@
 
-<div class="active-link-list">
+<nav class="active-link-list">
     @if($currentUser->can('settings-manage'))
-        <a href="{{ baseUrl('/settings') }}" @if($selected == 'settings') class="active" @endif>@icon('settings'){{ trans('settings.settings') }}</a>
-        <a href="{{ baseUrl('/settings/maintenance') }}" @if($selected == 'maintenance') class="active" @endif>@icon('spanner'){{ trans('settings.maint') }}</a>
+        <a href="{{ url('/settings') }}" @if($selected == 'settings') class="active" @endif>@icon('settings'){{ trans('settings.settings') }}</a>
+        <a href="{{ url('/settings/maintenance') }}" @if($selected == 'maintenance') class="active" @endif>@icon('spanner'){{ trans('settings.maint') }}</a>
     @endif
     @if($currentUser->can('users-manage'))
-        <a href="{{ baseUrl('/settings/users') }}" @if($selected == 'users') class="active" @endif>@icon('users'){{ trans('settings.users') }}</a>
+        <a href="{{ url('/settings/users') }}" @if($selected == 'users') class="active" @endif>@icon('users'){{ trans('settings.users') }}</a>
     @endif
     @if($currentUser->can('user-roles-manage'))
-        <a href="{{ baseUrl('/settings/roles') }}" @if($selected == 'roles') class="active" @endif>@icon('lock-open'){{ trans('settings.roles') }}</a>
+        <a href="{{ url('/settings/roles') }}" @if($selected == 'roles') class="active" @endif>@icon('lock-open'){{ trans('settings.roles') }}</a>
     @endif
-</div>
\ No newline at end of file
+</nav>
\ No newline at end of file
index 80a6fc3820d1bffb5c671652d02e0e56f409d391..df902133f3ee514858ae703df5e52b5ab10f934b 100644 (file)
@@ -8,7 +8,7 @@
             @include('settings.navbar', ['selected' => 'roles'])
         </div>
 
-        <form action="{{ baseUrl("/settings/roles/new") }}" method="POST">
+        <form action="{{ url("/settings/roles/new") }}" method="POST">
             @include('settings.roles.form', ['title' => trans('settings.role_create')])
         </form>
     </div>
index a2ea0d7281cd22fb30988048f2965cf0a067315d..4f40345df99947dec5de11fa2d44cf88dcf382e0 100644 (file)
@@ -12,7 +12,7 @@
 
             <p>{{ trans('settings.role_delete_confirm', ['roleName' => $role->display_name]) }}</p>
 
-            <form action="{{ baseUrl("/settings/roles/delete/{$role->id}") }}" method="POST">
+            <form action="{{ url("/settings/roles/delete/{$role->id}") }}" method="POST">
                 {!! csrf_field() !!}
                 <input type="hidden" name="_method" value="DELETE">
 
@@ -31,8 +31,8 @@
                     </div>
                     <div>
                         <div class="form-group text-right">
-                            <a href="{{ baseUrl("/settings/roles/{$role->id}") }}" class="button outline">{{ trans('common.cancel') }}</a>
-                            <button type="submit" class="button primary">{{ trans('common.confirm') }}</button>
+                            <a href="{{ url("/settings/roles/{$role->id}") }}" class="button outline">{{ trans('common.cancel') }}</a>
+                            <button type="submit" class="button">{{ trans('common.confirm') }}</button>
                         </div>
                     </div>
                 </div>
index a7b81322977c59eb48780d4fd8a8b13a2e23e3ea..0f83bdb0becca1370a8a3085055cf376e2a0b1e3 100644 (file)
@@ -7,7 +7,7 @@
             @include('settings.navbar', ['selected' => 'roles'])
         </div>
 
-        <form action="{{ baseUrl("/settings/roles/{$role->id}") }}" method="POST">
+        <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'])
         </form>
index 6d723086714bbc844cdd49ab672571c037324c2c..20b8d65ed9c22fd12a2050d6e2f2ba3a060d04d5 100644 (file)
@@ -38,6 +38,7 @@
                 <div>@include('settings.roles.checkbox', ['permission' => 'user-roles-manage', 'label' => trans('settings.role_manage_roles')])</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' => 'settings-manage', 'label' => trans('settings.role_manage_settings')])</div>
             </div>
         </div>
     </div>
 
     <div class="form-group text-right">
-        <a href="{{ baseUrl("/settings/roles") }}" class="button outline">{{ trans('common.cancel') }}</a>
+        <a href="{{ url("/settings/roles") }}" class="button outline">{{ trans('common.cancel') }}</a>
         @if (isset($role) && $role->id)
-            <a href="{{ baseUrl("/settings/roles/delete/{$role->id}") }}" class="button outline">{{ trans('settings.role_delete') }}</a>
+            <a href="{{ url("/settings/roles/delete/{$role->id}") }}" class="button outline">{{ trans('settings.role_delete') }}</a>
         @endif
-        <button type="submit" class="button primary">{{ trans('settings.role_save') }}</button>
+        <button type="submit" class="button">{{ trans('settings.role_save') }}</button>
     </div>
 
 </div>
                     </div>
                     <div>
                         @if(userCan('users-manage') || $currentUser->id == $user->id)
-                            <a href="{{ baseUrl("/settings/users/{$user->id}") }}">
+                            <a href="{{ url("/settings/users/{$user->id}") }}">
                                 @endif
                                 {{ $user->name }}
                                 @if(userCan('users-manage') || $currentUser->id == $user->id)
index 8eae235daf8a471906ed8d21edeb94f23e7c53a2..47cd8c920fffa07909215e12e8f8d3d5d8dedee9 100644 (file)
@@ -14,7 +14,7 @@
                 <h1 class="list-heading">{{ trans('settings.role_user_roles') }}</h1>
 
                 <div class="text-right">
-                    <a href="{{ baseUrl("/settings/roles/new") }}" class="button outline">{{ trans('settings.role_create') }}</a>
+                    <a href="{{ url("/settings/roles/new") }}" class="button outline">{{ trans('settings.role_create') }}</a>
                 </div>
             </div>
 
@@ -26,7 +26,7 @@
                 </tr>
                 @foreach($roles as $role)
                     <tr>
-                        <td><a href="{{ baseUrl("/settings/roles/{$role->id}") }}">{{ $role->display_name }}</a></td>
+                        <td><a href="{{ url("/settings/roles/{$role->id}") }}">{{ $role->display_name }}</a></td>
                         <td>{{ $role->description }}</td>
                         <td class="text-center">{{ $role->users->count() }}</td>
                     </tr>
index 706e15d07faafcb6f3f6855c8671375d79e46cb5..bea20eca93624cf234e89b73099b51fc5574c3c4 100644 (file)
             ]])
         </div>
 
-        <div class="card content-wrap">
+        <main class="card content-wrap">
             <h1 class="list-heading">{{ trans('entities.shelves_create') }}</h1>
-            <form action="{{ baseUrl("/shelves") }}" method="POST" enctype="multipart/form-data">
+            <form action="{{ url("/shelves") }}" method="POST" enctype="multipart/form-data">
                 @include('shelves.form', ['shelf' => null, 'books' => $books])
             </form>
-        </div>
+        </main>
 
     </div>
 
index 8c2cd4f45e7d11d816a3472a9e0b109c0aeb392d..5ae3638fee955ec3f30ee3c068f5c45e6f7b7976 100644 (file)
             ]])
         </div>
 
-        <div class="card content-wrap">
+        <main class="card content-wrap">
             <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])
             </form>
-        </div>
+        </main>
     </div>
 
 @stop
\ No newline at end of file
index 4191f421de0e10ab44484283d38f8fb14b0ed81b..19c5bbecd69ae95c70bd782703cfe377ddd32b30 100644 (file)
     <div class="form-group">
         <label for="books">{{ trans('entities.shelves_books') }}</label>
         <input type="hidden" id="books-input" name="books"
-               value="{{ isset($shelf) ? $shelf->books->implode('id', ',') : '' }}">
-        <div class="scroll-box">
-            <div class="scroll-box-item text-small text-muted instruction">
-                {{ trans('entities.shelves_drag_books') }}
-            </div>
-            <div class="scroll-box-item scroll-box-placeholder" style="display: none;">
-                <a href="#" class="text-muted">@icon('book') ...</a>
-            </div>
-            @if (isset($shelfBooks) && count($shelfBooks) > 0)
-                @foreach ($shelfBooks as $book)
+               value="{{ isset($shelf) ? $shelf->visibleBooks->implode('id', ',') : '' }}">
+        <div class="scroll-box" shelf-sort-assigned-books data-instruction="{{ trans('entities.shelves_drag_books') }}">
+            @if (count($shelf->visibleBooks ?? []) > 0)
+                @foreach ($shelf->visibleBooks as $book)
                     <div data-id="{{ $book->id }}" class="scroll-box-item">
                         <a href="{{ $book->getUrl() }}" class="text-book">@icon('book'){{ $book->name }}</a>
                     </div>
 
 
 <div class="form-group" collapsible id="logo-control">
-    <div class="collapse-title text-primary" collapsible-trigger>
-        <label for="user-avatar">{{ trans('common.cover_image') }}</label>
-    </div>
+    <button type="button" class="collapse-title text-primary" collapsible-trigger aria-expanded="false">
+        <label>{{ trans('common.cover_image') }}</label>
+    </button>
     <div class="collapse-content" collapsible-content>
         <p class="small">{{ trans('common.cover_image_description') }}</p>
 
         @include('components.image-picker', [
-            'defaultImage' => baseUrl('/book_default_cover.png'),
-            'currentImage' => (isset($shelf) && $shelf->cover) ? $shelf->getBookCover() : baseUrl('/book_default_cover.png') ,
+            'defaultImage' => url('/book_default_cover.png'),
+            'currentImage' => (isset($shelf) && $shelf->cover) ? $shelf->getBookCover() : url('/book_default_cover.png') ,
             'name' => 'image',
             'imageClass' => 'cover'
         ])
 </div>
 
 <div class="form-group" collapsible id="tags-control">
-    <div class="collapse-title text-primary" collapsible-trigger>
+    <button type="button" class="collapse-title text-primary" collapsible-trigger aria-expanded="false">
         <label for="tag-manager">{{ trans('entities.shelf_tags') }}</label>
-    </div>
+    </button>
     <div class="collapse-content" collapsible-content>
         @include('components.tag-manager', ['entity' => $shelf ?? null, 'entityType' => 'bookshelf'])
     </div>
 </div>
 
 <div class="form-group text-right">
-    <a href="{{ isset($shelf) ? $shelf->getUrl() : baseUrl('/shelves') }}" class="button outline">{{ trans('common.cancel') }}</a>
-    <button type="submit" class="button primary">{{ trans('entities.shelves_save') }}</button>
+    <a href="{{ isset($shelf) ? $shelf->getUrl() : url('/shelves') }}" class="button outline">{{ trans('common.cancel') }}</a>
+    <button type="submit" class="button">{{ trans('entities.shelves_save') }}</button>
 </div>
\ No newline at end of file
index 8cf959b1e49529092107324fff48e9e74bc8185e..98f97f1331b8c9985dd6f869759ac0116571c510 100644 (file)
@@ -10,7 +10,7 @@
         <h5>{{ trans('common.actions') }}</h5>
         <div class="icon-list text-primary">
             @if($currentUser->can('bookshelf-create-all'))
-                <a href="{{ baseUrl("/create-shelf") }}" class="icon-list-item">
+                <a href="{{ url("/create-shelf") }}" class="icon-list-item">
                     <span>@icon('add')</span>
                     <span>{{ trans('entities.shelves_new_action') }}</span>
                 </a>
index 70787f7e805ce9990d5403dadff7b58538d774a5..b20b08a2c59e40f8a110394404f63ac198c91f8f 100644 (file)
@@ -1,5 +1,5 @@
 
-<div class="content-wrap mt-m card">
+<main class="content-wrap mt-m card">
 
     <div class="grid half v-center">
         <h1 class="list-heading">{{ trans('entities.shelves') }}</h1>
@@ -31,8 +31,8 @@
     @else
         <p class="text-muted">{{ trans('entities.shelves_empty') }}</p>
         @if(userCan('bookshelf-create-all'))
-            <a href="{{ baseUrl("/create-shelf") }}" class="button outline">@icon('edit'){{ trans('entities.create_now') }}</a>
+            <a href="{{ url("/create-shelf") }}" class="button outline">@icon('edit'){{ trans('entities.create_now') }}</a>
         @endif
     @endif
 
-</div>
+</main>
index 3a9d599519a95035e7d4a8cb8dfbcca23303669f..2212e1c1e32ae3014b3f839f802d1dc534542c3a 100644 (file)
@@ -8,13 +8,13 @@
         ]])
     </div>
 
-    <div class="card content-wrap">
+    <main class="card content-wrap">
         <h1 class="break-text">{{$shelf->name}}</h1>
         <div class="book-content">
             <p class="text-muted">{!! nl2br(e($shelf->description)) !!}</p>
-            @if(count($books) > 0)
+            @if(count($shelf->visibleBooks) > 0)
                 <div class="entity-list">
-                    @foreach($books as $book)
+                    @foreach($shelf->visibleBooks as $book)
                         @include('books.list-item', ['book' => $book])
                     @endforeach
                 </div>
@@ -39,7 +39,7 @@
                 </div>
             @endif
         </div>
-    </div>
+    </main>
 
 @stop
 
index 00e9df2f970fbf9828bb11793eb3bbc3d64d4e17..71c546964ef6fb31d89a553256654324998de35b 100644 (file)
@@ -4,7 +4,7 @@
 
 @section('content')
 
-    <div class="tri-layout-mobile-tabs text-primary>
+    <div class="tri-layout-mobile-tabs text-primary print-hidden">
         <div class="grid half no-break no-gap">
             <div class="tri-layout-mobile-tab px-m py-s" tri-layout-mobile-tab="info">
                 {{ trans('common.tab_info') }}
@@ -18,9 +18,9 @@
     <div class="tri-layout-container" tri-layout @yield('container-attrs') >
 
         <div class="tri-layout-left print-hidden pt-m" id="sidebar">
-            <div class="tri-layout-left-contents">
+            <aside class="tri-layout-left-contents">
                 @yield('left')
-            </div>
+            </aside>
         </div>
 
         <div class="@yield('body-wrap-classes') tri-layout-middle">
@@ -30,9 +30,9 @@
         </div>
 
         <div class="tri-layout-right print-hidden pt-m">
-            <div class="tri-layout-right-contents">
+            <aside class="tri-layout-right-contents">
                 @yield('right')
-            </div>
+            </aside>
         </div>
     </div>
 
index cd5d75f8fb9b9da6a7d3e0c0569cab7485c7fb7d..9971eeeeb54ca63ba42045982f076e6324936989 100644 (file)
@@ -8,10 +8,10 @@
             @include('settings.navbar', ['selected' => 'users'])
         </div>
 
-        <div class="card content-wrap">
+        <main class="card content-wrap">
             <h1 class="list-heading">{{ trans('settings.users_add_new') }}</h1>
 
-            <form action="{{ baseUrl("/settings/users/create") }}" method="post">
+            <form action="{{ url("/settings/users/create") }}" method="post">
                 {!! csrf_field() !!}
 
                 <div class="setting-list">
                 </div>
 
                 <div class="form-group text-right">
-                    <a href="{{  baseUrl($currentUser->can('users-manage') ? "/settings/users" : "/") }}" class="button outline">{{ trans('common.cancel') }}</a>
-                    <button class="button primary" type="submit">{{ trans('common.save') }}</button>
+                    <a href="{{  url($currentUser->can('users-manage') ? "/settings/users" : "/") }}" class="button outline">{{ trans('common.cancel') }}</a>
+                    <button class="button" type="submit">{{ trans('common.save') }}</button>
                 </div>
 
             </form>
 
-        </div>
+        </main>
     </div>
 
 @stop
index 15ad7a9ec66df6678ed3777f261bb55f7e6dedf2..d3349c2f3fc29b93e6b10319e95d0cefc97df7e5 100644 (file)
             <div class="grid half">
                 <p class="text-neg"><strong>{{ trans('settings.users_delete_confirm') }}</strong></p>
                 <div>
-                    <form action="{{ baseUrl("/settings/users/{$user->id}") }}" method="POST" class="text-right">
+                    <form action="{{ url("/settings/users/{$user->id}") }}" method="POST" class="text-right">
                         {!! csrf_field() !!}
 
                         <input type="hidden" name="_method" value="DELETE">
-                        <a href="{{ baseUrl("/settings/users/{$user->id}") }}" class="button outline">{{ trans('common.cancel') }}</a>
-                        <button type="submit" class="button primary">{{ trans('common.confirm') }}</button>
+                        <a href="{{ url("/settings/users/{$user->id}") }}" class="button outline">{{ trans('common.cancel') }}</a>
+                        <button type="submit" class="button">{{ trans('common.confirm') }}</button>
                     </form>
                 </div>
             </div>
index 377500193dc7d3b910667224a31e8645585a83e9..ff1e7cbe5d51577e2cd807dc4842aee8b268f1c8 100644 (file)
@@ -7,9 +7,9 @@
             @include('settings.navbar', ['selected' => 'users'])
         </div>
 
-        <div class="card content-wrap">
+        <section class="card content-wrap">
             <h1 class="list-heading">{{ $user->id === $currentUser->id ? trans('settings.users_edit_profile') : trans('settings.users_edit') }}</h1>
-            <form action="{{ baseUrl("/settings/users/{$user->id}") }}" method="post" enctype="multipart/form-data">
+            <form action="{{ url("/settings/users/{$user->id}") }}" method="post" enctype="multipart/form-data">
                 {!! csrf_field() !!}
                 <input type="hidden" name="_method" value="PUT">
 
@@ -26,7 +26,7 @@
                                 'resizeHeight' => '512',
                                 'resizeWidth' => '512',
                                 'showRemove' => false,
-                                'defaultImage' => baseUrl('/user_avatar.png'),
+                                'defaultImage' => url('/user_avatar.png'),
                                 'currentImage' => $user->getAvatar(80),
                                 'currentId' => $user->image_id,
                                 'name' => 'profile_image',
                 </div>
 
                 <div class="text-right">
-                    <a href="{{  baseUrl($currentUser->can('users-manage') ? "/settings/users" : "/") }}" class="button outline">{{ trans('common.cancel') }}</a>
+                    <a href="{{  url($currentUser->can('users-manage') ? "/settings/users" : "/") }}" class="button outline">{{ trans('common.cancel') }}</a>
                     @if($authMethod !== 'system')
-                        <a href="{{ baseUrl("/settings/users/{$user->id}/delete") }}" class="button outline">{{ trans('settings.users_delete') }}</a>
+                        <a href="{{ url("/settings/users/{$user->id}/delete") }}" class="button outline">{{ trans('settings.users_delete') }}</a>
                     @endif
-                    <button class="button primary" type="submit">{{ trans('common.save') }}</button>
+                    <button class="button" type="submit">{{ trans('common.save') }}</button>
                 </div>
             </form>
-        </div>
+        </section>
 
         @if($currentUser->id === $user->id && count($activeSocialDrivers) > 0)
-            <div class="card content-wrap auto-height">
+            <section class="card content-wrap auto-height">
                 <h2 class="list-heading">{{ trans('settings.users_social_accounts') }}</h2>
                 <p class="text-muted">{{ trans('settings.users_social_accounts_info') }}</p>
                 <div class="container">
                     <div class="grid third">
                         @foreach($activeSocialDrivers as $driver => $enabled)
                             <div class="text-center mb-m">
-                                <div>@icon('auth/'. $driver, ['style' => 'width: 56px;height: 56px;'])</div>
+                                <div role="presentation">@icon('auth/'. $driver, ['style' => 'width: 56px;height: 56px;'])</div>
                                 <div>
                                     @if($user->hasSocialAccount($driver))
-                                        <a href="{{ baseUrl("/login/service/{$driver}/detach") }}" class="button small outline">{{ trans('settings.users_social_disconnect') }}</a>
+                                        <a href="{{ url("/login/service/{$driver}/detach") }}" aria-label="{{ trans('settings.users_social_disconnect') }} - {{ $driver }}"
+                                           class="button small outline">{{ trans('settings.users_social_disconnect') }}</a>
                                     @else
-                                        <a href="{{ baseUrl("/login/service/{$driver}") }}" class="button small outline">{{ trans('settings.users_social_connect') }}</a>
+                                        <a href="{{ url("/login/service/{$driver}") }}" aria-label="{{ trans('settings.users_social_connect') }} - {{ $driver }}"
+                                           class="button small outline">{{ trans('settings.users_social_connect') }}</a>
                                     @endif
                                 </div>
                             </div>
                         @endforeach
                     </div>
                 </div>
-            </div>
+            </section>
         @endif
     </div>
 
index 96beb7b2f97888bde380b582225b247bb8bc3040..32b717ec8c2ec4db4ef10211f54f0b1aab30f054 100644 (file)
@@ -19,7 +19,7 @@
         <div>
             @if($authMethod !== 'ldap' || userCan('users-manage'))
                 <label for="email">{{ trans('auth.email') }}</label>
-                @include('form.text', ['name' => 'email'])
+                @include('form.text', ['name' => 'email', 'disabled' => !userCan('users-manage')])
             @endif
         </div>
     </div>
 @endif
 
 @if($authMethod === 'standard')
-    <div>
+    <div new-user-password>
         <label class="setting-list-label">{{ trans('settings.users_password') }}</label>
-        <p class="small">{{ trans('settings.users_password_desc') }}</p>
-        @if(isset($model))
+
+        @if(!isset($model))
             <p class="small">
-                {{ trans('settings.users_password_warning') }}
+                {{ trans('settings.users_send_invite_text') }}
             </p>
+
+            @include('components.toggle-switch', [
+                'name' => 'send_invite',
+                'value' => old('send_invite', 'true') === 'true',
+                'label' => trans('settings.users_send_invite_option')
+            ])
+
         @endif
-        <div class="grid half mt-m gap-xl">
-            <div>
-                <label for="password">{{ trans('auth.password') }}</label>
-                @include('form.password', ['name' => 'password'])
-            </div>
-            <div>
-                <label for="password-confirm">{{ trans('auth.password_confirm') }}</label>
-                @include('form.password', ['name' => 'password-confirm'])
+
+        <div id="password-input-container" @if(!isset($model)) style="display: none;" @endif>
+            <p class="small">{{ trans('settings.users_password_desc') }}</p>
+            @if(isset($model))
+                <p class="small">
+                    {{ trans('settings.users_password_warning') }}
+                </p>
+            @endif
+            <div class="grid half mt-m gap-xl">
+                <div>
+                    <label for="password">{{ trans('auth.password') }}</label>
+                    @include('form.password', ['name' => 'password'])
+                </div>
+                <div>
+                    <label for="password-confirm">{{ trans('auth.password_confirm') }}</label>
+                    @include('form.password', ['name' => 'password-confirm'])
+                </div>
             </div>
         </div>
+
     </div>
 @endif
\ No newline at end of file
index af6b4d4f93e70d25c81a7dfff0af76e2479503c1..da373c1618b563fddcc9768644b7d1ac608e29cf 100644 (file)
@@ -7,14 +7,14 @@
             @include('settings.navbar', ['selected' => 'users'])
         </div>
 
-        <div class="card content-wrap">
+        <main class="card content-wrap">
 
             <div class="grid right-focus v-center">
                 <h1 class="list-heading">{{ trans('settings.users') }}</h1>
 
                 <div class="text-right">
                     <div class="block inline mr-s">
-                        <form method="get" action="{{ baseUrl("/settings/users") }}">
+                        <form method="get" action="{{ url("/settings/users") }}">
                             @foreach(collect($listDetails)->except('search') as $name => $val)
                                 <input type="hidden" name="{{ $name }}" value="{{ $val }}">
                             @endforeach
@@ -22,7 +22,7 @@
                         </form>
                     </div>
                     @if(userCan('users-manage'))
-                        <a href="{{ baseUrl("/settings/users/create") }}" style="margin-top: 0;" class="outline button">{{ trans('settings.users_add_new') }}</a>
+                        <a href="{{ url("/settings/users/create") }}" style="margin-top: 0;" class="outline button">{{ trans('settings.users_add_new') }}</a>
                     @endif
                 </div>
             </div>
@@ -43,7 +43,7 @@
                         <td class="text-center" style="line-height: 0;"><img class="avatar med" src="{{ $user->getAvatar(40)}}" alt="{{ $user->name }}"></td>
                         <td>
                             @if(userCan('users-manage') || $currentUser->id == $user->id)
-                                <a href="{{ baseUrl("/settings/users/{$user->id}") }}">
+                                <a href="{{ url("/settings/users/{$user->id}") }}">
                                     @endif
                                     {{ $user->name }} <br> <span class="text-muted">{{ $user->email }}</span>
                                     @if(userCan('users-manage') || $currentUser->id == $user->id)
@@ -52,7 +52,7 @@
                         </td>
                         <td>
                             @foreach($user->roles as $index => $role)
-                                <small><a href="{{ baseUrl("/settings/roles/{$role->id}") }}">{{$role->display_name}}</a>@if($index !== count($user->roles) -1),@endif</small>
+                                <small><a href="{{ url("/settings/roles/{$role->id}") }}">{{$role->display_name}}</a>@if($index !== count($user->roles) -1),@endif</small>
                             @endforeach
                         </td>
                     </tr>
@@ -62,7 +62,7 @@
             <div>
                 {{ $users->links() }}
             </div>
-        </div>
+        </main>
 
     </div>
 
index e2689790f185516d5d34408ba21dca05c138f3f0..4028b5c1da731c45925d8d27caea334b46cc0e74 100644 (file)
@@ -7,14 +7,14 @@
         <div class="grid right-focus reverse-collapse">
 
             <div>
-                <div id="recent-user-activity" class="mb-xl">
+                <section id="recent-user-activity" class="mb-xl">
                     <h5>{{ trans('entities.recent_activity') }}</h5>
                     @include('partials.activity-list', ['activity' => $activity])
-                </div>
+                </section>
             </div>
 
             <div>
-                <div class="card content-wrap auto-height">
+                <section class="card content-wrap auto-height">
                     <div class="grid half v-center">
                         <div>
                             <div class="mr-m float left">
 
                         </div>
                     </div>
-                </div>
+                </section>
 
-                <div class="card content-wrap auto-height book-contents">
+                <section class="card content-wrap auto-height book-contents">
                     <h2 id="recent-pages" class="list-heading">
                         {{ trans('entities.recently_created_pages') }}
                         @if (count($recentlyCreated['pages']) > 0)
-                            <a href="{{ baseUrl('/search?term=' . urlencode('{created_by:'.$user->id.'} {type:page}') ) }}" class="text-small ml-s">{{ trans('common.view_all') }}</a>
+                            <a href="{{ url('/search?term=' . urlencode('{created_by:'.$user->id.'} {type:page}') ) }}" class="text-small ml-s">{{ trans('common.view_all') }}</a>
                         @endif
                     </h2>
                     @if (count($recentlyCreated['pages']) > 0)
                     @else
                         <p class="text-muted">{{ trans('entities.profile_not_created_pages', ['userName' => $user->name]) }}</p>
                     @endif
-                </div>
+                </section>
 
-                <div class="card content-wrap auto-height book-contents">
+                <section class="card content-wrap auto-height book-contents">
                     <h2 id="recent-chapters" class="list-heading">
                         {{ trans('entities.recently_created_chapters') }}
                         @if (count($recentlyCreated['chapters']) > 0)
-                            <a href="{{ baseUrl('/search?term=' . urlencode('{created_by:'.$user->id.'} {type:chapter}') ) }}" class="text-small ml-s">{{ trans('common.view_all') }}</a>
+                            <a href="{{ url('/search?term=' . urlencode('{created_by:'.$user->id.'} {type:chapter}') ) }}" class="text-small ml-s">{{ trans('common.view_all') }}</a>
                         @endif
                     </h2>
                     @if (count($recentlyCreated['chapters']) > 0)
                     @else
                         <p class="text-muted">{{ trans('entities.profile_not_created_chapters', ['userName' => $user->name]) }}</p>
                     @endif
-                </div>
+                </section>
 
-                <div class="card content-wrap auto-height book-contents">
+                <section class="card content-wrap auto-height book-contents">
                     <h2 id="recent-books" class="list-heading">
                         {{ trans('entities.recently_created_books') }}
                         @if (count($recentlyCreated['books']) > 0)
-                            <a href="{{ baseUrl('/search?term=' . urlencode('{created_by:'.$user->id.'} {type:book}') ) }}" class="text-small ml-s">{{ trans('common.view_all') }}</a>
+                            <a href="{{ url('/search?term=' . urlencode('{created_by:'.$user->id.'} {type:book}') ) }}" class="text-small ml-s">{{ trans('common.view_all') }}</a>
                         @endif
                     </h2>
                     @if (count($recentlyCreated['books']) > 0)
                     @else
                         <p class="text-muted">{{ trans('entities.profile_not_created_books', ['userName' => $user->name]) }}</p>
                     @endif
-                </div>
+                </section>
 
-                <div class="card content-wrap auto-height book-contents">
+                <section class="card content-wrap auto-height book-contents">
                     <h2 id="recent-shelves" class="list-heading">
                         {{ trans('entities.recently_created_shelves') }}
                         @if (count($recentlyCreated['shelves']) > 0)
-                            <a href="{{ baseUrl('/search?term=' . urlencode('{created_by:'.$user->id.'} {type:bookshelf}') ) }}" class="text-small ml-s">{{ trans('common.view_all') }}</a>
+                            <a href="{{ url('/search?term=' . urlencode('{created_by:'.$user->id.'} {type:bookshelf}') ) }}" class="text-small ml-s">{{ trans('common.view_all') }}</a>
                         @endif
                     </h2>
                     @if (count($recentlyCreated['shelves']) > 0)
                     @else
                         <p class="text-muted">{{ trans('entities.profile_not_created_shelves', ['userName' => $user->name]) }}</p>
                     @endif
-                </div>
+                </section>
             </div>
 
         </div>
 
 
     </div>
-
-
 @stop
\ No newline at end of file
index 3e02cbba9ceb15e7dacc4cac74b3db58ca02fc57..f73b87b597853ccf5becce9eacbf5c4c7db68048 100644 (file)
@@ -1,5 +1,5 @@
 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
-<html>
+<html lang="{{ config('app.lang') }}">
 <head>
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
     <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
@@ -83,7 +83,7 @@ $style = [
                                 <!-- Logo -->
                                 <tr>
                                     <td style="{{ $style['email-masthead'] }}">
-                                        <a style="{{ $fontFamily }} {{ $style['email-masthead_name'] }}" href="{{ baseUrl('/') }}" target="_blank">
+                                        <a style="{{ $fontFamily }} {{ $style['email-masthead_name'] }}" href="{{ url('/') }}" target="_blank">
                                             {{ setting('app-name') }}
                                         </a>
                                     </td>
@@ -186,7 +186,7 @@ $style = [
                                                 <td style="{{ $fontFamily }} {{ $style['email-footer_cell'] }}">
                                                     <p style="{{ $style['paragraph-sub'] }}">
                                                         &copy; {{ date('Y') }}
-                                                        <a style="{{ $style['anchor'] }}" href="{{ baseUrl('/') }}" target="_blank">{{ setting('app-name') }}</a>.
+                                                        <a style="{{ $style['anchor'] }}" href="{{ url('/') }}" target="_blank">{{ setting('app-name') }}</a>.
                                                         {{ trans('common.email_rights') }}
                                                     </p>
                                                 </td>
index 25d7ab6928585ddfe9672136f33311dd9660bc13..5dee447a4c7abd56d67a495dcdd68336ac6b9026 100644 (file)
@@ -1,6 +1,5 @@
 <?php
 
-Route::get('/translations', 'HomeController@getTranslations');
 Route::get('/robots.txt', 'HomeController@getRobots');
 
 // Authenticated routes...
@@ -10,9 +9,7 @@ Route::group(['middleware' => 'auth'], function () {
     Route::get('/uploads/images/{path}', 'Images\ImageController@showImage')
         ->where('path', '.*$');
 
-    Route::group(['prefix' => 'pages'], function() {
-        Route::get('/recently-updated', 'PageController@showRecentlyUpdated');
-    });
+    Route::get('/pages/recently-updated', 'PageController@showRecentlyUpdated');
 
     // Shelves
     Route::get('/create-shelf', 'BookshelfController@create');
@@ -41,16 +38,16 @@ Route::group(['middleware' => 'auth'], function () {
         Route::get('/{slug}/edit', 'BookController@edit');
         Route::put('/{slug}', 'BookController@update');
         Route::delete('/{id}', 'BookController@destroy');
-        Route::get('/{slug}/sort-item', 'BookController@getSortItem');
+        Route::get('/{slug}/sort-item', 'BookSortController@showItem');
         Route::get('/{slug}', 'BookController@show');
         Route::get('/{bookSlug}/permissions', 'BookController@showPermissions');
         Route::put('/{bookSlug}/permissions', 'BookController@permissions');
         Route::get('/{slug}/delete', 'BookController@showDelete');
-        Route::get('/{bookSlug}/sort', 'BookController@sort');
-        Route::put('/{bookSlug}/sort', 'BookController@saveSort');
-        Route::get('/{bookSlug}/export/html', 'BookController@exportHtml');
-        Route::get('/{bookSlug}/export/pdf', 'BookController@exportPdf');
-        Route::get('/{bookSlug}/export/plaintext', 'BookController@exportPlainText');
+        Route::get('/{bookSlug}/sort', 'BookSortController@show');
+        Route::put('/{bookSlug}/sort', 'BookSortController@update');
+        Route::get('/{bookSlug}/export/html', 'BookExportController@html');
+        Route::get('/{bookSlug}/export/pdf', 'BookExportController@pdf');
+        Route::get('/{bookSlug}/export/plaintext', 'BookExportController@plainText');
 
         // Pages
         Route::get('/{bookSlug}/create-page', 'PageController@create');
@@ -58,9 +55,9 @@ Route::group(['middleware' => 'auth'], function () {
         Route::get('/{bookSlug}/draft/{pageId}', 'PageController@editDraft');
         Route::post('/{bookSlug}/draft/{pageId}', 'PageController@store');
         Route::get('/{bookSlug}/page/{pageSlug}', 'PageController@show');
-        Route::get('/{bookSlug}/page/{pageSlug}/export/pdf', 'PageController@exportPdf');
-        Route::get('/{bookSlug}/page/{pageSlug}/export/html', 'PageController@exportHtml');
-        Route::get('/{bookSlug}/page/{pageSlug}/export/plaintext', 'PageController@exportPlainText');
+        Route::get('/{bookSlug}/page/{pageSlug}/export/pdf', 'PageExportController@pdf');
+        Route::get('/{bookSlug}/page/{pageSlug}/export/html', 'PageExportController@html');
+        Route::get('/{bookSlug}/page/{pageSlug}/export/plaintext', 'PageExportController@plainText');
         Route::get('/{bookSlug}/page/{pageSlug}/edit', 'PageController@edit');
         Route::get('/{bookSlug}/page/{pageSlug}/move', 'PageController@showMove');
         Route::put('/{bookSlug}/page/{pageSlug}/move', 'PageController@move');
@@ -75,11 +72,11 @@ Route::group(['middleware' => 'auth'], function () {
         Route::delete('/{bookSlug}/draft/{pageId}', 'PageController@destroyDraft');
 
         // Revisions
-        Route::get('/{bookSlug}/page/{pageSlug}/revisions', 'PageController@showRevisions');
-        Route::get('/{bookSlug}/page/{pageSlug}/revisions/{revId}', 'PageController@showRevision');
-        Route::get('/{bookSlug}/page/{pageSlug}/revisions/{revId}/changes', 'PageController@showRevisionChanges');
-        Route::put('/{bookSlug}/page/{pageSlug}/revisions/{revId}/restore', 'PageController@restoreRevision');
-        Route::delete('/{bookSlug}/page/{pageSlug}/revisions/{revId}/delete', 'PageController@destroyRevision');
+        Route::get('/{bookSlug}/page/{pageSlug}/revisions', 'PageRevisionController@index');
+        Route::get('/{bookSlug}/page/{pageSlug}/revisions/{revId}', 'PageRevisionController@show');
+        Route::get('/{bookSlug}/page/{pageSlug}/revisions/{revId}/changes', 'PageRevisionController@changes');
+        Route::put('/{bookSlug}/page/{pageSlug}/revisions/{revId}/restore', 'PageRevisionController@restore');
+        Route::delete('/{bookSlug}/page/{pageSlug}/revisions/{revId}/delete', 'PageRevisionController@destroy');
 
         // Chapters
         Route::get('/{bookSlug}/chapter/{chapterSlug}/create-page', 'PageController@create');
@@ -92,9 +89,9 @@ Route::group(['middleware' => 'auth'], function () {
         Route::put('/{bookSlug}/chapter/{chapterSlug}/move', 'ChapterController@move');
         Route::get('/{bookSlug}/chapter/{chapterSlug}/edit', 'ChapterController@edit');
         Route::get('/{bookSlug}/chapter/{chapterSlug}/permissions', 'ChapterController@showPermissions');
-        Route::get('/{bookSlug}/chapter/{chapterSlug}/export/pdf', 'ChapterController@exportPdf');
-        Route::get('/{bookSlug}/chapter/{chapterSlug}/export/html', 'ChapterController@exportHtml');
-        Route::get('/{bookSlug}/chapter/{chapterSlug}/export/plaintext', 'ChapterController@exportPlainText');
+        Route::get('/{bookSlug}/chapter/{chapterSlug}/export/pdf', 'ChapterExportController@pdf');
+        Route::get('/{bookSlug}/chapter/{chapterSlug}/export/html', 'ChapterExportController@html');
+        Route::get('/{bookSlug}/chapter/{chapterSlug}/export/plaintext', 'ChapterExportController@plainText');
         Route::put('/{bookSlug}/chapter/{chapterSlug}/permissions', 'ChapterController@permissions');
         Route::get('/{bookSlug}/chapter/{chapterSlug}/delete', 'ChapterController@showDelete');
         Route::delete('/{bookSlug}/chapter/{chapterSlug}', 'ChapterController@destroy');
@@ -159,6 +156,9 @@ Route::group(['middleware' => 'auth'], function () {
     Route::get('/search/chapter/{bookId}', 'SearchController@searchChapter');
     Route::get('/search/entity/siblings', 'SearchController@searchSiblings');
 
+    Route::get('/templates', 'PageTemplateController@list');
+    Route::get('/templates/{templateId}', 'PageTemplateController@get');
+
     // Other Pages
     Route::get('/', 'HomeController@index');
     Route::get('/home', 'HomeController@index');
@@ -209,12 +209,16 @@ Route::get('/login', 'Auth\LoginController@getLogin');
 Route::post('/login', 'Auth\LoginController@login');
 Route::get('/logout', 'Auth\LoginController@logout');
 Route::get('/register', 'Auth\RegisterController@getRegister');
-Route::get('/register/confirm', 'Auth\RegisterController@getRegisterConfirmation');
-Route::get('/register/confirm/awaiting', 'Auth\RegisterController@showAwaitingConfirmation');
-Route::post('/register/confirm/resend', 'Auth\RegisterController@resendConfirmation');
-Route::get('/register/confirm/{token}', 'Auth\RegisterController@confirmEmail');
+Route::get('/register/confirm', 'Auth\ConfirmEmailController@show');
+Route::get('/register/confirm/awaiting', 'Auth\ConfirmEmailController@showAwaiting');
+Route::post('/register/confirm/resend', 'Auth\ConfirmEmailController@resend');
+Route::get('/register/confirm/{token}', 'Auth\ConfirmEmailController@confirm');
 Route::post('/register', 'Auth\RegisterController@postRegister');
 
+// User invitation routes
+Route::get('/register/invite/{token}', 'Auth\UserInviteController@showSetPassword');
+Route::post('/register/invite/{token}', 'Auth\UserInviteController@setPassword');
+
 // Password reset link request routes...
 Route::get('/password/email', 'Auth\ForgotPasswordController@showLinkRequestForm');
 Route::post('/password/email', 'Auth\ForgotPasswordController@sendResetLinkEmail');
index c96a04f008ee21e260b28f7701595ed59e2839e3..869804c2ad0b113b8edfc31984e4e94c5dedcf0d 100755 (executable)
@@ -1,2 +1,3 @@
 *
+!data/
 !.gitignore
\ No newline at end of file
index 0399f2b818e809121ea45a4cc5fa0d04087d9d87..eb83faded44edc67d721e595d1a11d3861e12255 100644 (file)
@@ -1,8 +1,8 @@
 <?php namespace Tests;
 
+use BookStack\Auth\User;
 use BookStack\Entities\Page;
 use BookStack\Notifications\ConfirmEmail;
-use BookStack\Auth\User;
 use BookStack\Settings\SettingService;
 use Illuminate\Support\Facades\Notification;
 
@@ -81,7 +81,7 @@ class AuthTest extends BrowserKitTest
             ->press('Create Account')
             ->see('The name must be at least 2 characters.')
             ->see('The email must be a valid email address.')
-            ->see('The password must be at least 6 characters.')
+            ->see('The password must be at least 8 characters.')
             ->seePageIs('/register');
     }
 
@@ -276,6 +276,15 @@ class AuthTest extends BrowserKitTest
     public function test_user_cannot_be_deleted_if_last_admin()
     {
         $adminRole = \BookStack\Auth\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();
@@ -341,7 +350,7 @@ class AuthTest extends BrowserKitTest
         $page = Page::query()->first();
 
         $this->visit($page->getUrl())
-            ->seePageUrlIs(baseUrl('/login'));
+            ->seePageUrlIs(url('/login'));
         $this->login('[email protected]', 'password')
             ->seePageUrlIs($page->getUrl());
     }
index 5923ef377700734f931b6a56098bfe14a18e255d..fe28698dfac6b79013020c43fb82a93f1bdcb8fe 100644 (file)
@@ -15,7 +15,7 @@ class LdapTest extends BrowserKitTest
     protected $mockUser;
     protected $resourceId = 'resource-test';
 
-    public function setUp()
+    public function setUp(): void
     {
         parent::setUp();
         if (!defined('LDAP_OPT_REFERRALS')) define('LDAP_OPT_REFERRALS', 1);
index b8ca81174c3911663c08e703266640ebd1abb212..526c0e199a184cf7ee0a7f74c49fcea0bf42cbba 100644 (file)
@@ -153,7 +153,7 @@ class SocialAuthTest extends TestCase
         config()->set('services.google.select_account', 'true');
 
         $resp = $this->get('/login/service/google');
-        $this->assertContains('prompt=select_account', $resp->headers->get('Location'));
+        $this->assertStringContainsString('prompt=select_account', $resp->headers->get('Location'));
     }
 
 }
diff --git a/tests/Auth/UserInviteTest.php b/tests/Auth/UserInviteTest.php
new file mode 100644 (file)
index 0000000..d200134
--- /dev/null
@@ -0,0 +1,114 @@
+<?php namespace Tests;
+
+
+use BookStack\Auth\Access\UserInviteService;
+use BookStack\Auth\User;
+use BookStack\Notifications\UserInvite;
+use Carbon\Carbon;
+use DB;
+use Illuminate\Support\Str;
+use Notification;
+
+class UserInviteTest extends TestCase
+{
+
+    public function test_user_creation_creates_invite()
+    {
+        Notification::fake();
+        $admin = $this->getAdmin();
+
+        $this->actingAs($admin)->post('/settings/users/create', [
+            'name' => 'Barry',
+            'email' => '[email protected]',
+            'send_invite' => 'true',
+        ]);
+
+        $newUser = User::query()->where('email', '=', '[email protected]')->orderBy('id', 'desc')->first();
+
+        Notification::assertSentTo($newUser, UserInvite::class);
+        $this->assertDatabaseHas('user_invites', [
+            'user_id' => $newUser->id
+        ]);
+    }
+
+    public function test_invite_set_password()
+    {
+        Notification::fake();
+        $user = $this->getViewer();
+        $inviteService = app(UserInviteService::class);
+
+        $inviteService->sendInvitation($user);
+        $token = DB::table('user_invites')->where('user_id', '=', $user->id)->first()->token;
+
+        $setPasswordPageResp = $this->get('/register/invite/' . $token);
+        $setPasswordPageResp->assertSuccessful();
+        $setPasswordPageResp->assertSee('Welcome to BookStack!');
+        $setPasswordPageResp->assertSee('Password');
+        $setPasswordPageResp->assertSee('Confirm Password');
+
+        $setPasswordResp = $this->followingRedirects()->post('/register/invite/' . $token, [
+            'password' => 'my test password',
+        ]);
+        $setPasswordResp->assertSee('Password set, you now have access to BookStack!');
+        $newPasswordValid = auth()->validate([
+            'email' => $user->email,
+            'password' => 'my test password'
+        ]);
+        $this->assertTrue($newPasswordValid);
+        $this->assertDatabaseMissing('user_invites', [
+            'user_id' => $user->id
+        ]);
+    }
+
+    public function test_invite_set_has_password_validation()
+    {
+        Notification::fake();
+        $user = $this->getViewer();
+        $inviteService = app(UserInviteService::class);
+
+        $inviteService->sendInvitation($user);
+        $token = DB::table('user_invites')->where('user_id', '=', $user->id)->first()->token;
+
+        $this->get('/register/invite/' . $token);
+        $shortPassword = $this->followingRedirects()->post('/register/invite/' . $token, [
+            'password' => 'mypassw',
+        ]);
+        $shortPassword->assertSee('The password must be at least 8 characters.');
+
+        $this->get('/register/invite/' . $token);
+        $noPassword = $this->followingRedirects()->post('/register/invite/' . $token, [
+            'password' => '',
+        ]);
+        $noPassword->assertSee('The password field is required.');
+
+        $this->assertDatabaseHas('user_invites', [
+            'user_id' => $user->id
+        ]);
+    }
+
+    public function test_non_existent_invite_token_redirects_to_home()
+    {
+        $setPasswordPageResp = $this->get('/register/invite/' . Str::random(12));
+        $setPasswordPageResp->assertRedirect('/');
+
+        $setPasswordResp = $this->post('/register/invite/' . Str::random(12), ['password' => 'Password Test']);
+        $setPasswordResp->assertRedirect('/');
+    }
+
+    public function test_token_expires_after_two_weeks()
+    {
+        Notification::fake();
+        $user = $this->getViewer();
+        $inviteService = app(UserInviteService::class);
+
+        $inviteService->sendInvitation($user);
+        $tokenEntry = DB::table('user_invites')->where('user_id', '=', $user->id)->first();
+        DB::table('user_invites')->update(['created_at' => Carbon::now()->subDays(14)->subHour(1)]);
+
+        $setPasswordPageResp = $this->get('/register/invite/' . $tokenEntry->token);
+        $setPasswordPageResp->assertRedirect('/password/email');
+        $setPasswordPageResp->assertSessionHas('error', 'This invitation link has expired. You can instead try to reset your account password.');
+    }
+
+
+}
\ No newline at end of file
index ab0d9d898b31db9a9f167fc7a4b9131ffe722a1f..b81afe31106025352a3d5c50a23b2162b1ae6d5f 100644 (file)
@@ -21,7 +21,7 @@ abstract class BrowserKitTest extends TestCase
      */
     protected $baseUrl = 'http://localhost';
 
-    public function tearDown()
+    public function tearDown() : void
     {
         \DB::disconnect();
         parent::tearDown();
index a884809696d32cd4e4126f7c7c68121738ee905f..4aef0ed2658861b2fe8a6f326c12ea131e3888ee 100644 (file)
@@ -2,7 +2,6 @@
 
 use BookStack\Auth\Permissions\JointPermission;
 use BookStack\Entities\Page;
-use BookStack\Entities\Repos\EntityRepo;
 use BookStack\Auth\User;
 use BookStack\Entities\Repos\PageRepo;
 
@@ -56,7 +55,7 @@ class CommandsTest extends TestCase
         $this->asEditor();
         $pageRepo = app(PageRepo::class);
         $page = Page::first();
-        $pageRepo->updatePage($page, $page->book_id, ['name' => 'updated page', 'html' => '<p>new content</p>', 'summary' => 'page revision testing']);
+        $pageRepo->update($page, ['name' => 'updated page', 'html' => '<p>new content</p>', 'summary' => 'page revision testing']);
         $pageRepo->updatePageDraft($page, ['name' => 'updated page', 'html' => '<p>new content in draft</p>', 'summary' => 'page revision testing']);
 
         $this->assertDatabaseHas('page_revisions', [
index 158fb5ca184e2075e0cef15339bf71075576dc12..5c7673847140eb71d284e14d94d92921f6b706c6 100644 (file)
@@ -4,6 +4,7 @@ use BookStack\Auth\Role;
 use BookStack\Auth\User;
 use BookStack\Entities\Book;
 use BookStack\Entities\Bookshelf;
+use Illuminate\Support\Str;
 
 class BookShelfTest extends TestCase
 {
@@ -55,8 +56,8 @@ class BookShelfTest extends TestCase
     {
         $booksToInclude = Book::take(2)->get();
         $shelfInfo = [
-            'name' => 'My test book' . str_random(4),
-            'description' => 'Test book description ' . str_random(10)
+            'name' => 'My test book' . Str::random(4),
+            'description' => 'Test book description ' . Str::random(10)
         ];
         $resp = $this->asEditor()->post('/shelves', array_merge($shelfInfo, [
             'books' => $booksToInclude->implode('id', ','),
@@ -120,8 +121,8 @@ class BookShelfTest extends TestCase
 
         $booksToInclude = Book::take(2)->get();
         $shelfInfo = [
-            'name' => 'My test book' . str_random(4),
-            'description' => 'Test book description ' . str_random(10)
+            'name' => 'My test book' . Str::random(4),
+            'description' => 'Test book description ' . Str::random(10)
         ];
 
         $resp = $this->asEditor()->put($shelf->getUrl(), array_merge($shelfInfo, [
index 2683f57cb601767243b745edd31b2caf1ea4e457..967e550a735e6294faf970bcfc1a605b8a57c2f7 100644 (file)
@@ -3,7 +3,7 @@
 class CommentSettingTest extends BrowserKitTest {
   protected $page;
 
-  public function setUp() {
+  public function setUp(): void {
       parent::setUp();
       $this->page = \BookStack\Entities\Page::first();
   }
index a3fb1cfe11833591a5d1282aeda43fdce23f9eb9..b506da2aa62387126f3b5c69ffb678b0b6548fe6 100644 (file)
@@ -3,7 +3,6 @@
 use BookStack\Entities\Book;
 use BookStack\Entities\Chapter;
 use BookStack\Entities\Page;
-use BookStack\Entities\Repos\EntityRepo;
 use BookStack\Auth\UserRepo;
 use BookStack\Entities\Repos\PageRepo;
 use Carbon\Carbon;
@@ -192,7 +191,7 @@ class EntityTest extends BrowserKitTest
         $entities = $this->createEntityChainBelongingToUser($creator, $updater);
         $this->actingAs($creator);
         app(UserRepo::class)->destroy($creator);
-        app(PageRepo::class)->savePageRevision($entities['page']);
+        app(PageRepo::class)->update($entities['page'], ['html' => '<p>hello!</p>>']);
 
         $this->checkEntitiesViewable($entities);
     }
@@ -205,7 +204,7 @@ class EntityTest extends BrowserKitTest
         $entities = $this->createEntityChainBelongingToUser($creator, $updater);
         $this->actingAs($updater);
         app(UserRepo::class)->destroy($updater);
-        app(PageRepo::class)->savePageRevision($entities['page']);
+        app(PageRepo::class)->update($entities['page'], ['html' => '<p>Hello there!</p>']);
 
         $this->checkEntitiesViewable($entities);
     }
@@ -273,8 +272,7 @@ class EntityTest extends BrowserKitTest
 
     public function test_slug_multi_byte_lower_casing()
     {
-        $entityRepo = app(EntityRepo::class);
-        $book = $entityRepo->createFromInput('book', [
+        $book = $this->newBook([
             'name' => 'КНИГА'
         ]);
 
@@ -284,8 +282,7 @@ class EntityTest extends BrowserKitTest
 
     public function test_slug_format()
     {
-        $entityRepo = app(EntityRepo::class);
-        $book = $entityRepo->createFromInput('book', [
+        $book = $this->newBook([
             'name' => 'PartA / PartB / PartC'
         ]);
 
index fdcd833665d267f380631110fc2fc51a7a6b6f27..9a2d32028e4b26887c7d231b68df2b1887668e3a 100644 (file)
@@ -3,6 +3,8 @@
 
 use BookStack\Entities\Chapter;
 use BookStack\Entities\Page;
+use BookStack\Uploads\HttpFetcher;
+use Illuminate\Support\Str;
 
 class ExportTest extends TestCase
 {
@@ -76,6 +78,20 @@ class ExportTest extends TestCase
         $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.html"');
     }
 
+    public function test_book_html_export_shows_chapter_descriptions()
+    {
+        $chapterDesc = 'My custom test chapter description ' . Str::random(12);
+        $chapter = Chapter::query()->first();
+        $chapter->description = $chapterDesc;
+        $chapter->save();
+
+        $book = $chapter->book;
+        $this->asEditor();
+
+        $resp = $this->get($book->getUrl('/export/html'));
+        $resp->assertSee($chapterDesc);
+    }
+
     public function test_chapter_text_export()
     {
         $chapter = Chapter::first();
@@ -134,4 +150,17 @@ class ExportTest extends TestCase
         $resp->assertDontSee($page->updated_at->diffForHumans());
     }
 
+    public function test_page_export_sets_right_data_type_for_svg_embeds()
+    {
+        $page = Page::first();
+        $page->html = '<img src="http://example.com/image.svg">';
+        $page->save();
+
+        $this->asEditor();
+        $this->mockHttpFetch('<svg></svg>');
+        $resp = $this->get($page->getUrl('/export/html'));
+        $resp->assertStatus(200);
+        $resp->assertSee('<img src="data:image/svg+xml;base64');
+    }
+
 }
\ No newline at end of file
index c481e444f59cf563f25791bd3725bac74ee6df1c..5d3af4f6e26705b8fed3e617a78d9104fe3fb99d 100644 (file)
@@ -4,7 +4,7 @@ class MarkdownTest extends BrowserKitTest
 {
     protected $page;
 
-    public function setUp()
+    public function setUp(): void
     {
         parent::setUp();
         $this->page = \BookStack\Entities\Page::first();
index 6201cf5d7af005243128b80657cf347b0f5a6dcb..8a78c8ac019fd08de3cc37d50e7df50954c83f6e 100644 (file)
@@ -1,8 +1,7 @@
 <?php namespace Tests;
 
+use BookStack\Entities\Managers\PageContent;
 use BookStack\Entities\Page;
-use BookStack\Entities\Repos\EntityRepo;
-use BookStack\Entities\Repos\PageRepo;
 
 class PageContentTest extends TestCase
 {
@@ -50,7 +49,7 @@ class PageContentTest extends TestCase
         $resp->assertStatus(302);
 
         $page = Page::find($page->id);
-        $this->assertContains($includeTag, $page->html);
+        $this->assertStringContainsString($includeTag, $page->html);
         $this->assertEquals('', $page->text);
     }
 
@@ -80,10 +79,66 @@ class PageContentTest extends TestCase
         $page->save();
 
         $pageView = $this->get($page->getUrl());
+        $pageView->assertStatus(200);
         $pageView->assertDontSee($script);
         $pageView->assertSee('abc123abc123');
     }
 
+    public function test_more_complex_content_script_escaping_scenarios()
+    {
+        $checks = [
+            "<p>Some script</p><script>alert('cat')</script>",
+            "<div><div><div><div><p>Some script</p><script>alert('cat')</script></div></div></div></div>",
+            "<p>Some script<script>alert('cat')</script></p>",
+            "<p>Some script <div><script>alert('cat')</script></div></p>",
+            "<p>Some script <script><div>alert('cat')</script></div></p>",
+            "<p>Some script <script><div>alert('cat')</script><script><div>alert('cat')</script></p><script><div>alert('cat')</script>",
+        ];
+
+        $this->asEditor();
+        $page = Page::first();
+
+        foreach ($checks as $check) {
+            $page->html = $check;
+            $page->save();
+
+            $pageView = $this->get($page->getUrl());
+            $pageView->assertStatus(200);
+            $pageView->assertElementNotContains('.page-content', '<script>');
+            $pageView->assertElementNotContains('.page-content', '</script>');
+        }
+
+    }
+
+    public function test_iframe_js_and_base64_urls_are_removed()
+    {
+        $checks = [
+            '<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 srcdoc="<script>window.alert(document.cookie)</script>"></iframe>'
+        ];
+
+        $this->asEditor();
+        $page = Page::first();
+
+        foreach ($checks as $check) {
+            $page->html = $check;
+            $page->save();
+
+            $pageView = $this->get($page->getUrl());
+            $pageView->assertStatus(200);
+            $pageView->assertElementNotContains('.page-content', '<iframe>');
+            $pageView->assertElementNotContains('.page-content', '</iframe>');
+            $pageView->assertElementNotContains('.page-content', 'src=');
+            $pageView->assertElementNotContains('.page-content', 'javascript:');
+            $pageView->assertElementNotContains('.page-content', 'data:');
+            $pageView->assertElementNotContains('.page-content', 'base64');
+        }
+
+    }
+
     public function test_page_inline_on_attributes_removed_by_default()
     {
         $this->asEditor();
@@ -93,10 +148,36 @@ class PageContentTest extends TestCase
         $page->save();
 
         $pageView = $this->get($page->getUrl());
+        $pageView->assertStatus(200);
         $pageView->assertDontSee($script);
         $pageView->assertSee('<p>Hello</p>');
     }
 
+    public function test_more_complex_inline_on_attributes_escaping_scenarios()
+    {
+        $checks = [
+            '<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)\'> ',
+        ];
+
+        $this->asEditor();
+        $page = Page::first();
+
+        foreach ($checks as $check) {
+            $page->html = $check;
+            $page->save();
+
+            $pageView = $this->get($page->getUrl());
+            $pageView->assertStatus(200);
+            $pageView->assertElementNotContains('.page-content', 'onclick');
+        }
+
+    }
+
     public function test_page_content_scripts_show_when_configured()
     {
         $this->asEditor();
@@ -160,4 +241,66 @@ class PageContentTest extends TestCase
         $updatedPage = Page::where('id', '=', $page->id)->first();
         $this->assertEquals(substr_count($updatedPage->html, "bkmrk-test\""), 1);
     }
+
+    public function test_get_page_nav_sets_correct_properties()
+    {
+        $content = '<h1 id="testa">Hello</h1><h2 id="testb">There</h2><h3 id="testc">Donkey</h3>';
+        $pageContent = new PageContent(new Page(['html' => $content]));
+        $navMap = $pageContent->getNavigation($content);
+
+        $this->assertCount(3, $navMap);
+        $this->assertArrayMapIncludes([
+            'nodeName' => 'h1',
+            'link' => '#testa',
+            'text' => 'Hello',
+            'level' => 1,
+        ], $navMap[0]);
+        $this->assertArrayMapIncludes([
+            'nodeName' => 'h2',
+            'link' => '#testb',
+            'text' => 'There',
+            'level' => 2,
+        ], $navMap[1]);
+        $this->assertArrayMapIncludes([
+            'nodeName' => 'h3',
+            'link' => '#testc',
+            'text' => 'Donkey',
+            'level' => 3,
+        ], $navMap[2]);
+    }
+
+    public function test_get_page_nav_does_not_show_empty_titles()
+    {
+        $content = '<h1 id="testa">Hello</h1><h2 id="testb">&nbsp;</h2><h3 id="testc"></h3>';
+        $pageContent = new PageContent(new Page(['html' => $content]));
+        $navMap = $pageContent->getNavigation($content);
+
+        $this->assertCount(1, $navMap);
+        $this->assertArrayMapIncludes([
+            'nodeName' => 'h1',
+            'link' => '#testa',
+            'text' => 'Hello'
+        ], $navMap[0]);
+    }
+
+    public function test_get_page_nav_shifts_headers_if_only_smaller_ones_are_used()
+    {
+        $content = '<h4 id="testa">Hello</h4><h5 id="testb">There</h5><h6 id="testc">Donkey</h6>';
+        $pageContent = new PageContent(new Page(['html' => $content]));
+        $navMap = $pageContent->getNavigation($content);
+
+        $this->assertCount(3, $navMap);
+        $this->assertArrayMapIncludes([
+            'nodeName' => 'h4',
+            'level' => 1,
+        ], $navMap[0]);
+        $this->assertArrayMapIncludes([
+            'nodeName' => 'h5',
+            'level' => 2,
+        ], $navMap[1]);
+        $this->assertArrayMapIncludes([
+            'nodeName' => 'h6',
+            'level' => 3,
+        ], $navMap[2]);
+    }
 }
index f15651f39b672824520bbdb096962cc2fe81b256..e83f78a10a8a8f2d566ece9586a115dd24d4c323 100644 (file)
@@ -1,14 +1,17 @@
 <?php namespace Tests;
 
-
 use BookStack\Entities\Repos\PageRepo;
 
 class PageDraftTest extends BrowserKitTest
 {
     protected $page;
+
+    /**
+     * @var PageRepo
+     */
     protected $pageRepo;
 
-    public function setUp()
+    public function setUp(): void
     {
         parent::setUp();
         $this->page = \BookStack\Entities\Page::first();
@@ -18,25 +21,26 @@ class PageDraftTest extends BrowserKitTest
     public function test_draft_content_shows_if_available()
     {
         $addedContent = '<p>test message content</p>';
-        $this->asAdmin()->visit($this->page->getUrl() . '/edit')
+        $this->asAdmin()->visit($this->page->getUrl('/edit'))
             ->dontSeeInField('html', $addedContent);
 
         $newContent = $this->page->html . $addedContent;
         $this->pageRepo->updatePageDraft($this->page, ['html' => $newContent]);
-        $this->asAdmin()->visit($this->page->getUrl() . '/edit')
+        $this->asAdmin()->visit($this->page->getUrl('/edit'))
             ->seeInField('html', $newContent);
     }
 
     public function test_draft_not_visible_by_others()
     {
         $addedContent = '<p>test message content</p>';
-        $this->asAdmin()->visit($this->page->getUrl() . '/edit')
+        $this->asAdmin()->visit($this->page->getUrl('/edit'))
             ->dontSeeInField('html', $addedContent);
 
         $newContent = $this->page->html . $addedContent;
         $newUser = $this->getEditor();
         $this->pageRepo->updatePageDraft($this->page, ['html' => $newContent]);
-        $this->actingAs($newUser)->visit($this->page->getUrl() . '/edit')
+
+        $this->actingAs($newUser)->visit($this->page->getUrl('/edit'))
             ->dontSeeInField('html', $newContent);
     }
 
@@ -44,7 +48,7 @@ class PageDraftTest extends BrowserKitTest
     {
         $this->asAdmin();
         $this->pageRepo->updatePageDraft($this->page, ['html' => 'test content']);
-        $this->asAdmin()->visit($this->page->getUrl() . '/edit')
+        $this->asAdmin()->visit($this->page->getUrl('/edit'))
             ->see('You are currently editing a draft');
     }
 
@@ -52,7 +56,7 @@ class PageDraftTest extends BrowserKitTest
     {
         $nonEditedPage = \BookStack\Entities\Page::take(10)->get()->last();
         $addedContent = '<p>test message content</p>';
-        $this->asAdmin()->visit($this->page->getUrl() . '/edit')
+        $this->asAdmin()->visit($this->page->getUrl('/edit'))
             ->dontSeeInField('html', $addedContent);
 
         $newContent = $this->page->html . $addedContent;
@@ -60,7 +64,7 @@ class PageDraftTest extends BrowserKitTest
         $this->pageRepo->updatePageDraft($this->page, ['html' => $newContent]);
 
         $this->actingAs($newUser)
-            ->visit($this->page->getUrl() . '/edit')
+            ->visit($this->page->getUrl('/edit'))
             ->see('Admin has started editing this page');
             $this->flushSession();
         $this->visit($nonEditedPage->getUrl() . '/edit')
@@ -84,11 +88,11 @@ class PageDraftTest extends BrowserKitTest
         $newUser = $this->getEditor();
 
         $this->actingAs($newUser)->visit('/')
-            ->visit($book->getUrl() . '/create-page')
-            ->visit($chapter->getUrl() . '/create-page')
+            ->visit($book->getUrl('/create-page'))
+            ->visit($chapter->getUrl('/create-page'))
             ->visit($book->getUrl())
             ->seeInElement('.book-contents', 'New Page');
-        
+
         $this->asAdmin()
             ->visit($book->getUrl())
             ->dontSeeInElement('.book-contents', 'New Page')
index 521ea79a4760ee6bcfa30c3497b8882f1a267ddb..38193ec1a6b7bd09cadbe04e6a8bcb3ea8765f98 100644 (file)
@@ -12,7 +12,7 @@ class PageRevisionTest extends TestCase
 
         $pageRepo = app(PageRepo::class);
         $page = Page::first();
-        $pageRepo->updatePage($page, $page->book_id, ['name' => 'updated page', 'html' => '<p>new content</p>', 'summary' => 'page revision testing']);
+        $pageRepo->update($page, ['name' => 'updated page', 'html' => '<p>new content</p>', 'summary' => 'page revision testing']);
         $pageRevision = $page->revisions->last();
 
         $revisionView = $this->get($page->getUrl() . '/revisions/' . $pageRevision->id);
@@ -30,8 +30,8 @@ class PageRevisionTest extends TestCase
 
         $pageRepo = app(PageRepo::class);
         $page = Page::first();
-        $pageRepo->updatePage($page, $page->book_id, ['name' => 'updated page abc123', 'html' => '<p>new contente def456</p>', 'summary' => 'initial page revision testing']);
-        $pageRepo->updatePage($page, $page->book_id, ['name' => 'updated page again', 'html' => '<p>new content</p>', 'summary' => 'page revision testing']);
+        $pageRepo->update($page, ['name' => 'updated page abc123', 'html' => '<p>new contente def456</p>', 'summary' => 'initial page revision testing']);
+        $pageRepo->update($page, ['name' => 'updated page again', 'html' => '<p>new content</p>', 'summary' => 'page revision testing']);
         $page =  Page::find($page->id);
 
 
@@ -87,7 +87,7 @@ class PageRevisionTest extends TestCase
         // Delete the first revision
         $revision = $page->revisions->get(1);
         $resp = $this->asEditor()->delete($revision->getUrl('/delete/'));
-        $resp->assertStatus(200);
+        $resp->assertRedirect($page->getUrl('/revisions'));
 
         $page = Page::find($page->id);
         $afterRevisionCount = $page->revisions->count();
@@ -98,7 +98,7 @@ class PageRevisionTest extends TestCase
         $beforeRevisionCount = $page->revisions->count();
         $currentRevision = $page->getCurrentRevision();
         $resp = $this->asEditor()->delete($currentRevision->getUrl('/delete/'));
-        $resp->assertStatus(400);
+        $resp->assertRedirect($page->getUrl('/revisions'));
 
         $page = Page::find($page->id);
         $afterRevisionCount = $page->revisions->count();
diff --git a/tests/Entity/PageTemplateTest.php b/tests/Entity/PageTemplateTest.php
new file mode 100644 (file)
index 0000000..883de4a
--- /dev/null
@@ -0,0 +1,90 @@
+<?php namespace Entity;
+
+use BookStack\Entities\Page;
+use Tests\TestCase;
+
+class PageTemplateTest extends TestCase
+{
+    public function test_active_templates_visible_on_page_view()
+    {
+        $page = Page::first();
+
+        $this->asEditor();
+        $templateView = $this->get($page->getUrl());
+        $templateView->assertDontSee('Page Template');
+
+        $page->template = true;
+        $page->save();
+
+        $templateView = $this->get($page->getUrl());
+        $templateView->assertSee('Page Template');
+    }
+
+    public function test_manage_templates_permission_required_to_change_page_template_status()
+    {
+        $page = Page::first();
+        $editor = $this->getEditor();
+        $this->actingAs($editor);
+
+        $pageUpdateData = [
+            'name' => $page->name,
+            'html' => $page->html,
+            'template' => 'true',
+        ];
+
+        $this->put($page->getUrl(), $pageUpdateData);
+        $this->assertDatabaseHas('pages', [
+            'id' => $page->id,
+            'template' => false,
+        ]);
+
+        $this->giveUserPermissions($editor, ['templates-manage']);
+
+        $this->put($page->getUrl(), $pageUpdateData);
+        $this->assertDatabaseHas('pages', [
+            'id' => $page->id,
+            'template' => true,
+        ]);
+    }
+
+    public function test_templates_content_should_be_fetchable_only_if_page_marked_as_template()
+    {
+        $content = '<div>my_custom_template_content</div>';
+        $page = Page::first();
+        $editor = $this->getEditor();
+        $this->actingAs($editor);
+
+        $templateFetch = $this->get('/templates/' . $page->id);
+        $templateFetch->assertStatus(404);
+
+        $page->html = $content;
+        $page->template = true;
+        $page->save();
+
+        $templateFetch = $this->get('/templates/' . $page->id);
+        $templateFetch->assertStatus(200);
+        $templateFetch->assertJson([
+            'html' => $content,
+            'markdown' => '',
+        ]);
+    }
+
+    public function test_template_endpoint_returns_paginated_list_of_templates()
+    {
+        $editor = $this->getEditor();
+        $this->actingAs($editor);
+
+        $toBeTemplates = Page::query()->orderBy('name', 'asc')->take(12)->get();
+        $page = $toBeTemplates->first();
+
+        $emptyTemplatesFetch = $this->get('/templates');
+        $emptyTemplatesFetch->assertDontSee($page->name);
+
+        Page::query()->whereIn('id', $toBeTemplates->pluck('id')->toArray())->update(['template' => true]);
+
+        $templatesFetch = $this->get('/templates');
+        $templatesFetch->assertSee($page->name);
+        $templatesFetch->assertSee('pagination');
+    }
+
+}
\ No newline at end of file
index a3c20e84c5b517bede1e3b3488516a299c1e853a..3c83d626aea73632dc93a5d08274a1a4d6e420da 100644 (file)
@@ -1,6 +1,5 @@
 <?php namespace Tests;
 
-use BookStack\Auth\Role;
 use BookStack\Entities\Book;
 use BookStack\Entities\Chapter;
 use BookStack\Entities\Page;
@@ -10,7 +9,7 @@ class SortTest extends TestCase
 {
     protected $book;
 
-    public function setUp()
+    public function setUp(): void
     {
         parent::setUp();
         $this->book = Book::first();
@@ -20,7 +19,7 @@ class SortTest extends TestCase
     {
         $this->asAdmin();
         $pageRepo = app(PageRepo::class);
-        $draft = $pageRepo->getDraftPage($this->book);
+        $draft = $pageRepo->getNewDraftPage($this->book);
 
         $resp = $this->get($this->book->getUrl());
         $resp->assertSee($draft->name);
@@ -214,7 +213,6 @@ class SortTest extends TestCase
             'entity_selection' => 'book:' . $newBook->id,
             'name' => 'My copied test page'
         ]);
-
         $pageCopy = Page::where('name', '=', 'My copied test page')->first();
 
         $movePageResp->assertRedirect($pageCopy->getUrl());
index 70b8c960b73980544b74356d2666c0cd89fee067..13876410a0cddccaf4e3d04783e5735ccfd78697 100644 (file)
@@ -3,6 +3,7 @@
 use BookStack\Entities\Book;
 use BookStack\Entities\Chapter;
 use BookStack\Actions\Tag;
+use BookStack\Entities\Entity;
 use BookStack\Entities\Page;
 use BookStack\Auth\Permissions\PermissionService;
 
@@ -14,9 +15,9 @@ class TagTest extends BrowserKitTest
     /**
      * Get an instance of a page that has many tags.
      * @param \BookStack\Actions\Tag[]|bool $tags
-     * @return mixed
+     * @return Entity
      */
-    protected function getEntityWithTags($class, $tags = false)
+    protected function getEntityWithTags($class, $tags = false): Entity
     {
         $entity = $class::first();
 
@@ -122,7 +123,7 @@ class TagTest extends BrowserKitTest
         // Set restricted permission the page
         $page->restricted = true;
         $page->save();
-        $permissionService->buildJointPermissionsForEntity($page);
+        $page->rebuildPermissions();
 
         $this->asAdmin()->get('/ajax/tags/suggest/names?search=co')->seeJsonEquals(['color', 'country']);
         $this->asEditor()->get('/ajax/tags/suggest/names?search=co')->seeJsonEquals([]);
index 286d4cf60aca6a3d06cb24d6d65be9580bac6b95..ada1f5aafde22b4929d31166be3d156a7c98e053 100644 (file)
@@ -38,10 +38,14 @@ class HomepageTest extends TestCase
         $name = 'My custom homepage';
         $content = str_repeat('This is the body content of my custom homepage.', 20);
         $customPage = $this->newPage(['name' => $name, 'html' => $content]);
-        $this->setSettings(['app-homepage' => $customPage->id]);
+        $this->setSettings([
+            'app-homepage' => $customPage->id,
+            'app-homepage-type' => 'page'
+        ]);
 
         $homeVisit = $this->get('/');
         $homeVisit->assertSee($name);
+        $homeVisit->assertElementNotExists('#home-default');
 
         $pageDeleteReq = $this->delete($customPage->getUrl());
         $pageDeleteReq->assertStatus(302);
@@ -54,6 +58,23 @@ class HomepageTest extends TestCase
         $homeVisit->assertStatus(200);
     }
 
+    public function test_custom_homepage_can_be_deleted_once_custom_homepage_no_longer_used()
+    {
+        $this->asEditor();
+        $name = 'My custom homepage';
+        $content = str_repeat('This is the body content of my custom homepage.', 20);
+        $customPage = $this->newPage(['name' => $name, 'html' => $content]);
+        $this->setSettings([
+            'app-homepage' => $customPage->id,
+            'app-homepage-type' => 'default'
+        ]);
+
+        $pageDeleteReq = $this->delete($customPage->getUrl());
+        $pageDeleteReq->assertStatus(302);
+        $pageDeleteReq->assertSessionHas('success');
+        $pageDeleteReq->assertSessionMissing('error');
+    }
+
     public function test_set_book_homepage()
     {
         $editor = $this->getEditor();
index 91abadc91904a97cf8bd14c425ab01f854f9350c..c8bc4445117a5819fe3c7561f043bd742b5d7182 100644 (file)
@@ -8,7 +8,7 @@ class LanguageTest extends TestCase
     /**
      * LanguageTest constructor.
      */
-    public function setUp()
+    public function setUp(): void
     {
         parent::setUp();
         $this->langs = array_diff(scandir(resource_path('lang')), ['..', '.', 'check.php', 'format.php']);
@@ -41,21 +41,6 @@ class LanguageTest extends TestCase
         $loginPageFrenchReq->assertDontSee('Se Connecter');
     }
 
-    public function test_js_endpoint_for_each_language()
-    {
-
-        $visibleKeys = ['common', 'components', 'entities', 'errors'];
-
-        $this->asEditor();
-        foreach ($this->langs as $lang) {
-            setting()->putUser($this->getEditor(), 'language', $lang);
-            $transResp = $this->get('/translations');
-            foreach ($visibleKeys as $key) {
-                $transResp->assertSee($key);
-            }
-        }
-    }
-
     public function test_all_lang_files_loadable()
     {
         $files = array_diff(scandir(resource_path('lang/en')), ['..', '.']);
@@ -98,13 +83,4 @@ class LanguageTest extends TestCase
         $this->assertNotEquals($enEmailActionHelp, $deInformalEmailActionHelp);
     }
 
-    public function test_de_informal_falls_base_to_de_in_js_endpoint()
-    {
-        $this->asEditor();
-        setting()->putUser($this->getEditor(), 'language', 'de_informal');
-
-        $transResp = $this->get('/translations');
-        $transResp->assertSee('"cancel":"Abbrechen"');
-    }
-
 }
\ No newline at end of file
index a7f681a37d1597d72a254d4e7b549e8c2e310202..d899c6396041a9d8a46d695efff0599ce0159a83 100644 (file)
@@ -5,14 +5,13 @@ use BookStack\Entities\Bookshelf;
 use BookStack\Entities\Chapter;
 use BookStack\Entities\Entity;
 use BookStack\Auth\User;
-use BookStack\Entities\Repos\EntityRepo;
 use BookStack\Entities\Page;
 
 class RestrictionsTest extends BrowserKitTest
 {
 
     /**
-     * @var \BookStack\Auth\User
+     * @var User
      */
     protected $user;
 
@@ -21,7 +20,7 @@ class RestrictionsTest extends BrowserKitTest
      */
     protected $viewer;
 
-    public function setUp()
+    public function setUp(): void
     {
         parent::setUp();
         $this->user = $this->getEditor();
@@ -327,7 +326,7 @@ class RestrictionsTest extends BrowserKitTest
 
     public function test_page_view_restriction()
     {
-        $page = \BookStack\Entities\Page::first();
+        $page = Page::first();
 
         $pageUrl = $page->getUrl();
         $this->actingAs($this->user)
@@ -367,7 +366,7 @@ class RestrictionsTest extends BrowserKitTest
 
     public function test_page_delete_restriction()
     {
-        $page = \BookStack\Entities\Page::first();
+        $page = Page::first();
 
         $pageUrl = $page->getUrl();
         $this->actingAs($this->user)
@@ -438,7 +437,7 @@ class RestrictionsTest extends BrowserKitTest
 
     public function test_page_restriction_form()
     {
-        $page = \BookStack\Entities\Page::first();
+        $page = Page::first();
         $this->asAdmin()->visit($page->getUrl() . '/permissions')
             ->see('Page Permissions')
             ->check('restricted')
@@ -665,10 +664,8 @@ class RestrictionsTest extends BrowserKitTest
         $this->setEntityRestrictions($firstBook, ['view', 'update']);
         $this->setEntityRestrictions($secondBook, ['view']);
 
-        $firstBookChapter = $this->app[EntityRepo::class]->createFromInput('chapter',
-                ['name' => 'first book chapter'], $firstBook);
-        $secondBookChapter = $this->app[EntityRepo::class]->createFromInput('chapter',
-                ['name' => 'second book chapter'], $secondBook);
+        $firstBookChapter = $this->newChapter(['name' => 'first book chapter'], $firstBook);
+        $secondBookChapter = $this->newChapter(['name' => 'second book chapter'], $secondBook);
 
         // Create request data
         $reqData = [
index 5bbdcf0bbb60c5f0c8ecf15d04f2285bbab5f7f5..371cffc0f3bd1be95a263c2bffd4c5f4277bb8f2 100644 (file)
@@ -11,7 +11,7 @@ class RolesTest extends BrowserKitTest
 {
     protected $user;
 
-    public function setUp()
+    public function setUp(): void
     {
         parent::setUp();
         $this->user = $this->getViewer();
@@ -119,6 +119,43 @@ class RolesTest extends BrowserKitTest
         $this->actingAs($this->user)->visit('/')->dontSee($usersLink);
     }
 
+    public function test_user_cannot_change_email_unless_they_have_manage_users_permission()
+    {
+        $userProfileUrl = '/settings/users/' . $this->user->id;
+        $originalEmail = $this->user->email;
+        $this->actingAs($this->user);
+
+        $this->visit($userProfileUrl)
+            ->assertResponseOk()
+            ->seeElement('input[name=email][disabled]');
+        $this->put($userProfileUrl, [
+            'name' => 'my_new_name',
+            'email' => '[email protected]',
+        ]);
+        $this->seeInDatabase('users', [
+            'id' => $this->user->id,
+            'email' => $originalEmail,
+            'name' => 'my_new_name',
+        ]);
+
+        $this->giveUserPermissions($this->user, ['users-manage']);
+
+        $this->visit($userProfileUrl)
+            ->assertResponseOk()
+            ->dontSeeElement('input[name=email][disabled]')
+            ->seeElement('input[name=email]');
+        $this->put($userProfileUrl, [
+            'name' => 'my_new_name_2',
+            'email' => '[email protected]',
+        ]);
+
+        $this->seeInDatabase('users', [
+            'id' => $this->user->id,
+            'email' => '[email protected]',
+            'name' => 'my_new_name_2',
+        ]);
+    }
+
     public function test_user_roles_manage_permission()
     {
         $this->actingAs($this->user)->visit('/settings/roles')
index 8e903be11a3089ed25a3c1ed77c7890924b25657..3433f3b832f86d61411729ea23983905596d57d0 100644 (file)
@@ -1,16 +1,23 @@
 <?php namespace Tests;
 
+use BookStack\Auth\User;
 use BookStack\Entities\Book;
 use BookStack\Entities\Bookshelf;
 use BookStack\Entities\Chapter;
 use BookStack\Entities\Entity;
 use BookStack\Entities\Page;
-use BookStack\Entities\Repos\EntityRepo;
+use BookStack\Entities\Repos\BookRepo;
+use BookStack\Entities\Repos\BookshelfRepo;
+use BookStack\Entities\Repos\ChapterRepo;
 use BookStack\Auth\Permissions\PermissionsRepo;
 use BookStack\Auth\Role;
 use BookStack\Auth\Permissions\PermissionService;
 use BookStack\Entities\Repos\PageRepo;
 use BookStack\Settings\SettingService;
+use BookStack\Uploads\HttpFetcher;
+use Illuminate\Support\Env;
+use Mockery;
+use Throwable;
 
 trait SharedTestHelpers
 {
@@ -68,7 +75,7 @@ trait SharedTestHelpers
      */
     protected function getViewer($attributes = [])
     {
-        $user = \BookStack\Auth\Role::getRole('viewer')->users()->first();
+        $user = Role::getRole('viewer')->users()->first();
         if (!empty($attributes)) $user->forceFill($attributes)->save();
         return $user;
     }
@@ -76,20 +83,21 @@ trait SharedTestHelpers
     /**
      * Regenerate the permission for an entity.
      * @param Entity $entity
+     * @throws Throwable
      */
     protected function regenEntityPermissions(Entity $entity)
     {
-        app(PermissionService::class)->buildJointPermissionsForEntity($entity);
+        $entity->rebuildPermissions();
         $entity->load('jointPermissions');
     }
 
     /**
      * Create and return a new bookshelf.
      * @param array $input
-     * @return \BookStack\Entities\Bookshelf
+     * @return Bookshelf
      */
     public function newShelf($input = ['name' => 'test shelf', 'description' => 'My new test shelf']) {
-        return app(EntityRepo::class)->createFromInput('bookshelf', $input, false);
+        return app(BookshelfRepo::class)->create($input, []);
     }
 
     /**
@@ -98,29 +106,30 @@ trait SharedTestHelpers
      * @return Book
      */
     public function newBook($input = ['name' => 'test book', 'description' => 'My new test book']) {
-        return app(EntityRepo::class)->createFromInput('book', $input, false);
+        return app(BookRepo::class)->create($input);
     }
 
     /**
      * Create and return a new test chapter
      * @param array $input
      * @param Book $book
-     * @return \BookStack\Entities\Chapter
+     * @return Chapter
      */
     public function newChapter($input = ['name' => 'test chapter', 'description' => 'My new test chapter'], Book $book) {
-        return app(EntityRepo::class)->createFromInput('chapter', $input, $book);
+        return app(ChapterRepo::class)->create($input, $book);
     }
 
     /**
      * Create and return a new test page
      * @param array $input
      * @return Page
+     * @throws Throwable
      */
     public function newPage($input = ['name' => 'test page', 'html' => 'My new test page']) {
         $book = Book::first();
         $pageRepo = app(PageRepo::class);
-        $draftPage = $pageRepo->getDraftPage($book);
-        return $pageRepo->publishPageDraft($draftPage, $input);
+        $draftPage = $pageRepo->getNewDraftPage($book);
+        return $pageRepo->publishDraft($draftPage, $input);
     }
 
     /**
@@ -165,10 +174,10 @@ trait SharedTestHelpers
 
     /**
      * Give the given user some permissions.
-     * @param \BookStack\Auth\User $user
+     * @param User $user
      * @param array $permissions
      */
-    protected function giveUserPermissions(\BookStack\Auth\User $user, $permissions = [])
+    protected function giveUserPermissions(User $user, $permissions = [])
     {
         $newRole = $this->createNewRole($permissions);
         $user->attachRole($newRole);
@@ -189,4 +198,68 @@ trait SharedTestHelpers
         return $permissionRepo->saveNewRole($roleData);
     }
 
+    /**
+     * Mock the HttpFetcher service and return the given data on fetch.
+     * @param $returnData
+     * @param int $times
+     */
+    protected function mockHttpFetch($returnData, int $times = 1)
+    {
+        $mockHttp = Mockery::mock(HttpFetcher::class);
+        $this->app[HttpFetcher::class] = $mockHttp;
+        $mockHttp->shouldReceive('fetch')
+            ->times($times)
+            ->andReturn($returnData);
+    }
+
+    /**
+     * Run a set test with the given env variable.
+     * Remembers the original and resets the value after test.
+     * @param string $name
+     * @param $value
+     * @param callable $callback
+     */
+    protected function runWithEnv(string $name, $value, callable $callback)
+    {
+        Env::disablePutenv();
+        $originalVal = $_SERVER[$name] ?? null;
+
+        if (is_null($value)) {
+            unset($_SERVER[$name]);
+        } else {
+            $_SERVER[$name] = $value;
+        }
+
+        $this->refreshApplication();
+        $callback();
+
+        if (is_null($originalVal)) {
+            unset($_SERVER[$name]);
+        } else {
+            $_SERVER[$name] = $originalVal;
+        }
+    }
+
+    /**
+     * Check the keys and properties in the given map to include
+     * exist, albeit not exclusively, within the map to check.
+     * @param array $mapToInclude
+     * @param array $mapToCheck
+     * @param string $message
+     */
+    protected function assertArrayMapIncludes(array $mapToInclude, array $mapToCheck, string $message = '') : void
+    {
+        $passed = true;
+
+        foreach ($mapToInclude as $key => $value) {
+            if (!isset($mapToCheck[$key]) || $mapToCheck[$key] !== $mapToInclude[$key]) {
+                $passed = false;
+            }
+        }
+
+        $toIncludeStr = print_r($mapToInclude, true);
+        $toCheckStr = print_r($mapToCheck, true);
+        self::assertThat($passed, self::isTrue(), "Failed asserting that given map:\n\n{$toCheckStr}\n\nincludes:\n\n{$toIncludeStr}");
+    }
+
 }
\ No newline at end of file
diff --git a/tests/Unit/ConfigTest.php b/tests/Unit/ConfigTest.php
new file mode 100644 (file)
index 0000000..c84305a
--- /dev/null
@@ -0,0 +1,53 @@
+<?php namespace Tests;
+
+/**
+ * Class ConfigTest
+ * Many of the tests here are to check on tweaks made
+ * to maintain backwards compatibility.
+ *
+ * @package Tests
+ */
+class ConfigTest extends TestCase
+{
+
+    public function test_filesystem_images_falls_back_to_storage_type_var()
+    {
+        $this->runWithEnv('STORAGE_TYPE', 'local_secure', function() {
+            $this->checkEnvConfigResult('STORAGE_IMAGE_TYPE', 's3', 'filesystems.images', 's3');
+            $this->checkEnvConfigResult('STORAGE_IMAGE_TYPE', null, 'filesystems.images', 'local_secure');
+        });
+    }
+
+    public function test_filesystem_attachments_falls_back_to_storage_type_var()
+    {
+        $this->runWithEnv('STORAGE_TYPE', 'local_secure', function() {
+            $this->checkEnvConfigResult('STORAGE_ATTACHMENT_TYPE', 's3', 'filesystems.attachments', 's3');
+            $this->checkEnvConfigResult('STORAGE_ATTACHMENT_TYPE', null, 'filesystems.attachments', 'local_secure');
+        });
+    }
+
+    public function test_app_url_blank_if_old_default_value()
+    {
+        $initUrl = 'https://example.com/docs';
+        $oldDefault = 'http://bookstack.dev';
+        $this->checkEnvConfigResult('APP_URL', $initUrl, 'app.url', $initUrl);
+        $this->checkEnvConfigResult('APP_URL', $oldDefault, 'app.url', '');
+    }
+
+    /**
+     * Set an environment variable of the given name and value
+     * then check the given config key to see if it matches the given result.
+     * Providing a null $envVal clears the variable.
+     * @param string $envName
+     * @param string|null $envVal
+     * @param string $configKey
+     * @param string $expectedResult
+     */
+    protected function checkEnvConfigResult(string $envName, $envVal, string $configKey, string $expectedResult)
+    {
+        $this->runWithEnv($envName, $envVal, function() use ($configKey, $expectedResult) {
+            $this->assertEquals($expectedResult, config($configKey));
+        });
+    }
+
+}
\ No newline at end of file
diff --git a/tests/Unit/HelpersTest.php b/tests/Unit/HelpersTest.php
deleted file mode 100644 (file)
index c8f4ce2..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-<?php namespace Tests;
-
-class HelpersTest extends TestCase
-{
-
-    public function test_base_url_takes_config_into_account()
-    {
-        config()->set('app.url', 'http://example.com/bookstack');
-        $result = baseUrl('/');
-        $this->assertEquals('http://example.com/bookstack/', $result);
-    }
-
-    public function test_base_url_takes_extra_path_into_account_on_forced_domain()
-    {
-        config()->set('app.url', 'http://example.com/bookstack');
-        $result = baseUrl('http://example.com/bookstack/', true);
-        $this->assertEquals('http://example.com/bookstack/', $result);
-    }
-
-    public function test_base_url_force_domain_works_as_expected_with_full_url_given()
-    {
-        config()->set('app.url', 'http://example.com');
-        $result = baseUrl('http://examps.com/books/test/page/cat', true);
-        $this->assertEquals('http://example.com/books/test/page/cat', $result);
-    }
-
-    public function test_base_url_force_domain_works_when_app_domain_is_same_as_given_url()
-    {
-        config()->set('app.url', 'http://example.com');
-        $result = baseUrl('http://example.com/books/test/page/cat', true);
-        $this->assertEquals('http://example.com/books/test/page/cat', $result);
-    }
-}
\ No newline at end of file
diff --git a/tests/Unit/PageRepoTest.php b/tests/Unit/PageRepoTest.php
deleted file mode 100644 (file)
index 36addcb..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-<?php
-namespace Tests;
-
-use BookStack\Entities\Repos\PageRepo;
-
-class PageRepoTest extends TestCase
-{
-    /**
-     * @var PageRepo $pageRepo
-     */
-    protected $pageRepo;
-
-    protected function setUp()
-    {
-        parent::setUp();
-        $this->pageRepo = app()->make(PageRepo::class);
-    }
-
-    public function test_get_page_nav_does_not_show_empty_titles()
-    {
-        $content = '<h1 id="testa">Hello</h1><h2 id="testb">&nbsp;</h2><h3 id="testc"></h3>';
-        $navMap = $this->pageRepo->getPageNav($content);
-
-        $this->assertCount(1, $navMap);
-        $this->assertArraySubset([
-            'nodeName' => 'h1',
-            'link' => '#testa',
-            'text' => 'Hello'
-        ], $navMap[0]);
-    }
-
-}
\ No newline at end of file
diff --git a/tests/Unit/UrlTest.php b/tests/Unit/UrlTest.php
new file mode 100644 (file)
index 0000000..c238644
--- /dev/null
@@ -0,0 +1,31 @@
+<?php namespace Tests;
+
+class UrlTest extends TestCase
+{
+
+    public function test_request_url_takes_custom_url_into_account()
+    {
+        config()->set('app.url', 'http://example.com/bookstack');
+        $this->get('/');
+        $this->assertEquals('http://example.com/bookstack', request()->getUri());
+
+        config()->set('app.url', 'http://example.com/docs/content');
+        $this->get('/');
+        $this->assertEquals('http://example.com/docs/content', request()->getUri());
+    }
+
+    public function test_url_helper_takes_custom_url_into_account()
+    {
+        $this->runWithEnv('APP_URL', 'http://example.com/bookstack', function() {
+            $this->assertEquals('http://example.com/bookstack/books', url('/books'));
+        });
+    }
+
+    public function test_url_helper_sets_correct_scheme_even_when_request_scheme_is_different()
+    {
+        $this->runWithEnv('APP_URL', 'https://example.com/', function() {
+            $this->get('http://example.com/login')->assertSee('https://example.com/dist/styles.css');
+        });
+    }
+
+}
\ No newline at end of file
index 35ffda821ef35262f1b135fcde4c8cbc90c7a519..12b254d00a4c84ac87877d8e3e49917163f7c071 100644 (file)
@@ -78,7 +78,7 @@ class AttachmentTest extends TestCase
         $upload->assertStatus(200);
 
         $attachment = Attachment::query()->orderBy('id', 'desc')->first();
-        $this->assertNotContains($fileName, $attachment->path);
+        $this->assertStringNotContainsString($fileName, $attachment->path);
         $this->assertStringEndsWith('.txt', $attachment->path);
     }
 
@@ -223,7 +223,7 @@ class AttachmentTest extends TestCase
     {
         $admin = $this->getAdmin();
         $viewer = $this->getViewer();
-        $page = Page::first();
+        $page = Page::first(); /** @var Page $page */
 
         $this->actingAs($admin);
         $fileName = 'permission_test.txt';
@@ -233,7 +233,7 @@ class AttachmentTest extends TestCase
         $page->restricted = true;
         $page->permissions()->delete();
         $page->save();
-        $this->app[PermissionService::class]->buildJointPermissionsForEntity($page);
+        $page->rebuildPermissions();
         $page->load('jointPermissions');
 
         $this->actingAs($viewer);
index 01bf23d5b2b3ba2ab9a631d9ef151e101ff94ada..0615a95ce52641cff442ce913d59bb65f812d2b3 100644 (file)
@@ -4,6 +4,7 @@ use BookStack\Entities\Repos\PageRepo;
 use BookStack\Uploads\Image;
 use BookStack\Entities\Page;
 use BookStack\Uploads\ImageService;
+use Illuminate\Support\Str;
 use Tests\TestCase;
 
 class ImageTest extends TestCase
@@ -43,7 +44,7 @@ class ImageTest extends TestCase
         $imgDetails = $this->uploadGalleryImage();
         $image = Image::query()->first();
 
-        $newName = str_random();
+        $newName = Str::random();
         $update = $this->put('/images/' . $image->id, ['name' => $newName]);
         $update->assertSuccessful();
         $update->assertJson([
@@ -89,7 +90,7 @@ class ImageTest extends TestCase
         $searchHitRequest = $this->get("/images/gallery?page=1&uploaded_to={$pageId}&search={$namePartial}");
         $searchHitRequest->assertSuccessful()->assertJson($resultJson);
 
-        $namePartial = str_random(16);
+        $namePartial = Str::random(16);
         $searchHitRequest = $this->get("/images/gallery?page=1&uploaded_to={$pageId}&search={$namePartial}");
         $searchHitRequest->assertSuccessful()->assertExactJson($emptyJson);
     }
@@ -176,7 +177,7 @@ class ImageTest extends TestCase
 
     public function test_secure_images_uploads_to_correct_place()
     {
-        config()->set('filesystems.default', 'local_secure');
+        config()->set('filesystems.images', 'local_secure');
         $this->asEditor();
         $galleryFile = $this->getTestImage('my-secure-test-upload.png');
         $page = Page::first();
@@ -194,7 +195,7 @@ class ImageTest extends TestCase
 
     public function test_secure_images_included_in_exports()
     {
-        config()->set('filesystems.default', 'local_secure');
+        config()->set('filesystems.images', 'local_secure');
         $this->asEditor();
         $galleryFile = $this->getTestImage('my-secure-test-upload.png');
         $page = Page::first();
@@ -208,7 +209,7 @@ class ImageTest extends TestCase
 
         $encodedImageContent = base64_encode(file_get_contents($expectedPath));
         $export = $this->get($page->getUrl('/export/html'));
-        $this->assertTrue(str_contains($export->getContent(), $encodedImageContent), 'Uploaded image in export content');
+        $this->assertTrue(strpos($export->getContent(), $encodedImageContent) !== false, 'Uploaded image in export content');
 
         if (file_exists($expectedPath)) {
             unlink($expectedPath);
@@ -217,7 +218,7 @@ class ImageTest extends TestCase
 
     public function test_system_images_remain_public()
     {
-        config()->set('filesystems.default', 'local_secure');
+        config()->set('filesystems.images', 'local_secure');
         $this->asAdmin();
         $galleryFile = $this->getTestImage('my-system-test-upload.png');
         $expectedPath = public_path('uploads/images/system/' . Date('Y-m') . '/my-system-test-upload.png');
@@ -366,7 +367,7 @@ class ImageTest extends TestCase
         $image = Image::where('type', '=', 'gallery')->first();
 
         $pageRepo = app(PageRepo::class);
-        $pageRepo->updatePage($page, $page->book_id, [
+        $pageRepo->update($page, [
             'name' => $page->name,
             'html' => $page->html . "<img src=\"{$image->url}\">",
             'summary' => ''
@@ -378,7 +379,7 @@ class ImageTest extends TestCase
         $this->assertCount(0, $toDelete);
 
         // Save a revision of our page without the image;
-        $pageRepo->updatePage($page, $page->book_id, [
+        $pageRepo->update($page, [
             'name' => $page->name,
             'html' => "<p>Hello</p>",
             'summary' => ''
index a7c7505a80db83867bdfb9c1636653589ccbe46a..fc1a529ae43076f59d38d2d642c534c0ff816cd7 100644 (file)
@@ -4,7 +4,7 @@ class UserProfileTest extends BrowserKitTest
 {
     protected $user;
 
-    public function setUp()
+    public function setUp(): void
     {
         parent::setUp();
         $this->user = \BookStack\Auth\User::all()->last();
diff --git a/version b/version
index a361484193e655a868708275458bacbb55113650..d400403770b07fdcd3eb850c6b28c62b7c1a5d83 100644 (file)
--- a/version
+++ b/version
@@ -1 +1 @@
-v0.26-dev
+v0.28-dev
index 4eae9b9be34683eec0f4efd08f039762e556c6ac..e496340c46c63a4409d3b51702a3de98c54f7459 100644 (file)
@@ -1,17 +1,16 @@
 const path = require('path');
 const dev = process.env.NODE_ENV !== 'production';
 
-const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
 const MiniCssExtractPlugin = require("mini-css-extract-plugin");
 
 const config = {
     target: 'web',
     mode: dev? 'development' : 'production',
     entry: {
-        app: './resources/assets/js/index.js',
-        styles: './resources/assets/sass/styles.scss',
-        "export-styles": './resources/assets/sass/export-styles.scss',
-        "print-styles": './resources/assets/sass/print-styles.scss',
+        app: './resources/js/index.js',
+        styles: './resources/sass/styles.scss',
+        "export-styles": './resources/sass/export-styles.scss',
+        "print-styles": './resources/sass/print-styles.scss',
     },
     output: {
         filename: '[name].js',
@@ -19,20 +18,6 @@ const config = {
     },
     module: {
         rules: [
-            {
-                test: /\.js$/,
-                exclude: /(node_modules)/,
-                use: {
-                    loader: 'babel-loader',
-                    options: {
-                        presets: [[
-                            '@babel/preset-env', {
-                                useBuiltIns: 'usage'
-                            }
-                        ]]
-                    }
-                }
-            },
             {
                 test: /\.scss$/,
                 use: [
@@ -44,15 +29,6 @@ const config = {
                         loader: "css-loader", options: {
                         sourceMap: dev
                     }
-                    }, {
-                        loader: 'postcss-loader',
-                        options: {
-                            ident: 'postcss',
-                            sourceMap: dev,
-                            plugins: (loader) => [
-                                require('autoprefixer')(),
-                            ]
-                        }
                     }, {
                         loader: "sass-loader", options: {
                             sourceMap: dev
@@ -73,8 +49,4 @@ if (dev) {
     config['devtool'] = 'inline-source-map';
 }
 
-if (!dev) {
-    config.plugins.push(new UglifyJsPlugin());
-}
-
 module.exports = config;
\ No newline at end of file