]> BookStack Code Mirror - bookstack/commitdiff
Merge pull request #4320 from devdot/improve-api-auth-exception
authorDan Brown <redacted>
Sun, 25 Jun 2023 22:35:19 +0000 (23:35 +0100)
committerGitHub <redacted>
Sun, 25 Jun 2023 22:35:19 +0000 (23:35 +0100)
Improve ApiAuthException control flow

41 files changed:
.env.example
.env.example.complete
app/Activity/Controllers/CommentController.php
app/Activity/Models/Activity.php
app/Api/ApiDocsGenerator.php
app/Config/mail.php
app/Entities/Controllers/BookshelfController.php
app/Entities/Controllers/PageApiController.php
app/Entities/Controllers/PageController.php
app/Entities/Models/Page.php
app/Entities/Tools/PageEditorData.php
app/Entities/Tools/PermissionsUpdater.php
app/Exceptions/JsonDebugException.php
app/Permissions/ContentPermissionApiController.php
app/Uploads/Controllers/ImageGalleryApiController.php
app/Uploads/HttpFetcher.php
app/Uploads/UserAvatars.php
app/Users/Controllers/UserApiController.php
composer.lock
database/migrations/2023_06_25_181952_remove_bookshelf_create_entity_permissions.php [new file with mode: 0644]
dev/api/responses/pages-create.json
dev/api/responses/pages-read.json
dev/api/responses/pages-update.json
dev/api/responses/users-list.json
lang/en/entities.php
resources/sass/_components.scss
resources/views/api-docs/parts/endpoint.blade.php
resources/views/comments/comment.blade.php
resources/views/comments/comments.blade.php
resources/views/form/entity-permissions-row.blade.php
resources/views/pages/parts/editor-toolbox.blade.php
resources/views/pages/parts/toolbox-comments.blade.php [new file with mode: 0644]
tests/Api/ApiDocsTest.php
tests/Api/ContentPermissionsApiTest.php
tests/Api/PagesApiTest.php
tests/Api/UsersApiTest.php
tests/Entity/BookShelfTest.php
tests/Entity/CommentTest.php
tests/Permissions/EntityPermissionsTest.php
tests/Unit/ConfigTest.php
tests/Uploads/AvatarTest.php

index a0a1b72e6836bcab495a34c0f8633ea295edb644..4dee3b3344124a99c4167517615e821cedce2180 100644 (file)
 # SMTP mail options
 # These settings can be checked using the "Send a Test Email"
 # feature found in the "Settings > Maintenance" area of the system.
+# For more detailed documentation on mail options, refer to:
+# https://www.bookstackapp.com/docs/admin/email-webhooks/#email-configuration
 MAIL_HOST=localhost
-MAIL_PORT=1025
+MAIL_PORT=587
 MAIL_USERNAME=null
 MAIL_PASSWORD=null
 MAIL_ENCRYPTION=null
index 7071846a36e9da31868d3b7a1ff24096463dd3df..96a3b448ff4de913254ae3883a8557ffab83d2d9 100644 (file)
@@ -69,23 +69,19 @@ DB_PASSWORD=database_user_password
 # certificate itself (Common Name or Subject Alternative Name).
 MYSQL_ATTR_SSL_CA="/path/to/ca.pem"
 
-# Mail system to use
-# Can be 'smtp' or 'sendmail'
+# Mail configuration
+# Refer to https://www.bookstackapp.com/docs/admin/email-webhooks/#email-configuration
 MAIL_DRIVER=smtp
-
-# Mail sending options
 MAIL_FROM_NAME=BookStack
 
-# SMTP mail options
 MAIL_HOST=localhost
-MAIL_PORT=1025
+MAIL_PORT=587
 MAIL_USERNAME=null
 MAIL_PASSWORD=null
 MAIL_ENCRYPTION=null
 MAIL_VERIFY_SSL=true
 
-# Command to use when email is sent via sendmail
 MAIL_SENDMAIL_COMMAND="/usr/sbin/sendmail -bs"
 
 # Cache & Session driver to use
index 9e7491fd7c71dfa2bea24ff675406ca5e9f5ee53..516bcac759a981921c3e610cd1c9dba65fdb9214 100644 (file)
@@ -42,6 +42,7 @@ class CommentController extends Controller
         $comment = $this->commentRepo->create($page, $request->get('text'), $request->get('parent_id'));
 
         return view('comments.comment-branch', [
+            'readOnly' => false,
             'branch' => [
                 'comment' => $comment,
                 'children' => [],
@@ -66,7 +67,7 @@ class CommentController extends Controller
 
         $comment = $this->commentRepo->update($comment, $request->get('text'));
 
-        return view('comments.comment', ['comment' => $comment]);
+        return view('comments.comment', ['comment' => $comment, 'readOnly' => false]);
     }
 
     /**
index 1fa36e5beaf76e2768b078fe58db64374c4892ea..9e4cb785864e32a60bfbcd7fa01a428022748ea1 100644 (file)
@@ -19,6 +19,8 @@ use Illuminate\Support\Str;
  * @property string $entity_type
  * @property int    $entity_id
  * @property int    $user_id
+ * @property Carbon $created_at
+ * @property Carbon $updated_at
  */
 class Activity extends Model
 {
index f13842328c67d8b1e555d2905cfc3462a14ed77b..3cd33ffa576b5d8564a91c7827f708e611ff8cdf 100644 (file)
@@ -16,8 +16,8 @@ use ReflectionMethod;
 
 class ApiDocsGenerator
 {
-    protected $reflectionClasses = [];
-    protected $controllerClasses = [];
+    protected array $reflectionClasses = [];
+    protected array $controllerClasses = [];
 
     /**
      * Load the docs form the cache if existing
@@ -139,9 +139,10 @@ class ApiDocsGenerator
     protected function parseDescriptionFromMethodComment(string $comment): string
     {
         $matches = [];
-        preg_match_all('/^\s*?\*\s((?![@\s]).*?)$/m', $comment, $matches);
+        preg_match_all('/^\s*?\*\s?($|((?![\/@\s]).*?))$/m', $comment, $matches);
 
-        return implode(' ', $matches[1] ?? []);
+        $text = implode(' ', $matches[1] ?? []);
+        return str_replace('  ', "\n", $text);
     }
 
     /**
index 87514aa409809c875cbe5523404214c440e71dba..e8ec9cc155c3385394771e7cfd88699afaf54939 100644 (file)
@@ -8,6 +8,10 @@
  * Do not edit this file unless you're happy to maintain any changes yourself.
  */
 
+// Configured mail encryption method.
+// STARTTLS should still be attempted, but tls/ssl forces TLS usage.
+$mailEncryption = env('MAIL_ENCRYPTION', null);
+
 return [
 
     // Mail driver to use.
@@ -27,9 +31,9 @@ return [
     'mailers' => [
         'smtp' => [
             'transport' => 'smtp',
+            'scheme' => ($mailEncryption === 'tls' || $mailEncryption === 'ssl') ? 'smtps' : null,
             'host' => env('MAIL_HOST', 'smtp.mailgun.org'),
             'port' => env('MAIL_PORT', 587),
-            'encryption' => env('MAIL_ENCRYPTION', 'tls'),
             'username' => env('MAIL_USERNAME'),
             'password' => env('MAIL_PASSWORD'),
             'verify_peer' => env('MAIL_VERIFY_SSL', true),
index d1b752dc23eef47b5f5e8297947d2ba6d46c375b..fcfd37538724a8c653e9997e3df732011cd30243 100644 (file)
@@ -30,7 +30,7 @@ class BookshelfController extends Controller
     }
 
     /**
-     * Display a listing of the book.
+     * Display a listing of bookshelves.
      */
     public function index(Request $request)
     {
@@ -111,8 +111,9 @@ class BookshelfController extends Controller
         ]);
 
         $sort = $listOptions->getSort();
-        $sortedVisibleShelfBooks = $shelf->visibleBooks()->get()
-            ->sortBy($sort === 'default' ? 'pivot.order' : $sort, SORT_REGULAR, $listOptions->getOrder() === 'desc')
+        $sortedVisibleShelfBooks = $shelf->visibleBooks()
+            ->reorder($sort === 'default' ? 'order' : $sort, $listOptions->getOrder())
+            ->get()
             ->values()
             ->all();
 
index 28dd36f973e0d487f74bc6be8b0af30e3bf986c8..0e8893450fe8e906522c96cea463cea61f4e2f6b 100644 (file)
@@ -13,8 +13,6 @@ use Illuminate\Http\Request;
 
 class PageApiController extends ApiController
 {
-    protected PageRepo $pageRepo;
-
     protected $rules = [
         'create' => [
             'book_id'    => ['required_without:chapter_id', 'integer'],
@@ -34,9 +32,9 @@ class PageApiController extends ApiController
         ],
     ];
 
-    public function __construct(PageRepo $pageRepo)
-    {
-        $this->pageRepo = $pageRepo;
+    public function __construct(
+        protected PageRepo $pageRepo
+    ) {
     }
 
     /**
@@ -84,10 +82,14 @@ class PageApiController extends ApiController
 
     /**
      * View the details of a single page.
-     *
      * Pages will always have HTML content. They may have markdown content
      * if the markdown editor was used to last update the page.
      *
+     * The 'html' property is the fully rendered & escaped HTML content that BookStack
+     * would show on page view, with page includes handled.
+     * The 'raw_html' property is the direct database stored HTML content, which would be
+     * what BookStack shows on page edit.
+     *
      * See the "Content Security" section of these docs for security considerations when using
      * the page content returned from this endpoint.
      */
index 3187e6486fa6d60b82ea2a0e6625a278ce4f223f..e96d41bb1b445bd290c0f13b04f6b09ccd638b95 100644 (file)
@@ -24,16 +24,10 @@ use Throwable;
 
 class PageController extends Controller
 {
-    protected PageRepo $pageRepo;
-    protected ReferenceFetcher $referenceFetcher;
-
-    /**
-     * PageController constructor.
-     */
-    public function __construct(PageRepo $pageRepo, ReferenceFetcher $referenceFetcher)
-    {
-        $this->pageRepo = $pageRepo;
-        $this->referenceFetcher = $referenceFetcher;
+    public function __construct(
+        protected PageRepo $pageRepo,
+        protected ReferenceFetcher $referenceFetcher
+    ) {
     }
 
     /**
index 40acb9a354b259e648dfb5461fb1fadd06b37528..7e2c12c2048d94f93745f803a8927942b79ad120 100644 (file)
@@ -139,6 +139,7 @@ class Page extends BookChild
     {
         $refreshed = $this->refresh()->unsetRelations()->load(['tags', 'createdBy', 'updatedBy', 'ownedBy']);
         $refreshed->setHidden(array_diff($refreshed->getHidden(), ['html', 'markdown']));
+        $refreshed->setAttribute('raw_html', $refreshed->html);
         $refreshed->html = (new PageContent($refreshed))->render();
 
         return $refreshed;
index 2342081bbb52376b807c4f24fa275b1705a9e1e8..3c7c9e2eaf0954e04c73089d34ecea1202c942f0 100644 (file)
@@ -2,6 +2,7 @@
 
 namespace BookStack\Entities\Tools;
 
+use BookStack\Activity\Tools\CommentTree;
 use BookStack\Entities\Models\Page;
 use BookStack\Entities\Repos\PageRepo;
 use BookStack\Entities\Tools\Markdown\HtmlToMarkdown;
@@ -9,19 +10,14 @@ use BookStack\Entities\Tools\Markdown\MarkdownToHtml;
 
 class PageEditorData
 {
-    protected Page $page;
-    protected PageRepo $pageRepo;
-    protected string $requestedEditor;
-
     protected array $viewData;
     protected array $warnings;
 
-    public function __construct(Page $page, PageRepo $pageRepo, string $requestedEditor)
-    {
-        $this->page = $page;
-        $this->pageRepo = $pageRepo;
-        $this->requestedEditor = $requestedEditor;
-
+    public function __construct(
+        protected Page $page,
+        protected PageRepo $pageRepo,
+        protected string $requestedEditor
+    ) {
         $this->viewData = $this->build();
     }
 
@@ -69,6 +65,7 @@ class PageEditorData
             'draftsEnabled'   => $draftsEnabled,
             'templates'       => $templates,
             'editor'          => $editorType,
+            'comments'        => new CommentTree($page),
         ];
     }
 
index 324755e4dbd628138c6c5be3bdb3f9b491d1db44..9f3b8f952777d8c1e9658f96fd2f5c65acb6eb95 100644 (file)
@@ -55,9 +55,9 @@ class PermissionsUpdater
         }
 
         if (isset($data['fallback_permissions']['inheriting']) && $data['fallback_permissions']['inheriting'] !== true) {
-            $data = $data['fallback_permissions'];
-            $data['role_id'] = 0;
-            $rolePermissionData = $this->formatPermissionsFromApiRequestToEntityPermissions([$data], true);
+            $fallbackData = $data['fallback_permissions'];
+            $fallbackData['role_id'] = 0;
+            $rolePermissionData = $this->formatPermissionsFromApiRequestToEntityPermissions([$fallbackData], true);
             $entity->permissions()->createMany($rolePermissionData);
         }
 
index 8acc19778e5dad7a0106edf7f9c4fe28659be52b..1fa52c4a2abf914486690cbc674b83a284258d65 100644 (file)
@@ -4,8 +4,9 @@ namespace BookStack\Exceptions;
 
 use Exception;
 use Illuminate\Http\JsonResponse;
+use Illuminate\Contracts\Support\Responsable;
 
-class JsonDebugException extends Exception
+class JsonDebugException extends Exception implements Responsable
 {
     protected array $data;
 
@@ -22,7 +23,7 @@ class JsonDebugException extends Exception
      * Convert this exception into a response.
      * We add a manual data conversion to UTF8 to ensure any binary data is presentable as a JSON string.
      */
-    public function render(): JsonResponse
+    public function toResponse($request): JsonResponse
     {
         $cleaned = mb_convert_encoding($this->data, 'UTF-8');
 
index cd561fdee3abf7544ab5ba459fb07475de7c644a..23b75db359a469962779c0d3cffffbdcd040ad1a 100644 (file)
@@ -38,8 +38,10 @@ class ContentPermissionApiController extends ApiController
 
     /**
      * Read the configured content-level permissions for the item of the given type and ID.
+     *
      * 'contentType' should be one of: page, book, chapter, bookshelf.
      * 'contentId' should be the relevant ID of that item type you'd like to handle permissions for.
+     *
      * The permissions shown are those that override the default for just the specified item, they do not show the
      * full evaluated permission for a role, nor do they reflect permissions inherited from other items in the hierarchy.
      * Fallback permission values may be `null` when inheriting is active.
@@ -57,6 +59,7 @@ class ContentPermissionApiController extends ApiController
     /**
      * Update the configured content-level permission overrides for the item of the given type and ID.
      * 'contentType' should be one of: page, book, chapter, bookshelf.
+     *
      * 'contentId' should be the relevant ID of that item type you'd like to handle permissions for.
      * Providing an empty `role_permissions` array will remove any existing configured role permissions,
      * so you may want to fetch existing permissions beforehand if just adding/removing a single item.
index c444ec663e81f5cfb7e895aa5c994dd202523d70..0d35d2905f71dbeda2302539760baff2119a36c2 100644 (file)
@@ -52,8 +52,10 @@ class ImageGalleryApiController extends ApiController
 
     /**
      * Create a new image in the system.
+     *
      * Since "image" is expected to be a file, this needs to be a 'multipart/form-data' type request.
      * The provided "uploaded_to" should be an existing page ID in the system.
+     *
      * If the "name" parameter is omitted, the filename of the provided image file will be used instead.
      * The "type" parameter should be 'gallery' for page content images, and 'drawio' should only be used
      * when the file is a PNG file with diagrams.net image data embedded within.
index 4198bb2a326dce7b3e28d6083a6dc6422daf86ee..fcb4147e9ee65e6b369d947cf706aeac2c438d99 100644 (file)
@@ -29,7 +29,8 @@ class HttpFetcher
         curl_close($ch);
 
         if ($err) {
-            throw new HttpFetchException($err);
+            $errno = curl_errno($ch);
+            throw new HttpFetchException($err, $errno);
         }
 
         return $data;
index 89e88bd19e02d804fa9132754579a2dcc6c21ab0..3cd37812acbd40f14aec23ae2bb1d386a49df693 100644 (file)
@@ -34,7 +34,7 @@ class UserAvatars
             $user->avatar()->associate($avatar);
             $user->save();
         } catch (Exception $e) {
-            Log::error('Failed to save user avatar image');
+            Log::error('Failed to save user avatar image', ['exception' => $e]);
         }
     }
 
@@ -49,7 +49,7 @@ class UserAvatars
             $user->avatar()->associate($avatar);
             $user->save();
         } catch (Exception $e) {
-            Log::error('Failed to save user avatar image');
+            Log::error('Failed to save user avatar image', ['exception' => $e]);
         }
     }
 
@@ -107,14 +107,14 @@ class UserAvatars
     /**
      * Gets an image from url and returns it as a string of image data.
      *
-     * @throws Exception
+     * @throws HttpFetchException
      */
     protected function getAvatarImageData(string $url): string
     {
         try {
             $imageData = $this->http->fetch($url);
         } catch (HttpFetchException $exception) {
-            throw new Exception(trans('errors.cannot_get_image_from_url', ['url' => $url]));
+            throw new HttpFetchException(trans('errors.cannot_get_image_from_url', ['url' => $url]), $exception->getCode(), $exception);
         }
 
         return $imageData;
index 759aafbd84a2364fed6a27773462af9db93394e7..880165e1bc77780b46a5d234c7ba2ed7d36bea49 100644 (file)
@@ -73,7 +73,7 @@ class UserApiController extends ApiController
      */
     public function list()
     {
-        $users = User::query()->select(['*'])
+        $users = User::query()->select(['users.*'])
             ->scopes('withLastActivityAt')
             ->with(['avatar']);
 
index 75d8d8a58e95144ce55828466cdbd00f3367e5b1..91bdbc93f09d0096d8c4db7b6719f0d61d5c6873 100644 (file)
         },
         {
             "name": "doctrine/dbal",
-            "version": "3.6.2",
+            "version": "3.6.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/doctrine/dbal.git",
-                "reference": "b4bd1cfbd2b916951696d82e57d054394d84864c"
+                "reference": "19f0dec95edd6a3c3c5ff1d188ea94c6b7fc903f"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/doctrine/dbal/zipball/b4bd1cfbd2b916951696d82e57d054394d84864c",
-                "reference": "b4bd1cfbd2b916951696d82e57d054394d84864c",
+                "url": "https://api.github.com/repos/doctrine/dbal/zipball/19f0dec95edd6a3c3c5ff1d188ea94c6b7fc903f",
+                "reference": "19f0dec95edd6a3c3c5ff1d188ea94c6b7fc903f",
                 "shasum": ""
             },
             "require": {
                 "psr/log": "^1|^2|^3"
             },
             "require-dev": {
-                "doctrine/coding-standard": "11.1.0",
+                "doctrine/coding-standard": "12.0.0",
                 "fig/log-test": "^1",
                 "jetbrains/phpstorm-stubs": "2022.3",
-                "phpstan/phpstan": "1.10.9",
+                "phpstan/phpstan": "1.10.14",
                 "phpstan/phpstan-strict-rules": "^1.5",
-                "phpunit/phpunit": "9.6.6",
+                "phpunit/phpunit": "9.6.7",
                 "psalm/plugin-phpunit": "0.18.4",
                 "squizlabs/php_codesniffer": "3.7.2",
                 "symfony/cache": "^5.4|^6.0",
             ],
             "support": {
                 "issues": "https://github.com/doctrine/dbal/issues",
-                "source": "https://github.com/doctrine/dbal/tree/3.6.2"
+                "source": "https://github.com/doctrine/dbal/tree/3.6.4"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-04-14T07:25:38+00:00"
+            "time": "2023-06-15T07:40:12+00:00"
         },
         {
             "name": "doctrine/deprecations",
-            "version": "v1.0.0",
+            "version": "v1.1.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/doctrine/deprecations.git",
-                "reference": "0e2a4f1f8cdfc7a92ec3b01c9334898c806b30de"
+                "reference": "612a3ee5ab0d5dd97b7cf3874a6efe24325efac3"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/doctrine/deprecations/zipball/0e2a4f1f8cdfc7a92ec3b01c9334898c806b30de",
-                "reference": "0e2a4f1f8cdfc7a92ec3b01c9334898c806b30de",
+                "url": "https://api.github.com/repos/doctrine/deprecations/zipball/612a3ee5ab0d5dd97b7cf3874a6efe24325efac3",
+                "reference": "612a3ee5ab0d5dd97b7cf3874a6efe24325efac3",
                 "shasum": ""
             },
             "require": {
-                "php": "^7.1|^8.0"
+                "php": "^7.1 || ^8.0"
             },
             "require-dev": {
                 "doctrine/coding-standard": "^9",
-                "phpunit/phpunit": "^7.5|^8.5|^9.5",
-                "psr/log": "^1|^2|^3"
+                "phpstan/phpstan": "1.4.10 || 1.10.15",
+                "phpstan/phpstan-phpunit": "^1.0",
+                "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5",
+                "psalm/plugin-phpunit": "0.18.4",
+                "psr/log": "^1 || ^2 || ^3",
+                "vimeo/psalm": "4.30.0 || 5.12.0"
             },
             "suggest": {
                 "psr/log": "Allows logging deprecations via PSR-3 logger implementation"
             "homepage": "https://www.doctrine-project.org/",
             "support": {
                 "issues": "https://github.com/doctrine/deprecations/issues",
-                "source": "https://github.com/doctrine/deprecations/tree/v1.0.0"
+                "source": "https://github.com/doctrine/deprecations/tree/v1.1.1"
             },
-            "time": "2022-05-02T15:47:09+00:00"
+            "time": "2023-06-03T09:27:29+00:00"
         },
         {
             "name": "doctrine/event-manager",
         },
         {
             "name": "doctrine/inflector",
-            "version": "2.0.6",
+            "version": "2.0.8",
             "source": {
                 "type": "git",
                 "url": "https://github.com/doctrine/inflector.git",
-                "reference": "d9d313a36c872fd6ee06d9a6cbcf713eaa40f024"
+                "reference": "f9301a5b2fb1216b2b08f02ba04dc45423db6bff"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/doctrine/inflector/zipball/d9d313a36c872fd6ee06d9a6cbcf713eaa40f024",
-                "reference": "d9d313a36c872fd6ee06d9a6cbcf713eaa40f024",
+                "url": "https://api.github.com/repos/doctrine/inflector/zipball/f9301a5b2fb1216b2b08f02ba04dc45423db6bff",
+                "reference": "f9301a5b2fb1216b2b08f02ba04dc45423db6bff",
                 "shasum": ""
             },
             "require": {
                 "php": "^7.2 || ^8.0"
             },
             "require-dev": {
-                "doctrine/coding-standard": "^10",
+                "doctrine/coding-standard": "^11.0",
                 "phpstan/phpstan": "^1.8",
                 "phpstan/phpstan-phpunit": "^1.1",
                 "phpstan/phpstan-strict-rules": "^1.3",
                 "phpunit/phpunit": "^8.5 || ^9.5",
-                "vimeo/psalm": "^4.25"
+                "vimeo/psalm": "^4.25 || ^5.4"
             },
             "type": "library",
             "autoload": {
             ],
             "support": {
                 "issues": "https://github.com/doctrine/inflector/issues",
-                "source": "https://github.com/doctrine/inflector/tree/2.0.6"
+                "source": "https://github.com/doctrine/inflector/tree/2.0.8"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-10-20T09:10:12+00:00"
+            "time": "2023-06-16T13:40:37+00:00"
         },
         {
             "name": "doctrine/lexer",
         },
         {
             "name": "egulias/email-validator",
-            "version": "3.2.5",
+            "version": "3.2.6",
             "source": {
                 "type": "git",
                 "url": "https://github.com/egulias/EmailValidator.git",
-                "reference": "b531a2311709443320c786feb4519cfaf94af796"
+                "reference": "e5997fa97e8790cdae03a9cbd5e78e45e3c7bda7"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/b531a2311709443320c786feb4519cfaf94af796",
-                "reference": "b531a2311709443320c786feb4519cfaf94af796",
+                "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/e5997fa97e8790cdae03a9cbd5e78e45e3c7bda7",
+                "reference": "e5997fa97e8790cdae03a9cbd5e78e45e3c7bda7",
                 "shasum": ""
             },
             "require": {
             ],
             "support": {
                 "issues": "https://github.com/egulias/EmailValidator/issues",
-                "source": "https://github.com/egulias/EmailValidator/tree/3.2.5"
+                "source": "https://github.com/egulias/EmailValidator/tree/3.2.6"
             },
             "funding": [
                 {
                     "type": "github"
                 }
             ],
-            "time": "2023-01-02T17:26:14+00:00"
+            "time": "2023-06-01T07:04:22+00:00"
         },
         {
             "name": "fruitcake/php-cors",
         },
         {
             "name": "laravel/framework",
-            "version": "v9.52.7",
+            "version": "v9.52.9",
             "source": {
                 "type": "git",
                 "url": "https://github.com/laravel/framework.git",
-                "reference": "675ea868fe36b18c8303e954aac540e6b1caa677"
+                "reference": "c512ece7b1ee393eac5893f37cb2b029a5413b97"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/laravel/framework/zipball/675ea868fe36b18c8303e954aac540e6b1caa677",
-                "reference": "675ea868fe36b18c8303e954aac540e6b1caa677",
+                "url": "https://api.github.com/repos/laravel/framework/zipball/c512ece7b1ee393eac5893f37cb2b029a5413b97",
+                "reference": "c512ece7b1ee393eac5893f37cb2b029a5413b97",
                 "shasum": ""
             },
             "require": {
                 "issues": "https://github.com/laravel/framework/issues",
                 "source": "https://github.com/laravel/framework"
             },
-            "time": "2023-04-25T13:44:05+00:00"
+            "time": "2023-06-08T20:06:23+00:00"
         },
         {
             "name": "laravel/serializable-closure",
         },
         {
             "name": "laravel/socialite",
-            "version": "v5.6.1",
+            "version": "v5.6.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/laravel/socialite.git",
-                "reference": "a14a177f2cc71d8add71e2b19e00800e83bdda09"
+                "reference": "00ea7f8630673ea49304fc8a9fca5a64eb838c7e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/laravel/socialite/zipball/a14a177f2cc71d8add71e2b19e00800e83bdda09",
-                "reference": "a14a177f2cc71d8add71e2b19e00800e83bdda09",
+                "url": "https://api.github.com/repos/laravel/socialite/zipball/00ea7f8630673ea49304fc8a9fca5a64eb838c7e",
+                "reference": "00ea7f8630673ea49304fc8a9fca5a64eb838c7e",
                 "shasum": ""
             },
             "require": {
             "require-dev": {
                 "mockery/mockery": "^1.0",
                 "orchestra/testbench": "^4.0|^5.0|^6.0|^7.0|^8.0",
+                "phpstan/phpstan": "^1.10",
                 "phpunit/phpunit": "^8.0|^9.3"
             },
             "type": "library",
                 "issues": "https://github.com/laravel/socialite/issues",
                 "source": "https://github.com/laravel/socialite"
             },
-            "time": "2023-01-20T15:42:35+00:00"
+            "time": "2023-06-06T13:42:43+00:00"
         },
         {
             "name": "laravel/tinker",
         },
         {
             "name": "nesbot/carbon",
-            "version": "2.66.0",
+            "version": "2.67.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/briannesbitt/Carbon.git",
-                "reference": "496712849902241f04902033b0441b269effe001"
+                "reference": "c1001b3bc75039b07f38a79db5237c4c529e04c8"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/496712849902241f04902033b0441b269effe001",
-                "reference": "496712849902241f04902033b0441b269effe001",
+                "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/c1001b3bc75039b07f38a79db5237c4c529e04c8",
+                "reference": "c1001b3bc75039b07f38a79db5237c4c529e04c8",
                 "shasum": ""
             },
             "require": {
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-01-29T18:53:47+00:00"
+            "time": "2023-05-25T22:09:47+00:00"
         },
         {
             "name": "nette/schema",
         },
         {
             "name": "phpseclib/phpseclib",
-            "version": "3.0.19",
+            "version": "3.0.20",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpseclib/phpseclib.git",
-                "reference": "cc181005cf548bfd8a4896383bb825d859259f95"
+                "reference": "543a1da81111a0bfd6ae7bbc2865c5e89ed3fc67"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/cc181005cf548bfd8a4896383bb825d859259f95",
-                "reference": "cc181005cf548bfd8a4896383bb825d859259f95",
+                "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/543a1da81111a0bfd6ae7bbc2865c5e89ed3fc67",
+                "reference": "543a1da81111a0bfd6ae7bbc2865c5e89ed3fc67",
                 "shasum": ""
             },
             "require": {
             ],
             "support": {
                 "issues": "https://github.com/phpseclib/phpseclib/issues",
-                "source": "https://github.com/phpseclib/phpseclib/tree/3.0.19"
+                "source": "https://github.com/phpseclib/phpseclib/tree/3.0.20"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-03-05T17:13:09+00:00"
+            "time": "2023-06-13T06:30:34+00:00"
         },
         {
             "name": "pragmarx/google2fa",
         },
         {
             "name": "predis/predis",
-            "version": "v2.1.2",
+            "version": "v2.2.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/predis/predis.git",
-                "reference": "a77a43913a74f9331f637bb12867eb8e274814e5"
+                "reference": "33b70b971a32b0d28b4f748b0547593dce316e0d"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/predis/predis/zipball/a77a43913a74f9331f637bb12867eb8e274814e5",
-                "reference": "a77a43913a74f9331f637bb12867eb8e274814e5",
+                "url": "https://api.github.com/repos/predis/predis/zipball/33b70b971a32b0d28b4f748b0547593dce316e0d",
+                "reference": "33b70b971a32b0d28b4f748b0547593dce316e0d",
                 "shasum": ""
             },
             "require": {
                 "phpstan/phpstan": "^1.9",
                 "phpunit/phpunit": "^8.0 || ~9.4.4"
             },
+            "suggest": {
+                "ext-relay": "Faster connection with in-memory caching (>=0.6.2)"
+            },
             "type": "library",
             "autoload": {
                 "psr-4": {
             ],
             "support": {
                 "issues": "https://github.com/predis/predis/issues",
-                "source": "https://github.com/predis/predis/tree/v2.1.2"
+                "source": "https://github.com/predis/predis/tree/v2.2.0"
             },
             "funding": [
                 {
                     "type": "github"
                 }
             ],
-            "time": "2023-03-02T18:32:04+00:00"
+            "time": "2023-06-14T10:37:31+00:00"
         },
         {
             "name": "psr/cache",
         },
         {
             "name": "psy/psysh",
-            "version": "v0.11.17",
+            "version": "v0.11.18",
             "source": {
                 "type": "git",
                 "url": "https://github.com/bobthecow/psysh.git",
-                "reference": "3dc5d4018dabd80bceb8fe1e3191ba8460569f0a"
+                "reference": "4f00ee9e236fa6a48f4560d1300b9c961a70a7ec"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/bobthecow/psysh/zipball/3dc5d4018dabd80bceb8fe1e3191ba8460569f0a",
-                "reference": "3dc5d4018dabd80bceb8fe1e3191ba8460569f0a",
+                "url": "https://api.github.com/repos/bobthecow/psysh/zipball/4f00ee9e236fa6a48f4560d1300b9c961a70a7ec",
+                "reference": "4f00ee9e236fa6a48f4560d1300b9c961a70a7ec",
                 "shasum": ""
             },
             "require": {
             ],
             "support": {
                 "issues": "https://github.com/bobthecow/psysh/issues",
-                "source": "https://github.com/bobthecow/psysh/tree/v0.11.17"
+                "source": "https://github.com/bobthecow/psysh/tree/v0.11.18"
             },
-            "time": "2023-05-05T20:02:42+00:00"
+            "time": "2023-05-23T02:31:11+00:00"
         },
         {
             "name": "ralouphie/getallheaders",
         },
         {
             "name": "fakerphp/faker",
-            "version": "v1.22.0",
+            "version": "v1.23.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/FakerPHP/Faker.git",
-                "reference": "f85772abd508bd04e20bb4b1bbe260a68d0066d2"
+                "reference": "e3daa170d00fde61ea7719ef47bb09bb8f1d9b01"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/f85772abd508bd04e20bb4b1bbe260a68d0066d2",
-                "reference": "f85772abd508bd04e20bb4b1bbe260a68d0066d2",
+                "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/e3daa170d00fde61ea7719ef47bb09bb8f1d9b01",
+                "reference": "e3daa170d00fde61ea7719ef47bb09bb8f1d9b01",
                 "shasum": ""
             },
             "require": {
             ],
             "support": {
                 "issues": "https://github.com/FakerPHP/Faker/issues",
-                "source": "https://github.com/FakerPHP/Faker/tree/v1.22.0"
+                "source": "https://github.com/FakerPHP/Faker/tree/v1.23.0"
             },
-            "time": "2023-05-14T12:31:37+00:00"
+            "time": "2023-06-12T08:44:38+00:00"
         },
         {
             "name": "filp/whoops",
         },
         {
             "name": "mockery/mockery",
-            "version": "1.5.1",
+            "version": "1.6.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/mockery/mockery.git",
-                "reference": "e92dcc83d5a51851baf5f5591d32cb2b16e3684e"
+                "reference": "13a7fa2642c76c58fa2806ef7f565344c817a191"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/mockery/mockery/zipball/e92dcc83d5a51851baf5f5591d32cb2b16e3684e",
-                "reference": "e92dcc83d5a51851baf5f5591d32cb2b16e3684e",
+                "url": "https://api.github.com/repos/mockery/mockery/zipball/13a7fa2642c76c58fa2806ef7f565344c817a191",
+                "reference": "13a7fa2642c76c58fa2806ef7f565344c817a191",
                 "shasum": ""
             },
             "require": {
                 "hamcrest/hamcrest-php": "^2.0.1",
                 "lib-pcre": ">=7.0",
-                "php": "^7.3 || ^8.0"
+                "php": "^7.4 || ^8.0"
             },
             "conflict": {
                 "phpunit/phpunit": "<8.0"
             },
             "require-dev": {
-                "phpunit/phpunit": "^8.5 || ^9.3"
+                "phpunit/phpunit": "^8.5 || ^9.3",
+                "psalm/plugin-phpunit": "^0.18",
+                "vimeo/psalm": "^5.9"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.4.x-dev"
+                    "dev-main": "1.6.x-dev"
                 }
             },
             "autoload": {
-                "psr-0": {
-                    "Mockery": "library/"
+                "files": [
+                    "library/helpers.php",
+                    "library/Mockery.php"
+                ],
+                "psr-4": {
+                    "Mockery\\": "library/Mockery"
                 }
             },
             "notification-url": "https://packagist.org/downloads/",
             ],
             "support": {
                 "issues": "https://github.com/mockery/mockery/issues",
-                "source": "https://github.com/mockery/mockery/tree/1.5.1"
+                "source": "https://github.com/mockery/mockery/tree/1.6.2"
             },
-            "time": "2022-09-07T15:32:08+00:00"
+            "time": "2023-06-07T09:07:52+00:00"
         },
         {
             "name": "myclabs/deep-copy",
         },
         {
             "name": "nunomaduro/larastan",
-            "version": "v2.6.0",
+            "version": "v2.6.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/nunomaduro/larastan.git",
-                "reference": "ccac5b25949576807862cf32ba1fce1769c06c42"
+                "reference": "73e5be5f5c732212ce6ca77ffd2753a136f36a23"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/nunomaduro/larastan/zipball/ccac5b25949576807862cf32ba1fce1769c06c42",
-                "reference": "ccac5b25949576807862cf32ba1fce1769c06c42",
+                "url": "https://api.github.com/repos/nunomaduro/larastan/zipball/73e5be5f5c732212ce6ca77ffd2753a136f36a23",
+                "reference": "73e5be5f5c732212ce6ca77ffd2753a136f36a23",
                 "shasum": ""
             },
             "require": {
             ],
             "support": {
                 "issues": "https://github.com/nunomaduro/larastan/issues",
-                "source": "https://github.com/nunomaduro/larastan/tree/v2.6.0"
+                "source": "https://github.com/nunomaduro/larastan/tree/v2.6.3"
             },
             "funding": [
                 {
                     "type": "patreon"
                 }
             ],
-            "time": "2023-04-20T12:40:01+00:00"
+            "time": "2023-06-13T21:39:27+00:00"
         },
         {
             "name": "phar-io/manifest",
         },
         {
             "name": "phpmyadmin/sql-parser",
-            "version": "5.7.0",
+            "version": "5.8.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpmyadmin/sql-parser.git",
-                "reference": "0f5895aab2b6002d00b6831b60983523dea30bff"
+                "reference": "db1b3069b5dbc220d393d67ff911e0ae76732755"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpmyadmin/sql-parser/zipball/0f5895aab2b6002d00b6831b60983523dea30bff",
-                "reference": "0f5895aab2b6002d00b6831b60983523dea30bff",
+                "url": "https://api.github.com/repos/phpmyadmin/sql-parser/zipball/db1b3069b5dbc220d393d67ff911e0ae76732755",
+                "reference": "db1b3069b5dbc220d393d67ff911e0ae76732755",
                 "shasum": ""
             },
             "require": {
                     "type": "other"
                 }
             ],
-            "time": "2023-01-25T10:43:40+00:00"
+            "time": "2023-06-05T18:19:38+00:00"
         },
         {
             "name": "phpstan/phpstan",
-            "version": "1.10.15",
+            "version": "1.10.21",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpstan/phpstan.git",
-                "reference": "762c4dac4da6f8756eebb80e528c3a47855da9bd"
+                "reference": "b2a30186be2e4d97dce754ae4e65eb0ec2f04eb5"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpstan/phpstan/zipball/762c4dac4da6f8756eebb80e528c3a47855da9bd",
-                "reference": "762c4dac4da6f8756eebb80e528c3a47855da9bd",
+                "url": "https://api.github.com/repos/phpstan/phpstan/zipball/b2a30186be2e4d97dce754ae4e65eb0ec2f04eb5",
+                "reference": "b2a30186be2e4d97dce754ae4e65eb0ec2f04eb5",
                 "shasum": ""
             },
             "require": {
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-05-09T15:28:01+00:00"
+            "time": "2023-06-21T20:07:58+00:00"
         },
         {
             "name": "phpunit/php-code-coverage",
         },
         {
             "name": "phpunit/phpunit",
-            "version": "9.6.8",
+            "version": "9.6.9",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/phpunit.git",
-                "reference": "17d621b3aff84d0c8b62539e269e87d8d5baa76e"
+                "reference": "a9aceaf20a682aeacf28d582654a1670d8826778"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/17d621b3aff84d0c8b62539e269e87d8d5baa76e",
-                "reference": "17d621b3aff84d0c8b62539e269e87d8d5baa76e",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a9aceaf20a682aeacf28d582654a1670d8826778",
+                "reference": "a9aceaf20a682aeacf28d582654a1670d8826778",
                 "shasum": ""
             },
             "require": {
             "support": {
                 "issues": "https://github.com/sebastianbergmann/phpunit/issues",
                 "security": "https://github.com/sebastianbergmann/phpunit/security/policy",
-                "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.8"
+                "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.9"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-05-11T05:14:45+00:00"
+            "time": "2023-06-11T06:13:56+00:00"
         },
         {
             "name": "sebastian/cli-parser",
diff --git a/database/migrations/2023_06_25_181952_remove_bookshelf_create_entity_permissions.php b/database/migrations/2023_06_25_181952_remove_bookshelf_create_entity_permissions.php
new file mode 100644 (file)
index 0000000..efb6597
--- /dev/null
@@ -0,0 +1,29 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Support\Facades\DB;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        DB::table('entity_permissions')
+            ->where('entity_type', '=', 'bookshelf')
+            ->update(['create' => 0]);
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        // No structural changes to make, and we cannot know the permissions to re-assign.
+    }
+};
index eeaa5303af6d5eea824c59c733945b209d46add0..5c3d8021504566f5eae13380c8476ddec75aeb7f 100644 (file)
@@ -5,6 +5,7 @@
        "name": "My API Page",
        "slug": "my-api-page",
        "html": "<p id=\"bkmrk-my-new-api-page\">my new API page</p>",
+       "raw_html": "<p id=\"bkmrk-my-new-api-page\">my new API page</p>",
        "priority": 14,
        "created_at": "2020-11-28T15:01:39.000000Z",
        "updated_at": "2020-11-28T15:01:39.000000Z",
index 9a21cd44cad93a2fc13846d7c54e687faece45da..a47990cc67532bfe7bfd5c191eb34ee6fb88ad6c 100644 (file)
@@ -4,7 +4,8 @@
        "chapter_id": 0,
        "name": "A page written in markdown",
        "slug": "a-page-written-in-markdown",
-       "html": "<h1 id=\"bkmrk-how-this-is-built\">How this is built</h1>\r\n<p id=\"bkmrk-this-page-is-written\">This page is written in markdown. BookStack stores the page data in HTML.</p>\r\n<p id=\"bkmrk-here%27s-a-cute-pictur\">Here's a cute picture of my cat:</p>\r\n<p id=\"bkmrk-\"><a href=\"http://example.com/uploads/images/gallery/2020-04/yXSrubes.jpg\"><img src=\"http://example.com/uploads/images/gallery/2020-04/scaled-1680-/yXSrubes.jpg\" alt=\"yXSrubes.jpg\"></a></p>",
+       "html": "<h1 id=\"bkmrk-this-is-my-cool-page\">This is my cool page! With some included text</h1>",
+       "raw_html": "<h1 id=\"bkmrk-this-is-my-cool-page\">This is my cool page! {{@1#bkmrk-a}}</h1>",
        "priority": 13,
        "created_at": "2020-02-02T21:40:38.000000Z",
        "updated_at": "2020-11-28T14:43:20.000000Z",
index 0b8b2374c180fe66c4497b2f56095b5fc5cb4675..e91b74661d8951fa35cb798ba368694b7cc6ca39 100644 (file)
@@ -5,6 +5,7 @@
        "name": "My updated API Page",
        "slug": "my-updated-api-page",
        "html": "<p id=\"bkmrk-my-new-api-page---up\">my new API page - Updated</p>",
+       "raw_html": "<p id=\"bkmrk-my-new-api-page---up\">my new API page - Updated</p>",
        "priority": 16,
        "created_at": "2020-11-28T15:10:54.000000Z",
        "updated_at": "2020-11-28T15:13:03.000000Z",
index e070ee6a64d648a66735a6bc6a6b3639e48e9118..cbc7fb1043ed3f2efc47e1fe96142c9b613a8991 100644 (file)
@@ -18,7 +18,7 @@
       "id": 2,
       "name": "Benny",
       "email": "[email protected]",
-      "created_at": "2022-01-31T20:39:24.000000Z",
+      "created_at": "2020-01-15T04:43:11.000000Z",
       "updated_at": "2021-11-18T17:10:58.000000Z",
       "external_auth_id": "",
       "slug": "benny",
index 5a148e1a259640349a07edde0269eda91bcd9049..8cd7e925f8e7ef71710d9d45e113be9f6399c5d2 100644 (file)
@@ -371,6 +371,7 @@ return [
     'comment_updated_success' => 'Comment updated',
     'comment_delete_confirm' => 'Are you sure you want to delete this comment?',
     'comment_in_reply_to' => 'In reply to :commentId',
+    'comment_editor_explain' => 'Here are the comments that have been left on this page. Comments can be added & managed when viewing the saved page.',
 
     // Revision
     'revision_delete_confirm' => 'Are you sure you want to delete this revision?',
index dab74341a7a2b26a30cea253871cb58202de98b6..e6c0fdcd16d0d5858dd0e25213643bbe8e457c40 100644 (file)
@@ -676,6 +676,7 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
   @include lightDark(background-color, #FFF, #222);
   .content {
     font-size: 0.666em;
+    padding: $-m $-s;
     p, ul, ol {
       font-size: $fs-m;
       margin: .5em 0;
@@ -700,6 +701,7 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
 
 .comment-box .header {
   border-bottom: 1px solid #DDD;
+  padding: $-s;
   @include lightDark(border-color, #DDD, #000);
   button {
     font-size: .8rem;
@@ -710,6 +712,9 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
   .text-muted {
     color: #999;
   }
+  .meta a, .meta span {
+    white-space: nowrap;
+  }
   .right-meta .text-muted {
     opacity: .8;
   }
@@ -735,6 +740,24 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
   display: block;
 }
 
+.comment-container-compact .comment-box {
+  .meta {
+    font-size: 0.8rem;
+  }
+  .header {
+    padding: $-xs;
+  }
+  .right-meta {
+    display: none;
+  }
+  .content {
+    padding: $-xs $-s;
+  }
+}
+.comment-container-compact .comment-thread-indicator {
+  width: $-m;
+}
+
 #tag-manager .drag-card {
   max-width: 500px;
 }
index 60c478fe563deaacd6c60c368c8f8cf16fafee6d..024a5ecdf04457a52447d1d8b8dc943a091f61d1 100644 (file)
@@ -1,15 +1,20 @@
-<h6 class="text-uppercase text-muted float right">{{ $endpoint['controller_method_kebab'] }}</h6>
+<div class="flex-container-row items-center gap-m">
+    <span class="api-method text-mono" data-method="{{ $endpoint['method'] }}">{{ $endpoint['method'] }}</span>
+    <h5 id="{{ $endpoint['name'] }}" class="text-mono pb-xs">
+        @if($endpoint['controller_method_kebab'] === 'list')
+            <a style="color: inherit;" target="_blank" rel="noopener" href="{{ url($endpoint['uri']) }}">{{ url($endpoint['uri']) }}</a>
+        @else
+            <span>{{ url($endpoint['uri']) }}</span>
+        @endif
+    </h5>
+    <h6 class="text-uppercase text-muted text-mono ml-auto">{{ $endpoint['controller_method_kebab'] }}</h6>
+</div>
 
-<h5 id="{{ $endpoint['name'] }}" class="text-mono mb-m">
-    <span class="api-method" data-method="{{ $endpoint['method'] }}">{{ $endpoint['method'] }}</span>
-    @if($endpoint['controller_method_kebab'] === 'list')
-        <a style="color: inherit;" target="_blank" rel="noopener" href="{{ url($endpoint['uri']) }}">{{ url($endpoint['uri']) }}</a>
-    @else
-        {{ url($endpoint['uri']) }}
-    @endif
-</h5>
-
-<p class="mb-m">{{ $endpoint['description'] ?? '' }}</p>
+<div class="mb-m">
+    @foreach(explode("\n", $endpoint['description'] ?? '') as $descriptionBlock)
+        <p class="mb-xxs">{{ $descriptionBlock }}</p>
+    @endforeach
+</div>
 
 @if($endpoint['body_params'] ?? false)
     <details class="mb-m">
index 04468b83c579a14ca1fde746810a098a74101e41..8933e2e6ab9be20744a13314a0dc00d387d7d4fb 100644 (file)
@@ -1,4 +1,4 @@
-<div component="page-comment"
+<div component="{{ $readOnly ? '' : 'page-comment' }}"
      option:page-comment:comment-id="{{ $comment->id }}"
      option:page-comment:comment-local-id="{{ $comment->local_id }}"
      option:page-comment:comment-parent-id="{{ $comment->parent_id }}"
@@ -6,12 +6,15 @@
      option:page-comment:deleted-text="{{ trans('entities.comment_deleted_success') }}"
      id="comment{{$comment->local_id}}"
      class="comment-box">
-    <div class="header p-s">
-        <div class="flex-container-row justify-space-between wrap">
-            <div class="meta text-muted flex-container-row items-center">
+    <div class="header">
+        <div class="flex-container-row wrap items-center gap-x-xs">
+            @if ($comment->createdBy)
+                <div>
+                    <img width="50" src="{{ $comment->createdBy->getAvatar(50) }}" class="avatar block mx-xs" alt="{{ $comment->createdBy->name }}">
+                </div>
+            @endif
+            <div class="meta text-muted flex-container-row wrap items-center flex">
                 @if ($comment->createdBy)
-                    <img width="50" src="{{ $comment->createdBy->getAvatar(50) }}" class="avatar mx-xs" alt="{{ $comment->createdBy->name }}">
-                    &nbsp;
                     <a href="{{ $comment->createdBy->getProfileUrl() }}">{{ $comment->createdBy->getShortName(16) }}</a>
                 @else
                     {{ trans('common.deleted_user') }}
@@ -25,6 +28,7 @@
                 @endif
             </div>
             <div class="right-meta flex-container-row justify-flex-end items-center px-s">
+                @if(!$readOnly && (userCan('comment-create-all') || userCan('comment-update', $comment) || userCan('comment-delete', $comment)))
                 <div class="actions mr-s">
                     @if(userCan('comment-create-all'))
                         <button refs="page-comment@reply-button" type="button" class="text-button text-muted hover-underline p-xs">@icon('reply') {{ trans('common.reply') }}</button>
@@ -50,6 +54,7 @@
                         &nbsp;&bull;&nbsp;
                     </span>
                 </div>
+                @endif
                 <div>
                     <a class="bold text-muted" href="#comment{{$comment->local_id}}">#{{$comment->local_id}}</a>
                 </div>
@@ -58,7 +63,7 @@
 
     </div>
 
-    <div refs="page-comment@content-container" class="content px-m py-s">
+    <div refs="page-comment@content-container" class="content">
         @if ($comment->parent_id)
             <p class="comment-reply mb-xxs">
                 <a class="text-muted text-small" href="#comment{{ $comment->parent_id }}">@icon('reply'){{ trans('entities.comment_in_reply_to', ['commentId' => '#' . $comment->parent_id]) }}</a>
@@ -67,7 +72,7 @@
         {!! $comment->html  !!}
     </div>
 
-    @if(userCan('comment-update', $comment))
+    @if(!$readOnly && userCan('comment-update', $comment))
         <form novalidate refs="page-comment@form" hidden class="content pt-s px-s block">
             <div class="form-group description-input">
                 <textarea refs="page-comment@input" name="markdown" rows="3" placeholder="{{ trans('entities.comment_placeholder') }}">{{ $comment->text }}</textarea>
index b79f0fd45fda1d6bb767210fe1e490b9eb0a8abc..26d286290c28964686926cafa78bd55768dd8854 100644 (file)
@@ -18,7 +18,7 @@
 
     <div refs="page-comments@commentContainer" class="comment-container">
         @foreach($commentTree->get() as $branch)
-            @include('comments.comment-branch', ['branch' => $branch])
+            @include('comments.comment-branch', ['branch' => $branch, 'readOnly' => false])
         @endforeach
     </div>
 
index 6b515af867970075a35f71c496863eafc176ffac..5c2e8674178b95e5ed2e3f16a4ffda0f77be7e1f 100644 (file)
@@ -44,7 +44,7 @@ $inheriting - Boolean if the current row should be marked as inheriting default
                 'disabled' => $inheriting
             ])
         </div>
-        @if($entityType !== 'page')
+        @if($entityType !== 'page' && $entityType !== 'bookshelf')
             <div class="px-l">
                 @include('form.custom-checkbox', [
                     'name' =>  'permissions[' . $role->id . '][create]',
index 8f332702486105f35c523e5e5660167eded8dfc2..08e61e082a49d965e030ccf366abb25424f7ae07 100644 (file)
@@ -7,6 +7,9 @@
             <button type="button" refs="editor-toolbox@tab-button" data-tab="files" title="{{ trans('entities.attachments') }}">@icon('attach')</button>
         @endif
         <button type="button" refs="editor-toolbox@tab-button" data-tab="templates" title="{{ trans('entities.templates') }}">@icon('template')</button>
+        @if($comments->enabled())
+            <button type="button" refs="editor-toolbox@tab-button" data-tab="comments" title="{{ trans('entities.comments') }}">@icon('comment')</button>
+        @endif
     </div>
 
     <div refs="editor-toolbox@tab-content" data-tab-content="tags" class="toolbox-tab-content">
         <div class="px-l">
             @include('pages.parts.template-manager', ['page' => $page, 'templates' => $templates])
         </div>
-
     </div>
 
+    @if($comments->enabled())
+        @include('pages.parts.toolbox-comments')
+    @endif
+
 </div>
diff --git a/resources/views/pages/parts/toolbox-comments.blade.php b/resources/views/pages/parts/toolbox-comments.blade.php
new file mode 100644 (file)
index 0000000..d632b85
--- /dev/null
@@ -0,0 +1,15 @@
+<div refs="editor-toolbox@tab-content" data-tab-content="comments" class="toolbox-tab-content">
+    <h4>{{ trans('entities.comments') }}</h4>
+
+    <div class="comment-container-compact px-l">
+        <p class="text-muted small mb-m">
+            {{ trans('entities.comment_editor_explain') }}
+        </p>
+        @foreach($comments->get() as $branch)
+            @include('comments.comment-branch', ['branch' => $branch, 'readOnly' => true])
+        @endforeach
+        @if($comments->empty())
+            <p class="italic text-muted">{{ trans('common.no_items') }}</p>
+        @endif
+    </div>
+</div>
\ No newline at end of file
index 56b09cfb85d707142c875af1daa77a174aaa6979..a1603e0ef8274d244835c75e12a076045cb31ad5 100644 (file)
@@ -8,7 +8,7 @@ class ApiDocsTest extends TestCase
 {
     use TestsApi;
 
-    protected $endpoint = '/api/docs';
+    protected string $endpoint = '/api/docs';
 
     public function test_api_endpoint_redirects_to_docs()
     {
index 50b82e5c4cdac3404d6daf41eebe8125adfdbb8f..a62abacc75e56ba9b2e223330d5a3c04cde1e895 100644 (file)
@@ -259,4 +259,36 @@ class ContentPermissionsApiTest extends TestCase
             ],
         ]);
     }
+
+    public function test_update_can_both_provide_owner_and_fallback_permissions()
+    {
+        $user = $this->users->viewer();
+        $page = $this->entities->page();
+        $page->owned_by = null;
+        $page->save();
+
+        $this->actingAsApiAdmin();
+        $resp = $this->putJson($this->baseEndpoint . "/page/{$page->id}", [
+            "owner_id" => $user->id,
+            'fallback_permissions' => [
+                'inheriting' => false,
+                'view' => false,
+                'create' => false,
+                'update' => false,
+                'delete' => false,
+            ],
+        ]);
+
+        $resp->assertOk();
+        $this->assertDatabaseHas('pages', ['id' => $page->id, 'owned_by' => $user->id]);
+        $this->assertDatabaseHas('entity_permissions', [
+            'entity_id' => $page->id,
+            'entity_type' => 'page',
+            'role_id' => 0,
+            'view' => false,
+            'create' => false,
+            'update' => false,
+            'delete' => false,
+        ]);
+    }
 }
index 75cc2807fa58fc8c5d5978474541d8c458eb4b60..4a81f738bbdb092f12ce922331a7011600331b71 100644 (file)
@@ -159,6 +159,20 @@ class PagesApiTest extends TestCase
         $this->assertStringContainsString('testing', $html);
     }
 
+    public function test_read_endpoint_provides_raw_html()
+    {
+        $html = "<p>testing</p><script>alert('danger')</script><h1>Hello</h1>";
+
+        $this->actingAsApiEditor();
+        $page = $this->entities->page();
+        $page->html = $html;
+        $page->save();
+
+        $resp = $this->getJson($this->baseEndpoint . "/{$page->id}");
+        $this->assertEquals($html, $resp->json('raw_html'));
+        $this->assertNotEquals($html, $resp->json('html'));
+    }
+
     public function test_read_endpoint_returns_not_found()
     {
         $this->actingAsApiEditor();
index 6af9c2a7a53b8f7ac453c50c2098d4258a1355c3..e2a04b528ee43cac66dec437b28b8b5e046df569 100644 (file)
@@ -3,7 +3,9 @@
 namespace Tests\Api;
 
 use BookStack\Activity\ActivityType;
+use BookStack\Activity\Models\Activity as ActivityModel;
 use BookStack\Entities\Models\Entity;
+use BookStack\Facades\Activity;
 use BookStack\Notifications\UserInvite;
 use BookStack\Users\Models\Role;
 use BookStack\Users\Models\User;
@@ -67,6 +69,27 @@ class UsersApiTest extends TestCase
         ]]);
     }
 
+    public function test_index_endpoint_has_correct_created_and_last_activity_dates()
+    {
+        $user = $this->users->editor();
+        $user->created_at = now()->subYear();
+        $user->save();
+
+        $this->actingAs($user);
+        Activity::add(ActivityType::AUTH_LOGIN, 'test login activity');
+        /** @var ActivityModel $activity */
+        $activity = ActivityModel::query()->where('user_id', '=', $user->id)->latest()->first();
+
+        $resp = $this->asAdmin()->getJson($this->baseEndpoint . '?filter[id]=3');
+        $resp->assertJson(['data' => [
+            [
+                'id'          => $user->id,
+                'created_at' => $user->created_at->toJSON(),
+                'last_activity_at' => $activity->created_at->toJson(),
+            ],
+        ]]);
+    }
+
     public function test_create_endpoint()
     {
         $this->actingAsApiAdmin();
index a10a56e9ef338355a8434069e48705fb2e256453..c1842c175a791c335810179a7111c5c49ae31a5e 100644 (file)
@@ -196,6 +196,31 @@ class BookShelfTest extends TestCase
         $this->withHtml($resp)->assertElementContains('.book-content a.grid-card:nth-child(3)', 'adsfsdfsdfsd');
     }
 
+    public function test_shelf_view_sorts_by_name_case_insensitively()
+    {
+        $shelf = Bookshelf::query()->whereHas('books')->with('books')->first();
+        $books = Book::query()->take(3)->get(['id', 'name']);
+        $books[0]->fill(['name' => 'Book Ab'])->save();
+        $books[1]->fill(['name' => 'Book ac'])->save();
+        $books[2]->fill(['name' => 'Book AD'])->save();
+
+        // Set book ordering
+        $this->asAdmin()->put($shelf->getUrl(), [
+            'books' => $books->implode('id', ','),
+            'tags'  => [], 'description' => 'abc', 'name' => 'abc',
+        ]);
+        $this->assertEquals(3, $shelf->books()->count());
+        $shelf->refresh();
+
+        setting()->putUser($this->users->editor(), 'shelf_books_sort', 'name');
+        setting()->putUser($this->users->editor(), 'shelf_books_sort_order', 'asc');
+        $html = $this->withHtml($this->asEditor()->get($shelf->getUrl()));
+
+        $html->assertElementContains('.book-content a.grid-card:nth-child(1)', 'Book Ab');
+        $html->assertElementContains('.book-content a.grid-card:nth-child(2)', 'Book ac');
+        $html->assertElementContains('.book-content a.grid-card:nth-child(3)', 'Book AD');
+    }
+
     public function test_shelf_edit()
     {
         $shelf = $this->entities->shelf();
index a04933ada6d653c61027cb3768d729048a5695ab..b3e9f3cd0ed40413336204fedb0e4e632587e174 100644 (file)
@@ -135,4 +135,14 @@ class CommentTest extends TestCase
         $respHtml->assertElementCount('.comment-branch', 4);
         $respHtml->assertElementContains('.comment-branch .comment-branch', 'My nested comment');
     }
+
+    public function test_comments_are_visible_in_the_page_editor()
+    {
+        $page = $this->entities->page();
+
+        $this->asAdmin()->postJson("/comment/$page->id", ['text' => 'My great comment to see in the editor']);
+
+        $respHtml = $this->withHtml($this->get($page->getUrl('/edit')));
+        $respHtml->assertElementContains('.comment-box .content', 'My great comment to see in the editor');
+    }
 }
index 3c4bf4a77311fa4d7b925f5db740b484795d641e..035546593d376bbf9a45d8467fb084b5d97e931c 100644 (file)
@@ -413,6 +413,15 @@ class EntityPermissionsTest extends TestCase
         $this->entityRestrictionFormTest(Page::class, 'Page Permissions', 'delete', '2');
     }
 
+    public function test_shelf_create_permission_not_visible()
+    {
+        $shelf = $this->entities->shelf();
+
+        $resp = $this->asAdmin()->get($shelf->getUrl('/permissions'));
+        $html = $this->withHtml($resp);
+        $html->assertElementNotExists('input[name$="[create]"]');
+    }
+
     public function test_restricted_pages_not_visible_in_book_navigation_on_pages()
     {
         $chapter = $this->entities->chapter();
index 10376751633c5e225a1451cc361ddb41870ae2e5..bfd35c6b1e134a4be575d72d0f247d0a3587245d 100644 (file)
@@ -5,6 +5,7 @@ namespace Tests\Unit;
 use Illuminate\Support\Facades\Log;
 use Illuminate\Support\Facades\Mail;
 use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport;
+use Symfony\Component\Mailer\Transport\Smtp\Stream\SocketStream;
 use Tests\TestCase;
 
 /**
@@ -122,6 +123,45 @@ class ConfigTest extends TestCase
         });
     }
 
+    public function test_non_null_mail_encryption_options_enforce_smtp_scheme()
+    {
+        $this->checkEnvConfigResult('MAIL_ENCRYPTION', 'tls', 'mail.mailers.smtp.scheme', 'smtps');
+        $this->checkEnvConfigResult('MAIL_ENCRYPTION', 'ssl', 'mail.mailers.smtp.scheme', 'smtps');
+        $this->checkEnvConfigResult('MAIL_ENCRYPTION', 'null', 'mail.mailers.smtp.scheme', null);
+    }
+
+    public function test_smtp_scheme_and_certain_port_forces_tls_usage()
+    {
+        $isMailTlsForcedEnabled = function () {
+            $transport = Mail::mailer('smtp')->getSymfonyTransport();
+            /** @var SocketStream $stream */
+            $stream = $transport->getStream();
+            Mail::purge('smtp');
+            return $stream->isTLS();
+        };
+
+        config()->set([
+            'mail.mailers.smtp.scheme' => null,
+            'mail.mailers.smtp.port' => 587,
+        ]);
+
+        $this->assertFalse($isMailTlsForcedEnabled());
+
+        config()->set([
+            'mail.mailers.smtp.scheme' => 'smtps',
+            'mail.mailers.smtp.port' => 587,
+        ]);
+
+        $this->assertTrue($isMailTlsForcedEnabled());
+
+        config()->set([
+            'mail.mailers.smtp.scheme' => '',
+            'mail.mailers.smtp.port' => 465,
+        ]);
+
+        $this->assertTrue($isMailTlsForcedEnabled());
+    }
+
     /**
      * Set an environment variable of the given name and value
      * then check the given config key to see if it matches the given result.
index 2217cd2bafffa28a79e36c86e2a37a557b7e6ed5..363c1fa95420b858d42980f27e05d8e38e10a07a 100644 (file)
@@ -4,6 +4,7 @@ namespace Tests\Uploads;
 
 use BookStack\Exceptions\HttpFetchException;
 use BookStack\Uploads\HttpFetcher;
+use BookStack\Uploads\UserAvatars;
 use BookStack\Users\Models\User;
 use Tests\TestCase;
 
@@ -110,4 +111,28 @@ class AvatarTest extends TestCase
         $this->createUserRequest($user);
         $this->assertTrue($logger->hasError('Failed to save user avatar image'));
     }
+
+    public function test_exception_message_on_failed_fetch()
+    {
+        // set wrong url
+        config()->set([
+            'services.disable_services' => false,
+            'services.avatar_url'       => 'http_malformed_url/${email}/${hash}/${size}',
+        ]);
+
+        $user = User::factory()->make();
+        $avatar = app()->make(UserAvatars::class);
+        $url = 'http_malformed_url/' . urlencode(strtolower($user->email)) . '/' . md5(strtolower($user->email)) . '/500';
+        $logger = $this->withTestLogger();
+
+        $avatar->fetchAndAssignToUser($user);
+
+        $this->assertTrue($logger->hasError('Failed to save user avatar image'));
+        $exception = $logger->getRecords()[0]['context']['exception'];
+        $this->assertEquals(new HttpFetchException(
+            'Cannot get image from ' . $url,
+            6,
+            (new HttpFetchException('Could not resolve host: http_malformed_url', 6))
+        ), $exception);
+    }
 }