]> BookStack Code Mirror - bookstack/commitdiff
Merge branch 'v23-08' into development
authorDan Brown <redacted>
Sat, 16 Sep 2023 10:55:57 +0000 (11:55 +0100)
committerDan Brown <redacted>
Sat, 16 Sep 2023 10:55:57 +0000 (11:55 +0100)
61 files changed:
.env.example.complete
.github/ISSUE_TEMPLATE/bug_report.yml
.github/ISSUE_TEMPLATE/feature_request.yml
.github/ISSUE_TEMPLATE/support_request.yml
.github/translators.txt
app/Access/EmailConfirmationService.php
app/Access/Notifications/ConfirmEmailNotification.php [moved from app/Notifications/ConfirmEmail.php with 82% similarity]
app/Access/Notifications/ResetPasswordNotification.php [moved from app/Notifications/ResetPassword.php with 81% similarity]
app/Access/Notifications/UserInviteNotification.php [moved from app/Notifications/UserInvite.php with 85% similarity]
app/Access/Oidc/OidcOAuthProvider.php
app/Access/Oidc/OidcProviderSettings.php
app/Access/Oidc/OidcService.php
app/Access/UserInviteService.php
app/Activity/Controllers/FavouriteController.php
app/Activity/Controllers/WatchController.php
app/Activity/DispatchWebhookJob.php
app/Activity/Notifications/Messages/BaseActivityNotification.php
app/App/MailNotification.php [moved from app/Notifications/MailNotification.php with 96% similarity]
app/App/Providers/AppServiceProvider.php
app/App/Providers/EventServiceProvider.php
app/Config/mail.php
app/Config/oidc.php
app/Console/Commands/CleanupImagesCommand.php
app/Entities/Tools/MixedEntityRequestHelper.php [new file with mode: 0644]
app/Exceptions/ThemeException.php [new file with mode: 0644]
app/Http/HttpClientHistory.php [new file with mode: 0644]
app/Http/HttpRequestService.php [new file with mode: 0644]
app/Settings/MaintenanceController.php
app/Settings/TestEmailNotification.php [moved from app/Notifications/TestEmail.php with 78% similarity]
app/Theming/ThemeService.php
app/Translation/LanguageManager.php
app/Uploads/HttpFetcher.php [deleted file]
app/Uploads/UserAvatars.php
app/Users/Controllers/UserPreferencesController.php
app/Users/Models/User.php
composer.json
composer.lock
lang/en/settings.php
readme.md
resources/views/books/show.blade.php
resources/views/entities/favourite-action.blade.php
resources/views/entities/watch-action.blade.php
resources/views/entities/watch-controls.blade.php
resources/views/home/default.blade.php
resources/views/layouts/base.blade.php
resources/views/shelves/show.blade.php
tests/Actions/WebhookCallTest.php
tests/Activity/WatchTest.php
tests/Api/UsersApiTest.php
tests/Auth/OidcTest.php
tests/Auth/RegistrationTest.php
tests/Auth/ResetPasswordTest.php
tests/Auth/UserInviteTest.php
tests/Commands/CleanupImagesCommandTest.php
tests/Entity/CommentTest.php
tests/FavouriteTest.php
tests/Settings/TestEmailTest.php
tests/TestCase.php
tests/ThemeTest.php
tests/Uploads/AvatarTest.php
tests/User/UserPreferencesTest.php

index 547e81818823db4a48ba30311e9692c0548b937a..0853bd1fecbba729352d46e46ed2c8ec9dde0d4c 100644 (file)
@@ -72,7 +72,7 @@ MYSQL_ATTR_SSL_CA="/path/to/ca.pem"
 # Mail configuration
 # Refer to https://www.bookstackapp.com/docs/admin/email-webhooks/#email-configuration
 MAIL_DRIVER=smtp
-MAIL_FROM=mail@bookstackapp.com
+MAIL_FROM=bookstack@example.com
 MAIL_FROM_NAME=BookStack
 
 MAIL_HOST=localhost
index 5c8734fd6c1d0de6d18e8e9757324e4e4a97ae13..d301826c9c6ee6c02bdb75311724a74b40e732ce 100644 (file)
@@ -1,7 +1,14 @@
 name: Bug Report
-description: Create a report to help us improve or fix things
+description: Create a report to help us fix bugs & issues in existing supported functionality
 labels: [":bug: Bug"]
 body:
+  - type: markdown
+    attributes:
+      value: |
+        Thanks for taking the time to fill out a bug report!
+        Please note that this form is for reporting bugs in existing supported functionality.
+        
+        If you are reporting something that's not an issue in functionality we've previously supported and/or is simply something different to your expectations, then it may be more appropriate to raise via a feature or support request instead.
   - type: textarea
     id: description
     attributes:
@@ -13,7 +20,7 @@ body:
     id: reproduction
     attributes:
       label: Steps to Reproduce
-      description: Detail the steps that would replicate this issue
+      description: Detail the steps that would replicate this issue.
       placeholder: |
         1. Go to '...'
         2. Click on '....'
@@ -32,7 +39,7 @@ body:
     id: context
     attributes:
       label: Screenshots or Additional Context
-      description: Provide any additional context and screenshots here to help us solve this issue
+      description: Provide any additional context and screenshots here to help us solve this issue.
     validations:
       required: false
   - type: input
@@ -48,23 +55,7 @@ body:
     id: bsversion
     attributes:
       label: Exact BookStack Version
-      description: This can be found in the settings view of BookStack. Please provide an exact version.
-      placeholder: (eg. v21.08.5)
-    validations:
-      required: true
-  - type: input
-    id: phpversion
-    attributes:
-      label: PHP Version
-      description: Keep in mind your command-line PHP version may differ to that of your webserver. Provide that relevant to the issue.
-      placeholder: (eg. 7.4)
-    validations:
-      required: false
-  - type: textarea
-    id: hosting
-    attributes:
-      label: Hosting Environment
-      description: Describe your hosting environment as much as possible including any proxies used (If applicable).
-      placeholder: (eg. Ubuntu 20.04 VPS, installed using official installation script)
+      description: This can be found in the settings view of BookStack. Please provide an exact version(s) you've tested on.
+      placeholder: (eg. v23.06.7)
     validations:
       required: true
index fb1e0b3b047d8cf5e71916ea696137cbad09d4a9..0ebb8e72f291268d547f5c5125fd1a7008747187 100644 (file)
@@ -33,9 +33,9 @@ body:
     attributes:
       label: Have you searched for an existing open/closed issue?
       description: |
-        To help us keep these issues under control, please ensure you have first [searched our issue list](https://github.com/BookStackApp/BookStack/issues?q=is%3Aissue) for any existing issues that cover the fundemental benefit/goal of your request.
+        To help us keep these issues under control, please ensure you have first [searched our issue list](https://github.com/BookStackApp/BookStack/issues?q=is%3Aissue) for any existing issues that cover the fundamental benefit/goal of your request.
       options:
-        - label: I have searched for existing issues and none cover my fundemental request
+        - label: I have searched for existing issues and none cover my fundamental request
           required: true
   - type: dropdown
     id: existing_usage
@@ -43,8 +43,8 @@ body:
       label: How long have you been using BookStack?
       options:
         - Not using yet, just scoping
-        - 0 to 6 months
-        - 6 months to 1 year
+        - Under 3 months
+        - 3 months to 1 year
         - 1 to 5 years
         - Over 5 years
     validations:
index cb247654683bdad522694b43d2009210595afaed..4db8d1cedc5b28e6de08609ba3faa7050db7eb46 100644 (file)
@@ -33,7 +33,7 @@ body:
     attributes:
       label: Exact BookStack Version
       description: This can be found in the settings view of BookStack. Please provide an exact version.
-      placeholder: (eg. v21.08.5)
+      placeholder: (eg. v23.06.7)
     validations:
       required: true
   - type: textarea
@@ -44,19 +44,11 @@ body:
       placeholder: Be sure to remove any confidential details in your logs
     validations:
       required: false
-  - type: input
-    id: phpversion
-    attributes:
-      label: PHP Version
-      description: Keep in mind your command-line PHP version may differ to that of your webserver. Provide that most relevant to the issue.
-      placeholder: (eg. 7.4)
-    validations:
-      required: false
   - type: textarea
     id: hosting
     attributes:
       label: Hosting Environment
       description: Describe your hosting environment as much as possible including any proxies used (If applicable).
-      placeholder: (eg. Ubuntu 20.04 VPS, installed using official installation script)
+      placeholder: (eg. PHP8.1 on Ubuntu 22.04 VPS, installed using official installation script)
     validations:
       required: true
index 3f73d71838521856b022c4006b1e5c6188c58425..e077301836d047c84c5fcc0c78f46b6068ec0319 100644 (file)
@@ -57,6 +57,7 @@ Name :: Languages
 @Jokuna :: Korean
 @smartshogu :: German; German Informal
 @samadha56 :: Persian
+@mrmuminov :: Uzbek
 cipi1965 :: Italian
 Mykola Ronik (Mantikor) :: Ukrainian
 furkanoyk :: Turkish
@@ -289,7 +290,7 @@ Ismael Mesquita (mesquitoliveira) :: Portuguese, Brazilian
 LiZerui (CNLiZerui) :: Chinese Traditional
 Fabrice Boyer (FabriceBoyer) :: French
 mikael (bitcanon) :: Swedish
-Matthias Mai (schnapsidee) :: German; German Informal
+Matthias Mai (schnapsidee) :: German Informal; German
 Ufuk Ayyıldız (ufukayyildiz) :: Turkish
 Jan Mitrof (jan.kachlik) :: Czech
 edwardsmirnov :: Russian
@@ -347,7 +348,7 @@ robing29 :: German
 Bruno Eduardo de Jesus Barroso (brunoejb) :: Portuguese, Brazilian
 Igor V Belousov (biv) :: Russian
 David Bauer (davbauer) :: German
-Guttorm Hveem (guttormhveem) :: Norwegian Bokmal
+Guttorm Hveem (guttormhveem) :: Norwegian Bokmal; Norwegian Nynorsk
 Minh Giang Truong (minhgiang1204) :: Vietnamese
 Ioannis Ioannides (i.ioannides) :: Greek
 Vadim (vadrozh) :: Russian
@@ -357,3 +358,5 @@ Dženan (Dzenan) :: Swedish
 Péter Péli (peter.peli) :: Hungarian
 TWME :: Chinese Traditional
 Sascha (Man-in-Black) :: German
+Mohammadreza Madadi (madadi.efl) :: Persian
+Konstantin Kovacheli (kkovacheli) :: Ukrainian
index 79220f996669d67a10d8979f0f50b8387d1125b9..de9ea69b882302236760a8202f62880eb77622f8 100644 (file)
@@ -2,8 +2,8 @@
 
 namespace BookStack\Access;
 
+use BookStack\Access\Notifications\ConfirmEmailNotification;
 use BookStack\Exceptions\ConfirmationEmailException;
-use BookStack\Notifications\ConfirmEmail;
 use BookStack\Users\Models\User;
 
 class EmailConfirmationService extends UserTokenService
@@ -26,7 +26,7 @@ class EmailConfirmationService extends UserTokenService
         $this->deleteByUser($user);
         $token = $this->createTokenForUser($user);
 
-        $user->notify(new ConfirmEmail($token));
+        $user->notify(new ConfirmEmailNotification($token));
     }
 
     /**
similarity index 82%
rename from app/Notifications/ConfirmEmail.php
rename to app/Access/Notifications/ConfirmEmailNotification.php
index 6b395e137f075490b4073505c95c4be69ffad92f..c67e1cf62839f49680d4da1e7dc20f84b66d0b2e 100644 (file)
@@ -1,11 +1,12 @@
 <?php
 
-namespace BookStack\Notifications;
+namespace BookStack\Access\Notifications;
 
+use BookStack\App\MailNotification;
 use BookStack\Users\Models\User;
 use Illuminate\Notifications\Messages\MailMessage;
 
-class ConfirmEmail extends MailNotification
+class ConfirmEmailNotification extends MailNotification
 {
     public function __construct(
         public string $token
similarity index 81%
rename from app/Notifications/ResetPassword.php
rename to app/Access/Notifications/ResetPasswordNotification.php
index 2d7e9c361332f38329b83aa9adb788a500946653..0a0f4ceb783da7d71a1926fd8fedfa7066edc86c 100644 (file)
@@ -1,11 +1,12 @@
 <?php
 
-namespace BookStack\Notifications;
+namespace BookStack\Access\Notifications;
 
+use BookStack\App\MailNotification;
 use BookStack\Users\Models\User;
 use Illuminate\Notifications\Messages\MailMessage;
 
-class ResetPassword extends MailNotification
+class ResetPasswordNotification extends MailNotification
 {
     public function __construct(
         public string $token
similarity index 85%
rename from app/Notifications/UserInvite.php
rename to app/Access/Notifications/UserInviteNotification.php
index 87ea31c04b2b1b19ad37ae282572c8c4d28c1ca8..b453fc95da29a120c746bb1e843c7a11b124221b 100644 (file)
@@ -1,11 +1,12 @@
 <?php
 
-namespace BookStack\Notifications;
+namespace BookStack\Access\Notifications;
 
+use BookStack\App\MailNotification;
 use BookStack\Users\Models\User;
 use Illuminate\Notifications\Messages\MailMessage;
 
-class UserInvite extends MailNotification
+class UserInviteNotification extends MailNotification
 {
     public function __construct(
         public string $token
index 2ed8cd5c9726ff40a41f33301b4be508425cfd5b..d2dc829b729ef4e342853e29fe2c87e35f236afe 100644 (file)
@@ -20,15 +20,8 @@ class OidcOAuthProvider extends AbstractProvider
 {
     use BearerAuthorizationTrait;
 
-    /**
-     * @var string
-     */
-    protected $authorizationEndpoint;
-
-    /**
-     * @var string
-     */
-    protected $tokenEndpoint;
+    protected string $authorizationEndpoint;
+    protected string $tokenEndpoint;
 
     /**
      * Scopes to use for the OIDC authorization call.
@@ -60,7 +53,7 @@ class OidcOAuthProvider extends AbstractProvider
     }
 
     /**
-     * Add an additional scope to this provider upon the default.
+     * Add another scope to this provider upon the default.
      */
     public function addScope(string $scope): void
     {
index 9c8b1b2647241f11830ce46bc8af7a526fd78307..fa3f579b18a8db4a667fb4597d71f00c85743db6 100644 (file)
@@ -59,7 +59,7 @@ class OidcProviderSettings
             }
         }
 
-        if (strpos($this->issuer, 'https://') !== 0) {
+        if (!str_starts_with($this->issuer, 'https://')) {
             throw new InvalidArgumentException('Issuer value must start with https://');
         }
     }
index 6d13fe8f1691ac8ef6dddd64e733a5def0e56ffd..8778cbd98c2e5dcfc17e923368eb7626e0838146 100644 (file)
@@ -9,13 +9,13 @@ use BookStack\Exceptions\JsonDebugException;
 use BookStack\Exceptions\StoppedAuthenticationException;
 use BookStack\Exceptions\UserRegistrationException;
 use BookStack\Facades\Theme;
+use BookStack\Http\HttpRequestService;
 use BookStack\Theming\ThemeEvents;
 use BookStack\Users\Models\User;
 use Illuminate\Support\Arr;
 use Illuminate\Support\Facades\Cache;
 use League\OAuth2\Client\OptionProvider\HttpBasicAuthOptionProvider;
 use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
-use Psr\Http\Client\ClientInterface as HttpClient;
 
 /**
  * Class OpenIdConnectService
@@ -26,7 +26,7 @@ class OidcService
     public function __construct(
         protected RegistrationService $registrationService,
         protected LoginService $loginService,
-        protected HttpClient $httpClient,
+        protected HttpRequestService $http,
         protected GroupSyncService $groupService
     ) {
     }
@@ -94,7 +94,7 @@ class OidcService
         // Run discovery
         if ($config['discover'] ?? false) {
             try {
-                $settings->discoverFromIssuer($this->httpClient, Cache::store(null), 15);
+                $settings->discoverFromIssuer($this->http->buildClient(5), Cache::store(null), 15);
             } catch (OidcIssuerDiscoveryException $exception) {
                 throw new OidcException('OIDC Discovery Error: ' . $exception->getMessage());
             }
@@ -111,7 +111,7 @@ class OidcService
     protected function getProvider(OidcProviderSettings $settings): OidcOAuthProvider
     {
         $provider = new OidcOAuthProvider($settings->arrayForProvider(), [
-            'httpClient'     => $this->httpClient,
+            'httpClient'     => $this->http->buildClient(5),
             'optionProvider' => new HttpBasicAuthOptionProvider(),
         ]);
 
@@ -142,10 +142,11 @@ class OidcService
      */
     protected function getUserDisplayName(OidcIdToken $token, string $defaultValue): string
     {
-        $displayNameAttr = $this->config()['display_name_claims'];
+        $displayNameAttrString = $this->config()['display_name_claims'] ?? '';
+        $displayNameAttrs = explode('|', $displayNameAttrString);
 
         $displayName = [];
-        foreach ($displayNameAttr as $dnAttr) {
+        foreach ($displayNameAttrs as $dnAttr) {
             $dnComponent = $token->getClaim($dnAttr) ?? '';
             if ($dnComponent !== '') {
                 $displayName[] = $dnComponent;
index 00e361e827fe973398c77d99548eb32ad7900663..7d955f3851d763c199a0073f05c2d4a3e1366ef7 100644 (file)
@@ -2,7 +2,7 @@
 
 namespace BookStack\Access;
 
-use BookStack\Notifications\UserInvite;
+use BookStack\Access\Notifications\UserInviteNotification;
 use BookStack\Users\Models\User;
 
 class UserInviteService extends UserTokenService
@@ -18,6 +18,6 @@ class UserInviteService extends UserTokenService
     {
         $this->deleteByUser($user);
         $token = $this->createTokenForUser($user);
-        $user->notify(new UserInvite($token));
+        $user->notify(new UserInviteNotification($token));
     }
 }
index 1b88ffd643a463095ba0adb02a67647190ba6065..d2534ddfe090b9370f0b4f7b9a51eae512bb43f3 100644 (file)
@@ -6,11 +6,17 @@ use BookStack\Activity\Models\Favouritable;
 use BookStack\App\Model;
 use BookStack\Entities\Models\Entity;
 use BookStack\Entities\Queries\TopFavourites;
+use BookStack\Entities\Tools\MixedEntityRequestHelper;
 use BookStack\Http\Controller;
 use Illuminate\Http\Request;
 
 class FavouriteController extends Controller
 {
+    public function __construct(
+        protected MixedEntityRequestHelper $entityHelper,
+    ) {
+    }
+
     /**
      * Show a listing of all favourite items for the current user.
      */
@@ -36,13 +42,14 @@ class FavouriteController extends Controller
      */
     public function add(Request $request)
     {
-        $favouritable = $this->getValidatedModelFromRequest($request);
-        $favouritable->favourites()->firstOrCreate([
+        $modelInfo = $this->validate($request, $this->entityHelper->validationRules());
+        $entity = $this->entityHelper->getVisibleEntityFromRequestData($modelInfo);
+        $entity->favourites()->firstOrCreate([
             'user_id' => user()->id,
         ]);
 
         $this->showSuccessNotification(trans('activities.favourite_add_notification', [
-            'name' => $favouritable->name,
+            'name' => $entity->name,
         ]));
 
         return redirect()->back();
@@ -53,48 +60,16 @@ class FavouriteController extends Controller
      */
     public function remove(Request $request)
     {
-        $favouritable = $this->getValidatedModelFromRequest($request);
-        $favouritable->favourites()->where([
+        $modelInfo = $this->validate($request, $this->entityHelper->validationRules());
+        $entity = $this->entityHelper->getVisibleEntityFromRequestData($modelInfo);
+        $entity->favourites()->where([
             'user_id' => user()->id,
         ])->delete();
 
         $this->showSuccessNotification(trans('activities.favourite_remove_notification', [
-            'name' => $favouritable->name,
+            'name' => $entity->name,
         ]));
 
         return redirect()->back();
     }
-
-    /**
-     * @throws \Illuminate\Validation\ValidationException
-     * @throws \Exception
-     */
-    protected function getValidatedModelFromRequest(Request $request): Entity
-    {
-        $modelInfo = $this->validate($request, [
-            'type' => ['required', 'string'],
-            'id'   => ['required', 'integer'],
-        ]);
-
-        if (!class_exists($modelInfo['type'])) {
-            throw new \Exception('Model not found');
-        }
-
-        /** @var Model $model */
-        $model = new $modelInfo['type']();
-        if (!$model instanceof Favouritable) {
-            throw new \Exception('Model not favouritable');
-        }
-
-        $modelInstance = $model->newQuery()
-            ->where('id', '=', $modelInfo['id'])
-            ->first(['id', 'name', 'owned_by']);
-
-        $inaccessibleEntity = ($modelInstance instanceof Entity && !userCan('view', $modelInstance));
-        if (is_null($modelInstance) || $inaccessibleEntity) {
-            throw new \Exception('Model instance not found');
-        }
-
-        return $modelInstance;
-    }
 }
index 3d7e18116bec0b404bd8dd804480c23ab150376d..c0b1c58724926752c8656fc3685431f0e6361a9f 100644 (file)
@@ -3,25 +3,22 @@
 namespace BookStack\Activity\Controllers;
 
 use BookStack\Activity\Tools\UserEntityWatchOptions;
-use BookStack\App\Model;
-use BookStack\Entities\Models\Entity;
+use BookStack\Entities\Tools\MixedEntityRequestHelper;
 use BookStack\Http\Controller;
-use Exception;
 use Illuminate\Http\Request;
-use Illuminate\Validation\ValidationException;
 
 class WatchController extends Controller
 {
-    public function update(Request $request)
+    public function update(Request $request, MixedEntityRequestHelper $entityHelper)
     {
         $this->checkPermission('receive-notifications');
         $this->preventGuestAccess();
 
-        $requestData = $this->validate($request, [
+        $requestData = $this->validate($request, array_merge([
             'level' => ['required', 'string'],
-        ]);
+        ], $entityHelper->validationRules()));
 
-        $watchable = $this->getValidatedModelFromRequest($request);
+        $watchable = $entityHelper->getVisibleEntityFromRequestData($requestData);
         $watchOptions = new UserEntityWatchOptions(user(), $watchable);
         $watchOptions->updateLevelByName($requestData['level']);
 
@@ -29,37 +26,4 @@ class WatchController extends Controller
 
         return redirect()->back();
     }
-
-    /**
-     * @throws ValidationException
-     * @throws Exception
-     */
-    protected function getValidatedModelFromRequest(Request $request): Entity
-    {
-        $modelInfo = $this->validate($request, [
-            'type' => ['required', 'string'],
-            'id'   => ['required', 'integer'],
-        ]);
-
-        if (!class_exists($modelInfo['type'])) {
-            throw new Exception('Model not found');
-        }
-
-        /** @var Model $model */
-        $model = new $modelInfo['type']();
-        if (!$model instanceof Entity) {
-            throw new Exception('Model not an entity');
-        }
-
-        $modelInstance = $model->newQuery()
-            ->where('id', '=', $modelInfo['id'])
-            ->first(['id', 'name', 'owned_by']);
-
-        $inaccessibleEntity = ($modelInstance instanceof Entity && !userCan('view', $modelInstance));
-        if (is_null($modelInstance) || $inaccessibleEntity) {
-            throw new Exception('Model instance not found');
-        }
-
-        return $modelInstance;
-    }
 }
index 405bca49cbee925b574ff7ed7c574d702df9e62c..e1771b114cf4a5364be84d2e66bcba1c6767aad8 100644 (file)
@@ -6,6 +6,7 @@ use BookStack\Activity\Models\Loggable;
 use BookStack\Activity\Models\Webhook;
 use BookStack\Activity\Tools\WebhookFormatter;
 use BookStack\Facades\Theme;
+use BookStack\Http\HttpRequestService;
 use BookStack\Theming\ThemeEvents;
 use BookStack\Users\Models\User;
 use BookStack\Util\SsrUrlValidator;
@@ -14,7 +15,6 @@ use Illuminate\Contracts\Queue\ShouldQueue;
 use Illuminate\Foundation\Bus\Dispatchable;
 use Illuminate\Queue\InteractsWithQueue;
 use Illuminate\Queue\SerializesModels;
-use Illuminate\Support\Facades\Http;
 use Illuminate\Support\Facades\Log;
 
 class DispatchWebhookJob implements ShouldQueue
@@ -49,25 +49,28 @@ class DispatchWebhookJob implements ShouldQueue
      *
      * @return void
      */
-    public function handle()
+    public function handle(HttpRequestService $http)
     {
         $lastError = null;
 
         try {
             (new SsrUrlValidator())->ensureAllowed($this->webhook->endpoint);
 
-            $response = Http::asJson()
-                ->withOptions(['allow_redirects' => ['strict' => true]])
-                ->timeout($this->webhook->timeout)
-                ->post($this->webhook->endpoint, $this->webhookData);
-        } catch (\Exception $exception) {
-            $lastError = $exception->getMessage();
-            Log::error("Webhook call to endpoint {$this->webhook->endpoint} failed with error \"{$lastError}\"");
-        }
+            $client = $http->buildClient($this->webhook->timeout, [
+                'connect_timeout' => 10,
+                'allow_redirects' => ['strict' => true],
+            ]);
 
-        if (isset($response) && $response->failed()) {
-            $lastError = "Response status from endpoint was {$response->status()}";
-            Log::error("Webhook call to endpoint {$this->webhook->endpoint} failed with status {$response->status()}");
+            $response = $client->sendRequest($http->jsonRequest('POST', $this->webhook->endpoint, $this->webhookData));
+            $statusCode = $response->getStatusCode();
+
+            if ($statusCode >= 400) {
+                $lastError = "Response status from endpoint was {$statusCode}";
+                Log::error("Webhook call to endpoint {$this->webhook->endpoint} failed with status {$statusCode}");
+            }
+        } catch (\Exception $error) {
+            $lastError = $error->getMessage();
+            Log::error("Webhook call to endpoint {$this->webhook->endpoint} failed with error \"{$lastError}\"");
         }
 
         $this->webhook->last_called_at = now();
index a2045c8dcd4e7117fc32dcf4026538276ac63aec..414859091f12f86a3ef17212d65e02c146e02b59 100644 (file)
@@ -4,7 +4,7 @@ namespace BookStack\Activity\Notifications\Messages;
 
 use BookStack\Activity\Models\Loggable;
 use BookStack\Activity\Notifications\MessageParts\LinkedMailMessageLine;
-use BookStack\Notifications\MailNotification;
+use BookStack\App\MailNotification;
 use BookStack\Users\Models\User;
 use Illuminate\Bus\Queueable;
 
similarity index 96%
rename from app/Notifications/MailNotification.php
rename to app/App/MailNotification.php
index 1f40219f9201790a3e914007c352130e7a9509b2..8c57b5621f1987e04cd4782c6018d9bb73ac8505 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 
-namespace BookStack\Notifications;
+namespace BookStack\App;
 
 use BookStack\Users\Models\User;
 use Illuminate\Bus\Queueable;
index deb664ba697d2ff639b42b00c812f4503d1286dc..0275a54891a18c70f5fa5cb852b754d94aad0f38 100644 (file)
@@ -9,16 +9,15 @@ use BookStack\Entities\Models\Bookshelf;
 use BookStack\Entities\Models\Chapter;
 use BookStack\Entities\Models\Page;
 use BookStack\Exceptions\BookStackExceptionHandlerPage;
+use BookStack\Http\HttpRequestService;
 use BookStack\Permissions\PermissionApplicator;
 use BookStack\Settings\SettingService;
 use BookStack\Util\CspService;
-use GuzzleHttp\Client;
 use Illuminate\Contracts\Foundation\ExceptionRenderer;
 use Illuminate\Database\Eloquent\Relations\Relation;
 use Illuminate\Support\Facades\Schema;
 use Illuminate\Support\Facades\URL;
 use Illuminate\Support\ServiceProvider;
-use Psr\Http\Client\ClientInterface as HttpClientInterface;
 
 class AppServiceProvider extends ServiceProvider
 {
@@ -39,6 +38,7 @@ class AppServiceProvider extends ServiceProvider
         SettingService::class => SettingService::class,
         SocialAuthService::class => SocialAuthService::class,
         CspService::class => CspService::class,
+        HttpRequestService::class => HttpRequestService::class,
     ];
 
     /**
@@ -51,7 +51,7 @@ class AppServiceProvider extends ServiceProvider
         // Set root URL
         $appUrl = config('app.url');
         if ($appUrl) {
-            $isHttps = (strpos($appUrl, 'https://') === 0);
+            $isHttps = str_starts_with($appUrl, 'https://');
             URL::forceRootUrl($appUrl);
             URL::forceScheme($isHttps ? 'https' : 'http');
         }
@@ -75,12 +75,6 @@ class AppServiceProvider extends ServiceProvider
      */
     public function register()
     {
-        $this->app->bind(HttpClientInterface::class, function ($app) {
-            return new Client([
-                'timeout' => 3,
-            ]);
-        });
-
         $this->app->singleton(PermissionApplicator::class, function ($app) {
             return new PermissionApplicator(null);
         });
index bf502d7dab2ea5ace1fff2bbf812ec6755db6430..4ec9facdfd7b82c793f3a57257327e604f009f85 100644 (file)
@@ -3,7 +3,12 @@
 namespace BookStack\App\Providers;
 
 use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
+use SocialiteProviders\Azure\AzureExtendSocialite;
+use SocialiteProviders\Discord\DiscordExtendSocialite;
+use SocialiteProviders\GitLab\GitLabExtendSocialite;
 use SocialiteProviders\Manager\SocialiteWasCalled;
+use SocialiteProviders\Okta\OktaExtendSocialite;
+use SocialiteProviders\Twitch\TwitchExtendSocialite;
 
 class EventServiceProvider extends ServiceProvider
 {
@@ -14,12 +19,11 @@ class EventServiceProvider extends ServiceProvider
      */
     protected $listen = [
         SocialiteWasCalled::class => [
-            'SocialiteProviders\Slack\SlackExtendSocialite@handle',
-            'SocialiteProviders\Azure\AzureExtendSocialite@handle',
-            'SocialiteProviders\Okta\OktaExtendSocialite@handle',
-            'SocialiteProviders\GitLab\GitLabExtendSocialite@handle',
-            'SocialiteProviders\Twitch\TwitchExtendSocialite@handle',
-            'SocialiteProviders\Discord\DiscordExtendSocialite@handle',
+            AzureExtendSocialite::class . '@handle',
+            OktaExtendSocialite::class . '@handle',
+            GitLabExtendSocialite::class . '@handle',
+            TwitchExtendSocialite::class . '@handle',
+            DiscordExtendSocialite::class . '@handle',
         ],
     ];
 
index 6400211e8940447f50d2b5e6d262641d1a5a8ace..2906d769afd929cac8d76f3d502472d6bda4d438 100644 (file)
@@ -22,7 +22,7 @@ return [
 
     // Global "From" address & name
     'from' => [
-        'address' => env('MAIL_FROM', 'mail@bookstackapp.com'),
+        'address' => env('MAIL_FROM', 'bookstack@example.com'),
         'name'    => env('MAIL_FROM_NAME', 'BookStack'),
     ],
 
index 1f73fb688662e8b72ea69700af2f1d4fa7138e31..b28b8a41a826a8faf5df82767206aa0d350df752 100644 (file)
@@ -9,7 +9,7 @@ return [
     'dump_user_details' => env('OIDC_DUMP_USER_DETAILS', false),
 
     // Claim, within an OpenId token, to find the user's display name
-    'display_name_claims' => explode('|', env('OIDC_DISPLAY_NAME_CLAIMS', 'name')),
+    'display_name_claims' => env('OIDC_DISPLAY_NAME_CLAIMS', 'name'),
 
     // Claim, within an OpenID token, to use to connect a BookStack user to the OIDC user.
     'external_id_claim' => env('OIDC_EXTERNAL_ID_CLAIM', 'sub'),
index fe924b0f46b79af8c694c37bf98087ae8a0e272b..18e60ff1773be7b9e9abc849006652d2c251f13a 100644 (file)
@@ -35,7 +35,7 @@ class CleanupImagesCommand extends Command
 
         if (!$dryRun) {
             $this->warn("This operation is destructive and is not guaranteed to be fully accurate.\nEnsure you have a backup of your images.\n");
-            $proceed = $this->confirm("Are you sure you want to proceed?");
+            $proceed = !$this->input->isInteractive() || $this->confirm("Are you sure you want to proceed?");
             if (!$proceed) {
                 return 0;
             }
@@ -46,7 +46,7 @@ class CleanupImagesCommand extends Command
 
         if ($dryRun) {
             $this->comment('Dry run, no images have been deleted');
-            $this->comment($deleteCount . ' images found that would have been deleted');
+            $this->comment($deleteCount . ' image(s) found that would have been deleted');
             $this->showDeletedImages($deleted);
             $this->comment('Run with -f or --force to perform deletions');
 
@@ -54,7 +54,8 @@ class CleanupImagesCommand extends Command
         }
 
         $this->showDeletedImages($deleted);
-        $this->comment($deleteCount . ' images deleted');
+        $this->comment("{$deleteCount} image(s) deleted");
+
         return 0;
     }
 
@@ -65,7 +66,7 @@ class CleanupImagesCommand extends Command
         }
 
         if (count($paths) > 0) {
-            $this->line('Images to delete:');
+            $this->line('Image(s) to delete:');
         }
 
         foreach ($paths as $path) {
diff --git a/app/Entities/Tools/MixedEntityRequestHelper.php b/app/Entities/Tools/MixedEntityRequestHelper.php
new file mode 100644 (file)
index 0000000..8319f6a
--- /dev/null
@@ -0,0 +1,39 @@
+<?php
+
+namespace BookStack\Entities\Tools;
+
+use BookStack\Entities\EntityProvider;
+use BookStack\Entities\Models\Entity;
+
+class MixedEntityRequestHelper
+{
+    public function __construct(
+        protected EntityProvider $entities,
+    ) {
+    }
+
+    /**
+     * Query out an entity, visible to the current user, for the given
+     * entity request details (this provided in a request validated by
+     * this classes' validationRules method).
+     * @param array{type: string, id: string} $requestData
+     */
+    public function getVisibleEntityFromRequestData(array $requestData): Entity
+    {
+        $entityType = $this->entities->get($requestData['type']);
+
+        return $entityType->newQuery()->scopes(['visible'])->findOrFail($requestData['id']);
+    }
+
+    /**
+     * Get the validation rules for an abstract entity request.
+     * @return array{type: string[], id: string[]}
+     */
+    public function validationRules(): array
+    {
+        return [
+                'type' => ['required', 'string'],
+                'id'   => ['required', 'integer'],
+        ];
+    }
+}
diff --git a/app/Exceptions/ThemeException.php b/app/Exceptions/ThemeException.php
new file mode 100644 (file)
index 0000000..b721eff
--- /dev/null
@@ -0,0 +1,7 @@
+<?php
+
+namespace BookStack\Exceptions;
+
+class ThemeException extends \Exception
+{
+}
diff --git a/app/Http/HttpClientHistory.php b/app/Http/HttpClientHistory.php
new file mode 100644 (file)
index 0000000..f224b17
--- /dev/null
@@ -0,0 +1,33 @@
+<?php
+
+namespace BookStack\Http;
+
+use GuzzleHttp\Psr7\Request as GuzzleRequest;
+
+class HttpClientHistory
+{
+    public function __construct(
+        protected &$container
+    ) {
+    }
+
+    public function requestCount(): int
+    {
+        return count($this->container);
+    }
+
+    public function requestAt(int $index): ?GuzzleRequest
+    {
+        return $this->container[$index]['request'] ?? null;
+    }
+
+    public function latestRequest(): ?GuzzleRequest
+    {
+        return $this->requestAt($this->requestCount() - 1);
+    }
+
+    public function all(): array
+    {
+        return $this->container;
+    }
+}
diff --git a/app/Http/HttpRequestService.php b/app/Http/HttpRequestService.php
new file mode 100644 (file)
index 0000000..f59c298
--- /dev/null
@@ -0,0 +1,70 @@
+<?php
+
+namespace BookStack\Http;
+
+use GuzzleHttp\Client;
+use GuzzleHttp\Handler\MockHandler;
+use GuzzleHttp\HandlerStack;
+use GuzzleHttp\Middleware;
+use GuzzleHttp\Psr7\Request as GuzzleRequest;
+use GuzzleHttp\Psr7\Response;
+use Psr\Http\Client\ClientInterface;
+
+class HttpRequestService
+{
+    protected ?HandlerStack $handler = null;
+
+    /**
+     * Build a new http client for sending requests on.
+     */
+    public function buildClient(int $timeout, array $options = []): ClientInterface
+    {
+        $defaultOptions = [
+            'timeout' => $timeout,
+            'handler' => $this->handler,
+        ];
+
+        return new Client(array_merge($options, $defaultOptions));
+    }
+
+    /**
+     * Create a new JSON http request for use with a client.
+     */
+    public function jsonRequest(string $method, string $uri, array $data): GuzzleRequest
+    {
+        $headers = ['Content-Type' => 'application/json'];
+        return new GuzzleRequest($method, $uri, $headers, json_encode($data));
+    }
+
+    /**
+     * Mock any http clients built from this service, and response with the given responses.
+     * Returns history which can then be queried.
+     * @link https://docs.guzzlephp.org/en/stable/testing.html#history-middleware
+     */
+    public function mockClient(array $responses = [], bool $pad = true): HttpClientHistory
+    {
+        // By default, we pad out the responses with 10 successful values so that requests will be
+        // properly recorded for inspection. Otherwise, we can't later check if we're received
+        // too many requests.
+        if ($pad) {
+            $response = new Response(200, [], 'success');
+            $responses = array_merge($responses, array_fill(0, 10, $response));
+        }
+
+        $container = [];
+        $history = Middleware::history($container);
+        $mock = new MockHandler($responses);
+        $this->handler = HandlerStack::create($mock);
+        $this->handler->push($history, 'history');
+
+        return new HttpClientHistory($container);
+    }
+
+    /**
+     * Clear mocking that has been set up for clients.
+     */
+    public function clearMocking(): void
+    {
+        $this->handler = null;
+    }
+}
index 9c48a432326c13bfcc46f1faf92158ebcb5de174..60e5fee283ffee34f9e968c2cd6cf15b9552e425 100644 (file)
@@ -5,7 +5,6 @@ namespace BookStack\Settings;
 use BookStack\Activity\ActivityType;
 use BookStack\Entities\Tools\TrashCan;
 use BookStack\Http\Controller;
-use BookStack\Notifications\TestEmail;
 use BookStack\References\ReferenceStore;
 use BookStack\Uploads\ImageService;
 use Illuminate\Http\Request;
@@ -69,7 +68,7 @@ class MaintenanceController extends Controller
         $this->logActivity(ActivityType::MAINTENANCE_ACTION_RUN, 'send-test-email');
 
         try {
-            user()->notifyNow(new TestEmail());
+            user()->notifyNow(new TestEmailNotification());
             $this->showSuccessNotification(trans('settings.maint_send_test_email_success', ['address' => user()->email]));
         } catch (\Exception $exception) {
             $errorMessage = trans('errors.maintenance_test_email_failure') . "\n" . $exception->getMessage();
similarity index 78%
rename from app/Notifications/TestEmail.php
rename to app/Settings/TestEmailNotification.php
index af9e5847f1f04b3118e363262678de3eeed151dc..3b13543e4246cad02f10e5500066f07d6db9cc16 100644 (file)
@@ -1,11 +1,12 @@
 <?php
 
-namespace BookStack\Notifications;
+namespace BookStack\Settings;
 
+use BookStack\App\MailNotification;
 use BookStack\Users\Models\User;
 use Illuminate\Notifications\Messages\MailMessage;
 
-class TestEmail extends MailNotification
+class TestEmailNotification extends MailNotification
 {
     public function toMail(User $notifiable): MailMessage
     {
index bb91643e308114b567119fed99349b7e6951f98e..31a7d3c64d32c778020474fbcc43837467304c61 100644 (file)
@@ -3,19 +3,23 @@
 namespace BookStack\Theming;
 
 use BookStack\Access\SocialAuthService;
+use BookStack\Exceptions\ThemeException;
 use Illuminate\Console\Application;
 use Illuminate\Console\Application as Artisan;
 use Symfony\Component\Console\Command\Command;
 
 class ThemeService
 {
-    protected $listeners = [];
+    /**
+     * @var array<string, callable[]>
+     */
+    protected array $listeners = [];
 
     /**
      * Listen to a given custom theme event,
      * setting up the action to be ran when the event occurs.
      */
-    public function listen(string $event, callable $action)
+    public function listen(string $event, callable $action): void
     {
         if (!isset($this->listeners[$event])) {
             $this->listeners[$event] = [];
@@ -31,10 +35,8 @@ class ThemeService
      *
      * If a callback returns a non-null value, this method will
      * stop and return that value itself.
-     *
-     * @return mixed
      */
-    public function dispatch(string $event, ...$args)
+    public function dispatch(string $event, ...$args): mixed
     {
         foreach ($this->listeners[$event] ?? [] as $action) {
             $result = call_user_func_array($action, $args);
@@ -49,7 +51,7 @@ class ThemeService
     /**
      * Register a new custom artisan command to be available.
      */
-    public function registerCommand(Command $command)
+    public function registerCommand(Command $command): void
     {
         Artisan::starting(function (Application $application) use ($command) {
             $application->addCommands([$command]);
@@ -59,18 +61,22 @@ class ThemeService
     /**
      * Read any actions from the set theme path if the 'functions.php' file exists.
      */
-    public function readThemeActions()
+    public function readThemeActions(): void
     {
         $themeActionsFile = theme_path('functions.php');
         if ($themeActionsFile && file_exists($themeActionsFile)) {
-            require $themeActionsFile;
+            try {
+                require $themeActionsFile;
+            } catch (\Error $exception) {
+                throw new ThemeException("Failed loading theme functions file at \"{$themeActionsFile}\" with error: {$exception->getMessage()}");
+            }
         }
     }
 
     /**
      * @see SocialAuthService::addSocialDriver
      */
-    public function addSocialDriver(string $driverName, array $config, string $socialiteHandler, callable $configureForRedirect = null)
+    public function addSocialDriver(string $driverName, array $config, string $socialiteHandler, callable $configureForRedirect = null): void
     {
         $socialAuthService = app()->make(SocialAuthService::class);
         $socialAuthService->addSocialDriver($driverName, $config, $socialiteHandler, $configureForRedirect);
index 5132929b17e63edf1dadb9ddae1e4585f4e6b865..ec116fd70a4b620b46d081baf143f027dc27dcbb 100644 (file)
@@ -57,6 +57,7 @@ class LanguageManager
         'sl'          => ['iso' => 'sl_SI', 'windows' => 'Slovenian'],
         'sv'          => ['iso' => 'sv_SE', 'windows' => 'Swedish'],
         'uk'          => ['iso' => 'uk_UA', 'windows' => 'Ukrainian'],
+        'uz'          => ['iso' => 'uz_UZ', 'windows' => 'Uzbek'],
         'vi'          => ['iso' => 'vi_VN', 'windows' => 'Vietnamese'],
         'zh_CN'       => ['iso' => 'zh_CN', 'windows' => 'Chinese (Simplified)'],
         'zh_TW'       => ['iso' => 'zh_TW', 'windows' => 'Chinese (Traditional)'],
diff --git a/app/Uploads/HttpFetcher.php b/app/Uploads/HttpFetcher.php
deleted file mode 100644 (file)
index fcb4147..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-<?php
-
-namespace BookStack\Uploads;
-
-use BookStack\Exceptions\HttpFetchException;
-
-class HttpFetcher
-{
-    /**
-     * Fetch content from an external URI.
-     *
-     * @param string $uri
-     *
-     * @throws HttpFetchException
-     *
-     * @return bool|string
-     */
-    public function fetch(string $uri)
-    {
-        $ch = curl_init();
-        curl_setopt_array($ch, [
-            CURLOPT_URL            => $uri,
-            CURLOPT_RETURNTRANSFER => 1,
-            CURLOPT_CONNECTTIMEOUT => 5,
-        ]);
-
-        $data = curl_exec($ch);
-        $err = curl_error($ch);
-        curl_close($ch);
-
-        if ($err) {
-            $errno = curl_errno($ch);
-            throw new HttpFetchException($err, $errno);
-        }
-
-        return $data;
-    }
-}
index 3cd37812acbd40f14aec23ae2bb1d386a49df693..9692b3f38aff75936355c2c57143727c16067b80 100644 (file)
@@ -3,20 +3,20 @@
 namespace BookStack\Uploads;
 
 use BookStack\Exceptions\HttpFetchException;
+use BookStack\Http\HttpRequestService;
 use BookStack\Users\Models\User;
 use Exception;
+use GuzzleHttp\Psr7\Request;
 use Illuminate\Support\Facades\Log;
 use Illuminate\Support\Str;
+use Psr\Http\Client\ClientExceptionInterface;
 
 class UserAvatars
 {
-    protected $imageService;
-    protected $http;
-
-    public function __construct(ImageService $imageService, HttpFetcher $http)
-    {
-        $this->imageService = $imageService;
-        $this->http = $http;
+    public function __construct(
+        protected ImageService $imageService,
+        protected HttpRequestService $http
+    ) {
     }
 
     /**
@@ -112,8 +112,10 @@ class UserAvatars
     protected function getAvatarImageData(string $url): string
     {
         try {
-            $imageData = $this->http->fetch($url);
-        } catch (HttpFetchException $exception) {
+            $client = $this->http->buildClient(5);
+            $response = $client->sendRequest(new Request('GET', $url));
+            $imageData = (string) $response->getBody();
+        } catch (ClientExceptionInterface $exception) {
             throw new HttpFetchException(trans('errors.cannot_get_image_from_url', ['url' => $url]), $exception->getCode(), $exception);
         }
 
@@ -127,7 +129,7 @@ class UserAvatars
     {
         $fetchUrl = $this->getAvatarUrl();
 
-        return is_string($fetchUrl) && strpos($fetchUrl, 'http') === 0;
+        return str_starts_with($fetchUrl, 'http');
     }
 
     /**
index 9c38ff2af750fd7542102d7ae744b8aecf2649ec..08d65743b8c766b1e76e7da08b55b07d5de59c18 100644 (file)
@@ -145,7 +145,7 @@ class UserPreferencesController extends Controller
      */
     public function toggleDarkMode()
     {
-        $enabled = setting()->getForCurrentUser('dark-mode-enabled', false);
+        $enabled = setting()->getForCurrentUser('dark-mode-enabled');
         setting()->putForCurrentUser('dark-mode-enabled', $enabled ? 'false' : 'true');
 
         return redirect()->back();
index e3d856a8d9964aa1c3ac3db5dbb0e385c1bed700..2479521a2e91d86f046490ef346a488534001c7f 100644 (file)
@@ -3,6 +3,7 @@
 namespace BookStack\Users\Models;
 
 use BookStack\Access\Mfa\MfaValue;
+use BookStack\Access\Notifications\ResetPasswordNotification;
 use BookStack\Access\SocialAccount;
 use BookStack\Activity\Models\Favourite;
 use BookStack\Activity\Models\Loggable;
@@ -11,7 +12,6 @@ use BookStack\Api\ApiToken;
 use BookStack\App\Model;
 use BookStack\App\Sluggable;
 use BookStack\Entities\Tools\SlugGenerator;
-use BookStack\Notifications\ResetPassword;
 use BookStack\Translation\LanguageManager;
 use BookStack\Uploads\Image;
 use Carbon\Carbon;
@@ -345,7 +345,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
             return $splitName[0];
         }
 
-        return '';
+        return mb_substr($this->name, 0, max($chars - 2, 0)) . '…';
     }
 
     /**
@@ -365,7 +365,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
      */
     public function sendPasswordResetNotification($token)
     {
-        $this->notify(new ResetPassword($token));
+        $this->notify(new ResetPasswordNotification($token));
     }
 
     /**
index 28bdd54892189c1f51fe2883ec5537ddb5df7c1c..037f8984e9057e0b61ceaf821d2958321725a207 100644 (file)
@@ -23,7 +23,7 @@
         "guzzlehttp/guzzle": "^7.4",
         "intervention/image": "^2.7",
         "laravel/framework": "^9.0",
-        "laravel/socialite": "^5.2",
+        "laravel/socialite": "^5.8",
         "laravel/tinker": "^2.6",
         "league/commonmark": "^2.3",
         "league/flysystem-aws-s3-v3": "^3.0",
@@ -37,7 +37,6 @@
         "socialiteproviders/gitlab": "^4.1",
         "socialiteproviders/microsoft-azure": "^5.1",
         "socialiteproviders/okta": "^4.2",
-        "socialiteproviders/slack": "^4.1",
         "socialiteproviders/twitch": "^5.3",
         "ssddanbrown/htmldiff": "^1.0.2"
     },
index 04232b1a686afbd551bcdaf77db653985ab34c79..5c3ad09a8bb2e738547733b242af81af9365c381 100644 (file)
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "d010cf625b58a0dc43addda7881ea42f",
+    "content-hash": "db3cca7a93d7d097c9f517b6fe00ee7b",
     "packages": [
         {
             "name": "aws/aws-crt-php",
         },
         {
             "name": "aws/aws-sdk-php",
-            "version": "3.279.2",
+            "version": "3.281.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/aws/aws-sdk-php.git",
-                "reference": "ebd5e47c5be0425bb5cf4f80737850ed74767107"
+                "reference": "a3cfb20dbfb11117b7174b2594fc597745252e0c"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/ebd5e47c5be0425bb5cf4f80737850ed74767107",
-                "reference": "ebd5e47c5be0425bb5cf4f80737850ed74767107",
+                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/a3cfb20dbfb11117b7174b2594fc597745252e0c",
+                "reference": "a3cfb20dbfb11117b7174b2594fc597745252e0c",
                 "shasum": ""
             },
             "require": {
             "support": {
                 "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
                 "issues": "https://github.com/aws/aws-sdk-php/issues",
-                "source": "https://github.com/aws/aws-sdk-php/tree/3.279.2"
+                "source": "https://github.com/aws/aws-sdk-php/tree/3.281.3"
             },
-            "time": "2023-08-18T18:13:09+00:00"
+            "time": "2023-09-08T18:06:26+00:00"
         },
         {
             "name": "bacon/bacon-qr-code",
         },
         {
             "name": "dasprid/enum",
-            "version": "1.0.4",
+            "version": "1.0.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/DASPRiD/Enum.git",
-                "reference": "8e6b6ea76eabbf19ea2bf5b67b98e1860474012f"
+                "reference": "6faf451159fb8ba4126b925ed2d78acfce0dc016"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/8e6b6ea76eabbf19ea2bf5b67b98e1860474012f",
-                "reference": "8e6b6ea76eabbf19ea2bf5b67b98e1860474012f",
+                "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/6faf451159fb8ba4126b925ed2d78acfce0dc016",
+                "reference": "6faf451159fb8ba4126b925ed2d78acfce0dc016",
                 "shasum": ""
             },
             "require": {
             ],
             "support": {
                 "issues": "https://github.com/DASPRiD/Enum/issues",
-                "source": "https://github.com/DASPRiD/Enum/tree/1.0.4"
+                "source": "https://github.com/DASPRiD/Enum/tree/1.0.5"
             },
-            "time": "2023-03-01T18:44:03+00:00"
+            "time": "2023-08-25T16:18:39+00:00"
         },
         {
             "name": "dflydev/dot-access-data",
         },
         {
             "name": "guzzlehttp/guzzle",
-            "version": "7.7.0",
+            "version": "7.8.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/guzzle/guzzle.git",
-                "reference": "fb7566caccf22d74d1ab270de3551f72a58399f5"
+                "reference": "1110f66a6530a40fe7aea0378fe608ee2b2248f9"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/guzzle/guzzle/zipball/fb7566caccf22d74d1ab270de3551f72a58399f5",
-                "reference": "fb7566caccf22d74d1ab270de3551f72a58399f5",
+                "url": "https://api.github.com/repos/guzzle/guzzle/zipball/1110f66a6530a40fe7aea0378fe608ee2b2248f9",
+                "reference": "1110f66a6530a40fe7aea0378fe608ee2b2248f9",
                 "shasum": ""
             },
             "require": {
                 "ext-json": "*",
-                "guzzlehttp/promises": "^1.5.3 || ^2.0",
-                "guzzlehttp/psr7": "^1.9.1 || ^2.4.5",
+                "guzzlehttp/promises": "^1.5.3 || ^2.0.1",
+                "guzzlehttp/psr7": "^1.9.1 || ^2.5.1",
                 "php": "^7.2.5 || ^8.0",
                 "psr/http-client": "^1.0",
                 "symfony/deprecation-contracts": "^2.2 || ^3.0"
             ],
             "support": {
                 "issues": "https://github.com/guzzle/guzzle/issues",
-                "source": "https://github.com/guzzle/guzzle/tree/7.7.0"
+                "source": "https://github.com/guzzle/guzzle/tree/7.8.0"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-05-21T14:04:53+00:00"
+            "time": "2023-08-27T10:20:53+00:00"
         },
         {
             "name": "guzzlehttp/promises",
         },
         {
             "name": "guzzlehttp/psr7",
-            "version": "2.6.0",
+            "version": "2.6.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/guzzle/psr7.git",
-                "reference": "8bd7c33a0734ae1c5d074360512beb716bef3f77"
+                "reference": "be45764272e8873c72dbe3d2edcfdfcc3bc9f727"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/guzzle/psr7/zipball/8bd7c33a0734ae1c5d074360512beb716bef3f77",
-                "reference": "8bd7c33a0734ae1c5d074360512beb716bef3f77",
+                "url": "https://api.github.com/repos/guzzle/psr7/zipball/be45764272e8873c72dbe3d2edcfdfcc3bc9f727",
+                "reference": "be45764272e8873c72dbe3d2edcfdfcc3bc9f727",
                 "shasum": ""
             },
             "require": {
             ],
             "support": {
                 "issues": "https://github.com/guzzle/psr7/issues",
-                "source": "https://github.com/guzzle/psr7/tree/2.6.0"
+                "source": "https://github.com/guzzle/psr7/tree/2.6.1"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-08-03T15:06:02+00:00"
+            "time": "2023-08-27T10:13:57+00:00"
         },
         {
             "name": "guzzlehttp/uri-template",
-            "version": "v1.0.1",
+            "version": "v1.0.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/guzzle/uri-template.git",
-                "reference": "b945d74a55a25a949158444f09ec0d3c120d69e2"
+                "reference": "61bf437fc2197f587f6857d3ff903a24f1731b5d"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/guzzle/uri-template/zipball/b945d74a55a25a949158444f09ec0d3c120d69e2",
-                "reference": "b945d74a55a25a949158444f09ec0d3c120d69e2",
+                "url": "https://api.github.com/repos/guzzle/uri-template/zipball/61bf437fc2197f587f6857d3ff903a24f1731b5d",
+                "reference": "61bf437fc2197f587f6857d3ff903a24f1731b5d",
                 "shasum": ""
             },
             "require": {
                 "symfony/polyfill-php80": "^1.17"
             },
             "require-dev": {
+                "bamarni/composer-bin-plugin": "^1.8.1",
                 "phpunit/phpunit": "^8.5.19 || ^9.5.8",
                 "uri-template/tests": "1.0.0"
             },
             "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "1.0-dev"
-                }
-            },
             "autoload": {
                 "psr-4": {
                     "GuzzleHttp\\UriTemplate\\": "src"
             ],
             "support": {
                 "issues": "https://github.com/guzzle/uri-template/issues",
-                "source": "https://github.com/guzzle/uri-template/tree/v1.0.1"
+                "source": "https://github.com/guzzle/uri-template/tree/v1.0.2"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2021-10-07T12:57:01+00:00"
+            "time": "2023-08-27T10:19:19+00:00"
         },
         {
             "name": "intervention/image",
         },
         {
             "name": "knplabs/knp-snappy",
-            "version": "v1.4.2",
+            "version": "v1.4.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/KnpLabs/snappy.git",
-                "reference": "b66f79334421c26d9c244427963fa2d92980b5d3"
+                "reference": "d3b742d61a68bf93866032c2c0a7f1486128b67e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/KnpLabs/snappy/zipball/b66f79334421c26d9c244427963fa2d92980b5d3",
-                "reference": "b66f79334421c26d9c244427963fa2d92980b5d3",
+                "url": "https://api.github.com/repos/KnpLabs/snappy/zipball/d3b742d61a68bf93866032c2c0a7f1486128b67e",
+                "reference": "d3b742d61a68bf93866032c2c0a7f1486128b67e",
                 "shasum": ""
             },
             "require": {
             "require-dev": {
                 "friendsofphp/php-cs-fixer": "^2.16||^3.0",
                 "pedrotroller/php-cs-custom-fixer": "^2.19",
-                "phpstan/phpstan": "^0.12.7",
-                "phpstan/phpstan-phpunit": "^0.12.6",
+                "phpstan/phpstan": "^1.0.0",
+                "phpstan/phpstan-phpunit": "^1.0.0",
                 "phpunit/phpunit": "~7.4||~8.5"
             },
             "suggest": {
             ],
             "support": {
                 "issues": "https://github.com/KnpLabs/snappy/issues",
-                "source": "https://github.com/KnpLabs/snappy/tree/v1.4.2"
+                "source": "https://github.com/KnpLabs/snappy/tree/v1.4.3"
             },
-            "time": "2023-03-17T14:47:54+00:00"
+            "time": "2023-09-06T15:24:48+00:00"
         },
         {
             "name": "laravel/framework",
         },
         {
             "name": "laravel/socialite",
-            "version": "v5.8.0",
+            "version": "v5.9.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/laravel/socialite.git",
-                "reference": "50148edf24b6cd3e428aa9bc06a5d915b24376bb"
+                "reference": "14acfa3262875f180fba51efe3c7aaa089a9ef24"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/laravel/socialite/zipball/50148edf24b6cd3e428aa9bc06a5d915b24376bb",
-                "reference": "50148edf24b6cd3e428aa9bc06a5d915b24376bb",
+                "url": "https://api.github.com/repos/laravel/socialite/zipball/14acfa3262875f180fba51efe3c7aaa089a9ef24",
+                "reference": "14acfa3262875f180fba51efe3c7aaa089a9ef24",
                 "shasum": ""
             },
             "require": {
                 "issues": "https://github.com/laravel/socialite/issues",
                 "source": "https://github.com/laravel/socialite"
             },
-            "time": "2023-07-14T14:22:58+00:00"
+            "time": "2023-09-05T15:20:21+00:00"
         },
         {
             "name": "laravel/tinker",
-            "version": "v2.8.1",
+            "version": "v2.8.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/laravel/tinker.git",
-                "reference": "04a2d3bd0d650c0764f70bf49d1ee39393e4eb10"
+                "reference": "b936d415b252b499e8c3b1f795cd4fc20f57e1f3"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/laravel/tinker/zipball/04a2d3bd0d650c0764f70bf49d1ee39393e4eb10",
-                "reference": "04a2d3bd0d650c0764f70bf49d1ee39393e4eb10",
+                "url": "https://api.github.com/repos/laravel/tinker/zipball/b936d415b252b499e8c3b1f795cd4fc20f57e1f3",
+                "reference": "b936d415b252b499e8c3b1f795cd4fc20f57e1f3",
                 "shasum": ""
             },
             "require": {
             },
             "require-dev": {
                 "mockery/mockery": "~1.3.3|^1.4.2",
+                "phpstan/phpstan": "^1.10",
                 "phpunit/phpunit": "^8.5.8|^9.3.3"
             },
             "suggest": {
             ],
             "support": {
                 "issues": "https://github.com/laravel/tinker/issues",
-                "source": "https://github.com/laravel/tinker/tree/v2.8.1"
+                "source": "https://github.com/laravel/tinker/tree/v2.8.2"
             },
-            "time": "2023-02-15T16:40:09+00:00"
+            "time": "2023-08-15T14:27:00+00:00"
         },
         {
             "name": "league/commonmark",
-            "version": "2.4.0",
+            "version": "2.4.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/thephpleague/commonmark.git",
-                "reference": "d44a24690f16b8c1808bf13b1bd54ae4c63ea048"
+                "reference": "3669d6d5f7a47a93c08ddff335e6d945481a1dd5"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/d44a24690f16b8c1808bf13b1bd54ae4c63ea048",
-                "reference": "d44a24690f16b8c1808bf13b1bd54ae4c63ea048",
+                "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/3669d6d5f7a47a93c08ddff335e6d945481a1dd5",
+                "reference": "3669d6d5f7a47a93c08ddff335e6d945481a1dd5",
                 "shasum": ""
             },
             "require": {
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-03-24T15:16:10+00:00"
+            "time": "2023-08-30T16:55:00+00:00"
         },
         {
             "name": "league/config",
         },
         {
             "name": "league/flysystem",
-            "version": "3.15.1",
+            "version": "3.16.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/thephpleague/flysystem.git",
-                "reference": "a141d430414fcb8bf797a18716b09f759a385bed"
+                "reference": "4fdf372ca6b63c6e281b1c01a624349ccb757729"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/a141d430414fcb8bf797a18716b09f759a385bed",
-                "reference": "a141d430414fcb8bf797a18716b09f759a385bed",
+                "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/4fdf372ca6b63c6e281b1c01a624349ccb757729",
+                "reference": "4fdf372ca6b63c6e281b1c01a624349ccb757729",
                 "shasum": ""
             },
             "require": {
                 "php": "^8.0.2"
             },
             "conflict": {
+                "async-aws/core": "<1.19.0",
+                "async-aws/s3": "<1.14.0",
                 "aws/aws-sdk-php": "3.209.31 || 3.210.0",
                 "guzzlehttp/guzzle": "<7.0",
                 "guzzlehttp/ringphp": "<1.1.1",
                 "microsoft/azure-storage-blob": "^1.1",
                 "phpseclib/phpseclib": "^3.0.14",
                 "phpstan/phpstan": "^0.12.26",
-                "phpunit/phpunit": "^9.5.11",
+                "phpunit/phpunit": "^9.5.11|^10.0",
                 "sabre/dav": "^4.3.1"
             },
             "type": "library",
             ],
             "support": {
                 "issues": "https://github.com/thephpleague/flysystem/issues",
-                "source": "https://github.com/thephpleague/flysystem/tree/3.15.1"
+                "source": "https://github.com/thephpleague/flysystem/tree/3.16.0"
             },
             "funding": [
                 {
                     "type": "github"
                 }
             ],
-            "time": "2023-05-04T09:04:26+00:00"
+            "time": "2023-09-07T19:22:17+00:00"
         },
         {
             "name": "league/flysystem-aws-s3-v3",
-            "version": "3.15.0",
+            "version": "3.16.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git",
-                "reference": "d8de61ee10b6a607e7996cff388c5a3a663e8c8a"
+                "reference": "ded9ba346bb01cb9cc4cc7f2743c2c0e14d18e1c"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/d8de61ee10b6a607e7996cff388c5a3a663e8c8a",
-                "reference": "d8de61ee10b6a607e7996cff388c5a3a663e8c8a",
+                "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/ded9ba346bb01cb9cc4cc7f2743c2c0e14d18e1c",
+                "reference": "ded9ba346bb01cb9cc4cc7f2743c2c0e14d18e1c",
                 "shasum": ""
             },
             "require": {
             ],
             "support": {
                 "issues": "https://github.com/thephpleague/flysystem-aws-s3-v3/issues",
-                "source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/3.15.0"
+                "source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/3.16.0"
             },
             "funding": [
                 {
                     "type": "github"
                 }
             ],
-            "time": "2023-05-02T20:02:14+00:00"
+            "time": "2023-08-30T10:14:57+00:00"
         },
         {
             "name": "league/flysystem-local",
-            "version": "3.15.0",
+            "version": "3.16.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/thephpleague/flysystem-local.git",
-                "reference": "543f64c397fefdf9cfeac443ffb6beff602796b3"
+                "reference": "ec7383f25642e6fd4bb0c9554fc2311245391781"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/543f64c397fefdf9cfeac443ffb6beff602796b3",
-                "reference": "543f64c397fefdf9cfeac443ffb6beff602796b3",
+                "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/ec7383f25642e6fd4bb0c9554fc2311245391781",
+                "reference": "ec7383f25642e6fd4bb0c9554fc2311245391781",
                 "shasum": ""
             },
             "require": {
             ],
             "support": {
                 "issues": "https://github.com/thephpleague/flysystem-local/issues",
-                "source": "https://github.com/thephpleague/flysystem-local/tree/3.15.0"
+                "source": "https://github.com/thephpleague/flysystem-local/tree/3.16.0"
             },
             "funding": [
                 {
                     "type": "github"
                 }
             ],
-            "time": "2023-05-02T20:02:14+00:00"
+            "time": "2023-08-30T10:23:59+00:00"
         },
         {
             "name": "league/html-to-markdown",
         },
         {
             "name": "mtdowling/jmespath.php",
-            "version": "2.6.1",
+            "version": "2.7.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/jmespath/jmespath.php.git",
-                "reference": "9b87907a81b87bc76d19a7fb2d61e61486ee9edb"
+                "reference": "bbb69a935c2cbb0c03d7f481a238027430f6440b"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/9b87907a81b87bc76d19a7fb2d61e61486ee9edb",
-                "reference": "9b87907a81b87bc76d19a7fb2d61e61486ee9edb",
+                "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/bbb69a935c2cbb0c03d7f481a238027430f6440b",
+                "reference": "bbb69a935c2cbb0c03d7f481a238027430f6440b",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.4 || ^7.0 || ^8.0",
+                "php": "^7.2.5 || ^8.0",
                 "symfony/polyfill-mbstring": "^1.17"
             },
             "require-dev": {
-                "composer/xdebug-handler": "^1.4 || ^2.0",
-                "phpunit/phpunit": "^4.8.36 || ^7.5.15"
+                "composer/xdebug-handler": "^3.0.3",
+                "phpunit/phpunit": "^8.5.33"
             },
             "bin": [
                 "bin/jp.php"
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "2.6-dev"
+                    "dev-master": "2.7-dev"
                 }
             },
             "autoload": {
                 "MIT"
             ],
             "authors": [
+                {
+                    "name": "Graham Campbell",
+                    "email": "[email protected]",
+                    "homepage": "https://github.com/GrahamCampbell"
+                },
                 {
                     "name": "Michael Dowling",
                     "email": "[email protected]",
             ],
             "support": {
                 "issues": "https://github.com/jmespath/jmespath.php/issues",
-                "source": "https://github.com/jmespath/jmespath.php/tree/2.6.1"
+                "source": "https://github.com/jmespath/jmespath.php/tree/2.7.0"
             },
-            "time": "2021-06-14T00:11:39+00:00"
+            "time": "2023-08-25T10:54:48+00:00"
         },
         {
             "name": "nesbot/carbon",
-            "version": "2.69.0",
+            "version": "2.70.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/briannesbitt/Carbon.git",
-                "reference": "4308217830e4ca445583a37d1bf4aff4153fa81c"
+                "reference": "d3298b38ea8612e5f77d38d1a99438e42f70341d"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/4308217830e4ca445583a37d1bf4aff4153fa81c",
-                "reference": "4308217830e4ca445583a37d1bf4aff4153fa81c",
+                "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/d3298b38ea8612e5f77d38d1a99438e42f70341d",
+                "reference": "d3298b38ea8612e5f77d38d1a99438e42f70341d",
                 "shasum": ""
             },
             "require": {
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-08-03T09:00:52+00:00"
+            "time": "2023-09-07T16:43:50+00:00"
         },
         {
             "name": "nette/schema",
         },
         {
             "name": "socialiteproviders/manager",
-            "version": "v4.3.0",
+            "version": "v4.4.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/SocialiteProviders/Manager.git",
-                "reference": "47402cbc5b7ef445317e799bf12fd5a12062206c"
+                "reference": "df5e45b53d918ec3d689f014d98a6c838b98ed96"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/SocialiteProviders/Manager/zipball/47402cbc5b7ef445317e799bf12fd5a12062206c",
-                "reference": "47402cbc5b7ef445317e799bf12fd5a12062206c",
+                "url": "https://api.github.com/repos/SocialiteProviders/Manager/zipball/df5e45b53d918ec3d689f014d98a6c838b98ed96",
+                "reference": "df5e45b53d918ec3d689f014d98a6c838b98ed96",
                 "shasum": ""
             },
             "require": {
                 "illuminate/support": "^6.0 || ^7.0 || ^8.0 || ^9.0 || ^10.0",
                 "laravel/socialite": "~5.0",
-                "php": "^7.4 || ^8.0"
+                "php": "^8.0"
             },
             "require-dev": {
                 "mockery/mockery": "^1.2",
                 "issues": "https://github.com/socialiteproviders/manager/issues",
                 "source": "https://github.com/socialiteproviders/manager"
             },
-            "time": "2023-01-26T23:11:27+00:00"
+            "time": "2023-08-27T23:46:34+00:00"
         },
         {
             "name": "socialiteproviders/microsoft-azure",
             },
             "time": "2022-09-06T03:39:26+00:00"
         },
-        {
-            "name": "socialiteproviders/slack",
-            "version": "4.1.1",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/SocialiteProviders/Slack.git",
-                "reference": "2b781c95daf06ec87a8f3deba2ab613d6bea5e8d"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/SocialiteProviders/Slack/zipball/2b781c95daf06ec87a8f3deba2ab613d6bea5e8d",
-                "reference": "2b781c95daf06ec87a8f3deba2ab613d6bea5e8d",
-                "shasum": ""
-            },
-            "require": {
-                "ext-json": "*",
-                "php": "^7.2 || ^8.0",
-                "socialiteproviders/manager": "~4.0"
-            },
-            "type": "library",
-            "autoload": {
-                "psr-4": {
-                    "SocialiteProviders\\Slack\\": ""
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Brian Faust",
-                    "email": "[email protected]"
-                }
-            ],
-            "description": "Slack OAuth2 Provider for Laravel Socialite",
-            "support": {
-                "source": "https://github.com/SocialiteProviders/Slack/tree/4.1.1"
-            },
-            "abandoned": "laravel/socialite",
-            "time": "2021-03-26T04:10:10+00:00"
-        },
         {
             "name": "socialiteproviders/twitch",
             "version": "5.3.1",
         },
         {
             "name": "symfony/polyfill-ctype",
-            "version": "v1.27.0",
+            "version": "v1.28.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-ctype.git",
-                "reference": "5bbc823adecdae860bb64756d639ecfec17b050a"
+                "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/5bbc823adecdae860bb64756d639ecfec17b050a",
-                "reference": "5bbc823adecdae860bb64756d639ecfec17b050a",
+                "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb",
+                "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb",
                 "shasum": ""
             },
             "require": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-main": "1.27-dev"
+                    "dev-main": "1.28-dev"
                 },
                 "thanks": {
                     "name": "symfony/polyfill",
                 "portable"
             ],
             "support": {
-                "source": "https://github.com/symfony/polyfill-ctype/tree/v1.27.0"
+                "source": "https://github.com/symfony/polyfill-ctype/tree/v1.28.0"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-11-03T14:55:06+00:00"
+            "time": "2023-01-26T09:26:14+00:00"
         },
         {
             "name": "symfony/polyfill-intl-grapheme",
-            "version": "v1.27.0",
+            "version": "v1.28.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-intl-grapheme.git",
-                "reference": "511a08c03c1960e08a883f4cffcacd219b758354"
+                "reference": "875e90aeea2777b6f135677f618529449334a612"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/511a08c03c1960e08a883f4cffcacd219b758354",
-                "reference": "511a08c03c1960e08a883f4cffcacd219b758354",
+                "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/875e90aeea2777b6f135677f618529449334a612",
+                "reference": "875e90aeea2777b6f135677f618529449334a612",
                 "shasum": ""
             },
             "require": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-main": "1.27-dev"
+                    "dev-main": "1.28-dev"
                 },
                 "thanks": {
                     "name": "symfony/polyfill",
                 "shim"
             ],
             "support": {
-                "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.27.0"
+                "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.28.0"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-11-03T14:55:06+00:00"
+            "time": "2023-01-26T09:26:14+00:00"
         },
         {
             "name": "symfony/polyfill-intl-idn",
-            "version": "v1.27.0",
+            "version": "v1.28.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-intl-idn.git",
-                "reference": "639084e360537a19f9ee352433b84ce831f3d2da"
+                "reference": "ecaafce9f77234a6a449d29e49267ba10499116d"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/639084e360537a19f9ee352433b84ce831f3d2da",
-                "reference": "639084e360537a19f9ee352433b84ce831f3d2da",
+                "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/ecaafce9f77234a6a449d29e49267ba10499116d",
+                "reference": "ecaafce9f77234a6a449d29e49267ba10499116d",
                 "shasum": ""
             },
             "require": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-main": "1.27-dev"
+                    "dev-main": "1.28-dev"
                 },
                 "thanks": {
                     "name": "symfony/polyfill",
                 "shim"
             ],
             "support": {
-                "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.27.0"
+                "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.28.0"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-11-03T14:55:06+00:00"
+            "time": "2023-01-26T09:30:37+00:00"
         },
         {
             "name": "symfony/polyfill-intl-normalizer",
-            "version": "v1.27.0",
+            "version": "v1.28.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-intl-normalizer.git",
-                "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6"
+                "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/19bd1e4fcd5b91116f14d8533c57831ed00571b6",
-                "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6",
+                "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92",
+                "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92",
                 "shasum": ""
             },
             "require": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-main": "1.27-dev"
+                    "dev-main": "1.28-dev"
                 },
                 "thanks": {
                     "name": "symfony/polyfill",
                 "shim"
             ],
             "support": {
-                "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.27.0"
+                "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.28.0"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-11-03T14:55:06+00:00"
+            "time": "2023-01-26T09:26:14+00:00"
         },
         {
             "name": "symfony/polyfill-mbstring",
-            "version": "v1.27.0",
+            "version": "v1.28.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-mbstring.git",
-                "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534"
+                "reference": "42292d99c55abe617799667f454222c54c60e229"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/8ad114f6b39e2c98a8b0e3bd907732c207c2b534",
-                "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534",
+                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229",
+                "reference": "42292d99c55abe617799667f454222c54c60e229",
                 "shasum": ""
             },
             "require": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-main": "1.27-dev"
+                    "dev-main": "1.28-dev"
                 },
                 "thanks": {
                     "name": "symfony/polyfill",
                 "shim"
             ],
             "support": {
-                "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.27.0"
+                "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.28.0"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-11-03T14:55:06+00:00"
+            "time": "2023-07-28T09:04:16+00:00"
         },
         {
             "name": "symfony/polyfill-php72",
-            "version": "v1.27.0",
+            "version": "v1.28.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-php72.git",
-                "reference": "869329b1e9894268a8a61dabb69153029b7a8c97"
+                "reference": "70f4aebd92afca2f865444d30a4d2151c13c3179"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/869329b1e9894268a8a61dabb69153029b7a8c97",
-                "reference": "869329b1e9894268a8a61dabb69153029b7a8c97",
+                "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/70f4aebd92afca2f865444d30a4d2151c13c3179",
+                "reference": "70f4aebd92afca2f865444d30a4d2151c13c3179",
                 "shasum": ""
             },
             "require": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-main": "1.27-dev"
+                    "dev-main": "1.28-dev"
                 },
                 "thanks": {
                     "name": "symfony/polyfill",
                 "shim"
             ],
             "support": {
-                "source": "https://github.com/symfony/polyfill-php72/tree/v1.27.0"
+                "source": "https://github.com/symfony/polyfill-php72/tree/v1.28.0"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-11-03T14:55:06+00:00"
+            "time": "2023-01-26T09:26:14+00:00"
         },
         {
             "name": "symfony/polyfill-php80",
-            "version": "v1.27.0",
+            "version": "v1.28.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-php80.git",
-                "reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936"
+                "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936",
-                "reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936",
+                "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/6caa57379c4aec19c0a12a38b59b26487dcfe4b5",
+                "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5",
                 "shasum": ""
             },
             "require": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-main": "1.27-dev"
+                    "dev-main": "1.28-dev"
                 },
                 "thanks": {
                     "name": "symfony/polyfill",
                 "shim"
             ],
             "support": {
-                "source": "https://github.com/symfony/polyfill-php80/tree/v1.27.0"
+                "source": "https://github.com/symfony/polyfill-php80/tree/v1.28.0"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-11-03T14:55:06+00:00"
+            "time": "2023-01-26T09:26:14+00:00"
         },
         {
             "name": "symfony/polyfill-php81",
-            "version": "v1.27.0",
+            "version": "v1.28.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-php81.git",
-                "reference": "707403074c8ea6e2edaf8794b0157a0bfa52157a"
+                "reference": "7581cd600fa9fd681b797d00b02f068e2f13263b"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/707403074c8ea6e2edaf8794b0157a0bfa52157a",
-                "reference": "707403074c8ea6e2edaf8794b0157a0bfa52157a",
+                "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/7581cd600fa9fd681b797d00b02f068e2f13263b",
+                "reference": "7581cd600fa9fd681b797d00b02f068e2f13263b",
                 "shasum": ""
             },
             "require": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-main": "1.27-dev"
+                    "dev-main": "1.28-dev"
                 },
                 "thanks": {
                     "name": "symfony/polyfill",
                 "shim"
             ],
             "support": {
-                "source": "https://github.com/symfony/polyfill-php81/tree/v1.27.0"
+                "source": "https://github.com/symfony/polyfill-php81/tree/v1.28.0"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-11-03T14:55:06+00:00"
+            "time": "2023-01-26T09:26:14+00:00"
         },
         {
             "name": "symfony/polyfill-uuid",
-            "version": "v1.27.0",
+            "version": "v1.28.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-uuid.git",
-                "reference": "f3cf1a645c2734236ed1e2e671e273eeb3586166"
+                "reference": "9c44518a5aff8da565c8a55dbe85d2769e6f630e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/f3cf1a645c2734236ed1e2e671e273eeb3586166",
-                "reference": "f3cf1a645c2734236ed1e2e671e273eeb3586166",
+                "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/9c44518a5aff8da565c8a55dbe85d2769e6f630e",
+                "reference": "9c44518a5aff8da565c8a55dbe85d2769e6f630e",
                 "shasum": ""
             },
             "require": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-main": "1.27-dev"
+                    "dev-main": "1.28-dev"
                 },
                 "thanks": {
                     "name": "symfony/polyfill",
                 "uuid"
             ],
             "support": {
-                "source": "https://github.com/symfony/polyfill-uuid/tree/v1.27.0"
+                "source": "https://github.com/symfony/polyfill-uuid/tree/v1.28.0"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-11-03T14:55:06+00:00"
+            "time": "2023-01-26T09:26:14+00:00"
         },
         {
             "name": "symfony/process",
         },
         {
             "name": "phpstan/phpstan",
-            "version": "1.10.29",
+            "version": "1.10.33",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpstan/phpstan.git",
-                "reference": "ee5d8f2d3977fb09e55603eee6fb53bdd76ee9c1"
+                "reference": "03b1cf9f814ba0863c4e9affea49a4d1ed9a2ed1"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpstan/phpstan/zipball/ee5d8f2d3977fb09e55603eee6fb53bdd76ee9c1",
-                "reference": "ee5d8f2d3977fb09e55603eee6fb53bdd76ee9c1",
+                "url": "https://api.github.com/repos/phpstan/phpstan/zipball/03b1cf9f814ba0863c4e9affea49a4d1ed9a2ed1",
+                "reference": "03b1cf9f814ba0863c4e9affea49a4d1ed9a2ed1",
                 "shasum": ""
             },
             "require": {
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-08-14T13:24:11+00:00"
+            "time": "2023-09-04T12:20:53+00:00"
         },
         {
             "name": "phpunit/php-code-coverage",
index 8821c77f0d1e99ecb9097c6e956aa9e56c85216c..9f60606ac57f0506e7af46fc30c7f6d11c6ebda5 100644 (file)
@@ -314,6 +314,7 @@ return [
         'sv' => 'Svenska',
         'tr' => 'Türkçe',
         'uk' => 'Українська',
+        'uz' => 'O‘zbekcha',
         'vi' => 'Tiếng Việt',
         'zh_CN' => '简体中文',
         'zh_TW' => '繁體中文',
index 72f7d8a35c484454899d062bc48a8a8fde923201..aa335f9daa35a7d66ab155f8039cfa9ee002aa63 100644 (file)
--- a/readme.md
+++ b/readme.md
@@ -11,6 +11,7 @@
 [![Discord](https://img.shields.io/static/v1?label=Discord&message=chat&color=738adb&logo=discord)](https://discord.gg/ztkBqR2)
 [![Mastodon](https://img.shields.io/static/v1?label=Mastodon&message=@bookstack&color=595aff&logo=mastodon)](https://fosstodon.org/@bookstack)
 [![Twitter](https://img.shields.io/static/v1?label=Twitter&message=@bookstack_app&color=1d9bf0&logo=twitter)](https://twitter.com/bookstack_app)
+[![PeerTube](https://img.shields.io/static/v1?label=PeerTube&[email protected]&color=f2690d&logo=peertube)](https://foss.video/c/bookstack)
 [![YouTube](https://img.shields.io/static/v1?label=YouTube&message=bookstackapp&color=ff0000&logo=youtube)](https://www.youtube.com/bookstackapp)
 
 A platform for storing and organising information and documentation. Details for BookStack can be found on the official website at https://www.bookstackapp.com/.
@@ -81,6 +82,7 @@ Details about BookStack's versioning scheme and the general release process [can
 ## 🌎 Translations
 
 Translations for text within BookStack is managed through the [BookStack project on Crowdin](https://crowdin.com/project/bookstack). Some strings have colon-prefixed variables such as `:userName`. Leave these values as they are as they will be replaced at run-time. Crowdin is the preferred way to provide translations, otherwise the raw translations files can be found within the `resources/lang` path.
+Translations to original files, provided via pull request, will be fed back through Crowdin instead of being merged directly and therefore your changes & commits will not be reflected in git contribution history.  
 
 If you'd like a new language to be added to Crowdin, for you to be able to provide translations for, please [open a new issue here](https://github.com/BookStackApp/BookStack/issues/new?template=language_request.yml).
 
index 75b01a379242cd97666172d8f7f0da72fc984a27..e3aef845ce09a061c969c909fe9b9308090a60e0 100644 (file)
     @endif
 
     @if(count($activity) > 0)
-        <div class="mb-xl">
+        <div id="recent-activity" class="mb-xl">
             <h5>{{ trans('entities.recent_activity') }}</h5>
             @include('common.activity-list', ['activity' => $activity])
         </div>
index 35189044bda5a28497c3dd0a622c4b1475e41baa..e596fbdce4329d4fda466d386fa36a1699c8ef1a 100644 (file)
@@ -3,7 +3,7 @@
 @endphp
 <form action="{{ url('/favourites/' . ($isFavourite ? 'remove' : 'add')) }}" method="POST">
     {{ csrf_field() }}
-    <input type="hidden" name="type" value="{{ get_class($entity) }}">
+    <input type="hidden" name="type" value="{{ $entity->getMorphClass() }}">
     <input type="hidden" name="id" value="{{ $entity->id }}">
     <button type="submit" data-shortcut="favourite" class="icon-list-item text-link">
         <span>@icon($isFavourite ? 'star' : 'star-outline')</span>
index 34e287804cc62b0174058bd3c5a54b3dbe6b0a18..c87bc2b2b0c6e06a83ccd0518cc065c5f6527915 100644 (file)
@@ -1,7 +1,7 @@
 <form action="{{ url('/watching/update') }}" method="POST">
     {{ csrf_field() }}
     {{ method_field('PUT') }}
-    <input type="hidden" name="type" value="{{ get_class($entity) }}">
+    <input type="hidden" name="type" value="{{ $entity->getMorphClass() }}">
     <input type="hidden" name="id" value="{{ $entity->id }}">
     <button type="submit"
             name="level"
index d24e120182166aec6dc0d895ae67ade32476cf8f..9389a6c5a6df44ee6bc7b1201d0e51265572915f 100644 (file)
@@ -7,7 +7,7 @@
     <form action="{{ url('/watching/update') }}" method="POST">
         {{ method_field('PUT') }}
         {{ csrf_field() }}
-        <input type="hidden" name="type" value="{{ get_class($entity) }}">
+        <input type="hidden" name="type" value="{{ $entity->getMorphClass() }}">
         <input type="hidden" name="id" value="{{ $entity->id }}">
 
         <ul refs="dropdown@menu" class="dropdown-menu xl-limited anchor-left pb-none">
index c273db276df964d8d30e9d2d462e93404f070c6b..46015354da28dfc031173e1e6b9590795b367f2d 100644 (file)
             </div>
 
             <div>
-                <div id="recent-activity">
-                    <div class="card mb-xl">
-                        <h3 class="card-title">{{ trans('entities.recent_activity') }}</h3>
-                        @include('common.activity-list', ['activity' => $activity])
-                    </div>
+                <div id="recent-activity" class="card mb-xl">
+                    <h3 class="card-title">{{ trans('entities.recent_activity') }}</h3>
+                    @include('common.activity-list', ['activity' => $activity])
                 </div>
             </div>
 
index e0a6f46d0166c3b9698a4bed4bc389ec63438b38..8875788a63fd3e40be232f0f8fbef707ab843f39 100644 (file)
@@ -65,7 +65,9 @@
     </div>
 
     @yield('bottom')
-    <script src="{{ versioned_asset('dist/app.js') }}" nonce="{{ $cspNonce }}"></script>
+    @if($cspNonce ?? false)
+        <script src="{{ versioned_asset('dist/app.js') }}" nonce="{{ $cspNonce }}"></script>
+    @endif
     @yield('scripts')
 
     @include('layouts.parts.base-body-end')
index 8019a557f601ea17b589681b146780f1a842747e..86dd6326d758b030af2143a99f203b4fd2086cbd 100644 (file)
@@ -99,7 +99,7 @@
     </div>
 
     @if(count($activity) > 0)
-        <div class="mb-xl">
+        <div id="recent-activity" class="mb-xl">
             <h5>{{ trans('entities.recent_activity') }}</h5>
             @include('common.activity-list', ['activity' => $activity])
         </div>
index 0746aa3a1b2e87d1c22e424517530012bb55003d..81bd7e7e8ce303df28e86f5fc07177c4a5762096 100644 (file)
@@ -7,11 +7,10 @@ use BookStack\Activity\DispatchWebhookJob;
 use BookStack\Activity\Models\Webhook;
 use BookStack\Activity\Tools\ActivityLogger;
 use BookStack\Api\ApiToken;
-use BookStack\Entities\Models\PageRevision;
 use BookStack\Users\Models\User;
-use Illuminate\Http\Client\Request;
+use GuzzleHttp\Exception\ConnectException;
+use GuzzleHttp\Psr7\Response;
 use Illuminate\Support\Facades\Bus;
-use Illuminate\Support\Facades\Http;
 use Tests\TestCase;
 
 class WebhookCallTest extends TestCase
@@ -50,10 +49,10 @@ class WebhookCallTest extends TestCase
 
     public function test_webhook_runs_for_delete_actions()
     {
+        // This test must not fake the queue/bus since this covers an issue
+        // around handling and serialization of items now deleted from the database.
         $this->newWebhook(['active' => true, 'endpoint' => 'https://wh.example.com'], ['all']);
-        Http::fake([
-            '*' => Http::response('', 500),
-        ]);
+        $this->mockHttpClient([new Response(500)]);
 
         $user = $this->users->newUser();
         $resp = $this->asAdmin()->delete($user->getEditUrl());
@@ -69,9 +68,7 @@ class WebhookCallTest extends TestCase
     public function test_failed_webhook_call_logs_error()
     {
         $logger = $this->withTestLogger();
-        Http::fake([
-            '*' => Http::response('', 500),
-        ]);
+        $this->mockHttpClient([new Response(500)]);
         $webhook = $this->newWebhook(['active' => true, 'endpoint' => 'https://wh.example.com'], ['all']);
         $this->assertNull($webhook->last_errored_at);
 
@@ -86,7 +83,7 @@ class WebhookCallTest extends TestCase
 
     public function test_webhook_call_exception_is_caught_and_logged()
     {
-        Http::shouldReceive('asJson')->andThrow(new \Exception('Failed to perform request'));
+        $this->mockHttpClient([new ConnectException('Failed to perform request', new \GuzzleHttp\Psr7\Request('GET', ''))]);
 
         $logger = $this->withTestLogger();
         $webhook = $this->newWebhook(['active' => true, 'endpoint' => 'https://wh.example.com'], ['all']);
@@ -104,11 +101,11 @@ class WebhookCallTest extends TestCase
     public function test_webhook_uses_ssr_hosts_option_if_set()
     {
         config()->set('app.ssr_hosts', 'https://*.example.com');
-        $http = Http::fake();
+        $responses = $this->mockHttpClient();
 
         $webhook = $this->newWebhook(['active' => true, 'endpoint' => 'https://wh.example.co.uk'], ['all']);
         $this->runEvent(ActivityType::ROLE_CREATE);
-        $http->assertNothingSent();
+        $this->assertEquals(0, $responses->requestCount());
 
         $webhook->refresh();
         $this->assertEquals('The URL does not match the configured allowed SSR hosts', $webhook->last_error);
@@ -117,29 +114,24 @@ class WebhookCallTest extends TestCase
 
     public function test_webhook_call_data_format()
     {
-        Http::fake([
-            '*' => Http::response('', 200),
-        ]);
+        $responses = $this->mockHttpClient([new Response(200, [], '')]);
         $webhook = $this->newWebhook(['active' => true, 'endpoint' => 'https://wh.example.com'], ['all']);
         $page = $this->entities->page();
         $editor = $this->users->editor();
 
         $this->runEvent(ActivityType::PAGE_UPDATE, $page, $editor);
 
-        Http::assertSent(function (Request $request) use ($editor, $page, $webhook) {
-            $reqData = $request->data();
-
-            return $request->isJson()
-                && $reqData['event'] === 'page_update'
-                && $reqData['text'] === ($editor->name . ' updated page "' . $page->name . '"')
-                && is_string($reqData['triggered_at'])
-                && $reqData['triggered_by']['name'] === $editor->name
-                && $reqData['triggered_by_profile_url'] === $editor->getProfileUrl()
-                && $reqData['webhook_id'] === $webhook->id
-                && $reqData['webhook_name'] === $webhook->name
-                && $reqData['url'] === $page->getUrl()
-                && $reqData['related_item']['name'] === $page->name;
-        });
+        $request = $responses->latestRequest();
+        $reqData = json_decode($request->getBody(), true);
+        $this->assertEquals('page_update', $reqData['event']);
+        $this->assertEquals(($editor->name . ' updated page "' . $page->name . '"'), $reqData['text']);
+        $this->assertIsString($reqData['triggered_at']);
+        $this->assertEquals($editor->name, $reqData['triggered_by']['name']);
+        $this->assertEquals($editor->getProfileUrl(), $reqData['triggered_by_profile_url']);
+        $this->assertEquals($webhook->id, $reqData['webhook_id']);
+        $this->assertEquals($webhook->name, $reqData['webhook_name']);
+        $this->assertEquals($page->getUrl(), $reqData['url']);
+        $this->assertEquals($page->name, $reqData['related_item']['name']);
     }
 
     protected function runEvent(string $event, $detail = '', ?User $user = null)
index 464886155f4ae12fb2aeda9b1864cc7127bc4d54..5b9ae5a4c9e3d1996b652cab41f00675d7d9962a 100644 (file)
@@ -66,7 +66,7 @@ class WatchTest extends TestCase
 
         $this->actingAs($editor)->get($book->getUrl());
         $resp = $this->put('/watching/update', [
-            'type' => get_class($book),
+            'type' => $book->getMorphClass(),
             'id' => $book->id,
             'level' => 'comments'
         ]);
@@ -81,7 +81,7 @@ class WatchTest extends TestCase
         ]);
 
         $resp = $this->put('/watching/update', [
-            'type' => get_class($book),
+            'type' => $book->getMorphClass(),
             'id' => $book->id,
             'level' => 'default'
         ]);
@@ -101,7 +101,7 @@ class WatchTest extends TestCase
         $book = $this->entities->book();
 
         $resp = $this->put('/watching/update', [
-            'type' => get_class($book),
+            'type' => $book->getMorphClass(),
             'id' => $book->id,
             'level' => 'comments'
         ]);
index e2a04b528ee43cac66dec437b28b8b5e046df569..6ad7272577bd3adfd8bb73056427c9fc8a7cafb7 100644 (file)
@@ -2,11 +2,11 @@
 
 namespace Tests\Api;
 
+use BookStack\Access\Notifications\UserInviteNotification;
 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;
 use Illuminate\Support\Facades\Hash;
@@ -140,7 +140,7 @@ class UsersApiTest extends TestCase
         $resp->assertStatus(200);
         /** @var User $user */
         $user = User::query()->where('email', '=', '[email protected]')->first();
-        Notification::assertSentTo($user, UserInvite::class);
+        Notification::assertSentTo($user, UserInviteNotification::class);
     }
 
     public function test_create_name_and_email_validation()
index 191a25f8801e732a75a3df5d46d4c19de8389756..204a3bb5f960243e99ff30cc3f1427759b6a9470 100644 (file)
@@ -7,7 +7,6 @@ use BookStack\Facades\Theme;
 use BookStack\Theming\ThemeEvents;
 use BookStack\Users\Models\Role;
 use BookStack\Users\Models\User;
-use GuzzleHttp\Psr7\Request;
 use GuzzleHttp\Psr7\Response;
 use Illuminate\Testing\TestResponse;
 use Tests\Helpers\OidcJwtHelper;
@@ -31,7 +30,7 @@ class OidcTest extends TestCase
             'auth.method'                 => 'oidc',
             'auth.defaults.guard'         => 'oidc',
             'oidc.name'                   => 'SingleSignOn-Testing',
-            'oidc.display_name_claims'    => ['name'],
+            'oidc.display_name_claims'    => 'name',
             'oidc.client_id'              => OidcJwtHelper::defaultClientId(),
             'oidc.client_secret'          => 'testpass',
             'oidc.jwt_public_key'         => $this->keyFilePath,
@@ -137,7 +136,7 @@ class OidcTest extends TestCase
         $this->post('/oidc/login');
         $state = session()->get('oidc_state');
 
-        $transactions = &$this->mockHttpClient([$this->getMockAuthorizationResponse([
+        $transactions = $this->mockHttpClient([$this->getMockAuthorizationResponse([
             'email' => '[email protected]',
             'sub'   => 'benny1010101',
         ])]);
@@ -146,9 +145,8 @@ class OidcTest extends TestCase
         // App calls token endpoint to get id token
         $resp = $this->get('/oidc/callback?code=SplxlOBeZQQYbYS6WxSbIA&state=' . $state);
         $resp->assertRedirect('/');
-        $this->assertCount(1, $transactions);
-        /** @var Request $tokenRequest */
-        $tokenRequest = $transactions[0]['request'];
+        $this->assertEquals(1, $transactions->requestCount());
+        $tokenRequest = $transactions->latestRequest();
         $this->assertEquals('https://oidc.local/token', (string) $tokenRequest->getUri());
         $this->assertEquals('POST', $tokenRequest->getMethod());
         $this->assertEquals('Basic ' . base64_encode(OidcJwtHelper::defaultClientId() . ':testpass'), $tokenRequest->getHeader('Authorization')[0]);
@@ -279,7 +277,7 @@ class OidcTest extends TestCase
     {
         $this->withAutodiscovery();
 
-        $transactions = &$this->mockHttpClient([
+        $transactions = $this->mockHttpClient([
             $this->getAutoDiscoveryResponse(),
             $this->getJwksResponse(),
         ]);
@@ -289,11 +287,9 @@ class OidcTest extends TestCase
         $this->runLogin();
 
         $this->assertTrue(auth()->check());
-        /** @var Request $discoverRequest */
-        $discoverRequest = $transactions[0]['request'];
-        /** @var Request $discoverRequest */
-        $keysRequest = $transactions[1]['request'];
 
+        $discoverRequest = $transactions->requestAt(0);
+        $keysRequest = $transactions->requestAt(1);
         $this->assertEquals('GET', $keysRequest->getMethod());
         $this->assertEquals('GET', $discoverRequest->getMethod());
         $this->assertEquals(OidcJwtHelper::defaultIssuer() . '/.well-known/openid-configuration', $discoverRequest->getUri());
@@ -316,7 +312,7 @@ class OidcTest extends TestCase
     {
         $this->withAutodiscovery();
 
-        $transactions = &$this->mockHttpClient([
+        $transactions = $this->mockHttpClient([
             $this->getAutoDiscoveryResponse(),
             $this->getJwksResponse(),
             $this->getAutoDiscoveryResponse([
@@ -327,15 +323,15 @@ class OidcTest extends TestCase
 
         // Initial run
         $this->post('/oidc/login');
-        $this->assertCount(2, $transactions);
+        $this->assertEquals(2, $transactions->requestCount());
         // Second run, hits cache
         $this->post('/oidc/login');
-        $this->assertCount(2, $transactions);
+        $this->assertEquals(2, $transactions->requestCount());
 
         // Third run, different issuer, new cache key
         config()->set(['oidc.issuer' => 'https://auto.example.com']);
         $this->post('/oidc/login');
-        $this->assertCount(4, $transactions);
+        $this->assertEquals(4, $transactions->requestCount());
     }
 
     public function test_auth_login_with_autodiscovery_with_keys_that_do_not_have_alg_property()
@@ -412,6 +408,23 @@ class OidcTest extends TestCase
         $this->assertEquals('xXBennyTheGeezXx', $user->external_auth_id);
     }
 
+    public function test_auth_uses_mulitple_display_name_claims_if_configured()
+    {
+        config()->set(['oidc.display_name_claims' => 'first_name|last_name']);
+
+        $this->runLogin([
+            'email'      => '[email protected]',
+            'sub'        => 'benny1010101',
+            'first_name' => 'Benny',
+            'last_name'  => 'Jenkins'
+        ]);
+
+        $this->assertDatabaseHas('users', [
+            'name' => 'Benny Jenkins',
+            'email' => '[email protected]',
+        ]);
+    }
+
     public function test_login_group_sync()
     {
         config()->set([
index bc190afd81f6a6dbfb2b50af92ef5b599c1feecc..ff1a9d66b11b8ad942490189ffe9b61dfde9e992 100644 (file)
@@ -2,7 +2,7 @@
 
 namespace Tests\Auth;
 
-use BookStack\Notifications\ConfirmEmail;
+use BookStack\Access\Notifications\ConfirmEmailNotification;
 use BookStack\Users\Models\Role;
 use BookStack\Users\Models\User;
 use Illuminate\Support\Facades\DB;
@@ -28,7 +28,7 @@ class RegistrationTest extends TestCase
         // Ensure notification sent
         /** @var User $dbUser */
         $dbUser = User::query()->where('email', '=', $user->email)->first();
-        Notification::assertSentTo($dbUser, ConfirmEmail::class);
+        Notification::assertSentTo($dbUser, ConfirmEmailNotification::class);
 
         // Test access and resend confirmation email
         $resp = $this->post('/login', ['email' => $user->email, 'password' => $user->password]);
@@ -42,7 +42,7 @@ class RegistrationTest extends TestCase
 
         // Get confirmation and confirm notification matches
         $emailConfirmation = DB::table('email_confirmations')->where('user_id', '=', $dbUser->id)->first();
-        Notification::assertSentTo($dbUser, ConfirmEmail::class, function ($notification, $channels) use ($emailConfirmation) {
+        Notification::assertSentTo($dbUser, ConfirmEmailNotification::class, function ($notification, $channels) use ($emailConfirmation) {
             return $notification->token === $emailConfirmation->token;
         });
 
index b97a2f2d380fb3b1b72eb69dbaa76b6f3767d621..e60ac5643ac6e9a8d403c5839f011c797f8cb93c 100644 (file)
@@ -2,7 +2,7 @@
 
 namespace Tests\Auth;
 
-use BookStack\Notifications\ResetPassword;
+use BookStack\Access\Notifications\ResetPasswordNotification;
 use BookStack\Users\Models\User;
 use Illuminate\Support\Facades\Notification;
 use Tests\TestCase;
@@ -34,8 +34,8 @@ class ResetPasswordTest extends TestCase
         /** @var User $user */
         $user = User::query()->where('email', '=', '[email protected]')->first();
 
-        Notification::assertSentTo($user, ResetPassword::class);
-        $n = Notification::sent($user, ResetPassword::class);
+        Notification::assertSentTo($user, ResetPasswordNotification::class);
+        $n = Notification::sent($user, ResetPasswordNotification::class);
 
         $this->get('/password/reset/' . $n->first()->token)
             ->assertOk()
@@ -95,7 +95,7 @@ class ResetPasswordTest extends TestCase
         $resp = $this->followingRedirects()->post('/password/email', [
             'email' => $editor->email,
         ]);
-        Notification::assertTimesSent(1, ResetPassword::class);
+        Notification::assertTimesSent(1, ResetPasswordNotification::class);
         $resp->assertSee('A password reset link will be sent to ' . $editor->email . ' if that email address is found in the system.');
     }
 }
index 8d6143877d3529d9532df40586c61f6c5c0b7380..a9dee0007f5870e25a6cc65391d3f8eeb02347ac 100644 (file)
@@ -2,8 +2,8 @@
 
 namespace Tests\Auth;
 
+use BookStack\Access\Notifications\UserInviteNotification;
 use BookStack\Access\UserInviteService;
-use BookStack\Notifications\UserInvite;
 use BookStack\Users\Models\User;
 use Carbon\Carbon;
 use Illuminate\Notifications\Messages\MailMessage;
@@ -29,7 +29,7 @@ class UserInviteTest extends TestCase
 
         $newUser = User::query()->where('email', '=', $email)->orderBy('id', 'desc')->first();
 
-        Notification::assertSentTo($newUser, UserInvite::class);
+        Notification::assertSentTo($newUser, UserInviteNotification::class);
         $this->assertDatabaseHas('user_invites', [
             'user_id' => $newUser->id,
         ]);
@@ -50,7 +50,7 @@ class UserInviteTest extends TestCase
         $resp->assertRedirect('/settings/users');
 
         $newUser = User::query()->where('email', '=', $email)->orderBy('id', 'desc')->first();
-        Notification::assertSentTo($newUser, UserInvite::class, function ($notification, $channels, $notifiable) {
+        Notification::assertSentTo($newUser, UserInviteNotification::class, function ($notification, $channels, $notifiable) {
             /** @var MailMessage $mail */
             $mail = $notification->toMail($notifiable);
 
index a1a5ab98540e58cebc655f177e052021ef5b4f69..36fd51e968ea4d0e4a56c8c19ebeb1dc04b46aee 100644 (file)
@@ -14,7 +14,7 @@ class CleanupImagesCommandTest extends TestCase
 
         $this->artisan('bookstack:cleanup-images -v')
             ->expectsOutput('Dry run, no images have been deleted')
-            ->expectsOutput('1 images found that would have been deleted')
+            ->expectsOutput('1 image(s) found that would have been deleted')
             ->expectsOutputToContain($image->path)
             ->assertExitCode(0);
 
@@ -29,7 +29,7 @@ class CleanupImagesCommandTest extends TestCase
         $this->artisan('bookstack:cleanup-images --force')
             ->expectsOutputToContain('This operation is destructive and is not guaranteed to be fully accurate')
             ->expectsConfirmation('Are you sure you want to proceed?', 'yes')
-            ->expectsOutput('1 images deleted')
+            ->expectsOutput('1 image(s) deleted')
             ->assertExitCode(0);
 
         $this->assertDatabaseMissing('images', ['id' => $image->id]);
@@ -46,4 +46,17 @@ class CleanupImagesCommandTest extends TestCase
 
         $this->assertDatabaseHas('images', ['id' => $image->id]);
     }
+
+    public function test_command_force_no_interaction_run()
+    {
+        $page = $this->entities->page();
+        $image = Image::factory()->create(['uploaded_to' => $page->id]);
+
+        $this->artisan('bookstack:cleanup-images --force --no-interaction')
+            ->expectsOutputToContain('This operation is destructive and is not guaranteed to be fully accurate')
+            ->expectsOutput('1 image(s) deleted')
+            ->assertExitCode(0);
+
+        $this->assertDatabaseMissing('images', ['id' => $image->id]);
+    }
 }
index 0a71bb6eff4ab6167f70f21d9a610a6ab576829f..23fc68197411c220ccd4a173d14b11edee5aeed1 100644 (file)
@@ -152,4 +152,16 @@ class CommentTest extends TestCase
         $respHtml = $this->withHtml($this->get($page->getUrl('/edit')));
         $respHtml->assertElementContains('.comment-box .content', 'My great comment to see in the editor');
     }
+
+    public function test_comment_creator_name_truncated()
+    {
+        [$longNamedUser] = $this->users->newUserWithRole(['name' => 'Wolfeschlegelsteinhausenbergerdorff'], ['comment-create-all', 'page-view-all']);
+        $page = $this->entities->page();
+
+        $comment = Comment::factory()->make();
+        $this->actingAs($longNamedUser)->postJson("/comment/$page->id", $comment->getAttributes());
+
+        $pageResp = $this->asAdmin()->get($page->getUrl());
+        $pageResp->assertSee('Wolfeschlegels…');
+    }
 }
index 0e30cbd58cadc608e5150cc47207dbffb606f8ee..48048e2845d64a4fbe503e394d69502186239918 100644 (file)
@@ -14,10 +14,10 @@ class FavouriteTest extends TestCase
 
         $resp = $this->actingAs($editor)->get($page->getUrl());
         $this->withHtml($resp)->assertElementContains('button', 'Favourite');
-        $this->withHtml($resp)->assertElementExists('form[method="POST"][action$="/favourites/add"]');
+        $this->withHtml($resp)->assertElementExists('form[method="POST"][action$="/favourites/add"] input[name="type"][value="page"]');
 
         $resp = $this->post('/favourites/add', [
-            'type' => get_class($page),
+            'type' => $page->getMorphClass(),
             'id'   => $page->id,
         ]);
         $resp->assertRedirect($page->getUrl());
@@ -45,7 +45,7 @@ class FavouriteTest extends TestCase
         $this->withHtml($resp)->assertElementExists('form[method="POST"][action$="/favourites/remove"]');
 
         $resp = $this->post('/favourites/remove', [
-            'type' => get_class($page),
+            'type' => $page->getMorphClass(),
             'id'   => $page->id,
         ]);
         $resp->assertRedirect($page->getUrl());
@@ -67,7 +67,7 @@ class FavouriteTest extends TestCase
 
         $this->actingAs($user)->get($book->getUrl());
         $resp = $this->post('/favourites/add', [
-            'type' => get_class($book),
+            'type' => $book->getMorphClass(),
             'id'   => $book->id,
         ]);
         $resp->assertRedirect($book->getUrl());
index 322f90107ddb67d8e3a29c4d2075f5f71269423e..e96024e7b74fb1e4f13038336ee6864986001bf1 100644 (file)
@@ -2,7 +2,7 @@
 
 namespace Tests\Settings;
 
-use BookStack\Notifications\TestEmail;
+use BookStack\Settings\TestEmailNotification;
 use Illuminate\Contracts\Notifications\Dispatcher;
 use Illuminate\Support\Facades\Notification;
 use Tests\TestCase;
@@ -26,7 +26,7 @@ class TestEmailTest extends TestCase
         $sendReq->assertRedirect('/settings/maintenance#image-cleanup');
         $this->assertSessionHas('success', 'Email sent to ' . $admin->email);
 
-        Notification::assertSentTo($admin, TestEmail::class);
+        Notification::assertSentTo($admin, TestEmailNotification::class);
     }
 
     public function test_send_test_email_failure_displays_error_notification()
@@ -57,6 +57,6 @@ class TestEmailTest extends TestCase
 
         $this->permissions->grantUserRolePermissions($user, ['settings-manage']);
         $sendReq = $this->actingAs($user)->post('/settings/maintenance/send-test-email');
-        Notification::assertSentTo($user, TestEmail::class);
+        Notification::assertSentTo($user, TestEmailNotification::class);
     }
 }
index 0ab0792bd00228dfb306b7216e7f61a66be894e3..f8f59977a1e1ac6a282f91e0cca4399e0c47b853 100644 (file)
@@ -3,13 +3,10 @@
 namespace Tests;
 
 use BookStack\Entities\Models\Entity;
+use BookStack\Http\HttpClientHistory;
+use BookStack\Http\HttpRequestService;
 use BookStack\Settings\SettingService;
-use BookStack\Uploads\HttpFetcher;
 use BookStack\Users\Models\User;
-use GuzzleHttp\Client;
-use GuzzleHttp\Handler\MockHandler;
-use GuzzleHttp\HandlerStack;
-use GuzzleHttp\Middleware;
 use Illuminate\Contracts\Console\Kernel;
 use Illuminate\Foundation\Testing\DatabaseTransactions;
 use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
@@ -18,10 +15,8 @@ use Illuminate\Support\Env;
 use Illuminate\Support\Facades\DB;
 use Illuminate\Support\Facades\Log;
 use Illuminate\Testing\Assert as PHPUnit;
-use Mockery;
 use Monolog\Handler\TestHandler;
 use Monolog\Logger;
-use Psr\Http\Client\ClientInterface;
 use Ssddanbrown\AssertHtml\TestsHtml;
 use Tests\Helpers\EntityProvider;
 use Tests\Helpers\FileProvider;
@@ -111,33 +106,11 @@ abstract class TestCase extends BaseTestCase
     }
 
     /**
-     * Mock the HttpFetcher service and return the given data on fetch.
+     * Mock the http client used in BookStack http calls.
      */
-    protected function mockHttpFetch($returnData, int $times = 1)
+    protected function mockHttpClient(array $responses = []): HttpClientHistory
     {
-        $mockHttp = Mockery::mock(HttpFetcher::class);
-        $this->app[HttpFetcher::class] = $mockHttp;
-        $mockHttp->shouldReceive('fetch')
-            ->times($times)
-            ->andReturn($returnData);
-    }
-
-    /**
-     * Mock the http client used in BookStack.
-     * Returns a reference to the container which holds all history of http transactions.
-     *
-     * @link https://docs.guzzlephp.org/en/stable/testing.html#history-middleware
-     */
-    protected function &mockHttpClient(array $responses = []): array
-    {
-        $container = [];
-        $history = Middleware::history($container);
-        $mock = new MockHandler($responses);
-        $handlerStack = new HandlerStack($mock);
-        $handlerStack->push($history);
-        $this->app[ClientInterface::class] = new Client(['handler' => $handlerStack]);
-
-        return $container;
+        return $this->app->make(HttpRequestService::class)->mockClient($responses);
     }
 
     /**
index 6976f23847cf136f257ad9735fa8540cd2ea86eb..f0266cd0c1349420fdd4c07724b68d9b77b7ef56 100644 (file)
@@ -8,17 +8,15 @@ use BookStack\Activity\Models\Webhook;
 use BookStack\Entities\Models\Book;
 use BookStack\Entities\Models\Page;
 use BookStack\Entities\Tools\PageContent;
+use BookStack\Exceptions\ThemeException;
 use BookStack\Facades\Theme;
 use BookStack\Theming\ThemeEvents;
 use BookStack\Users\Models\User;
 use Illuminate\Console\Command;
-use Illuminate\Http\Client\Request as HttpClientRequest;
 use Illuminate\Http\Request;
 use Illuminate\Http\Response;
 use Illuminate\Support\Facades\Artisan;
 use Illuminate\Support\Facades\File;
-use Illuminate\Support\Facades\Http;
-use League\CommonMark\ConfigurableEnvironmentInterface;
 use League\CommonMark\Environment\Environment;
 
 class ThemeTest extends TestCase
@@ -54,6 +52,19 @@ class ThemeTest extends TestCase
         });
     }
 
+    public function test_theme_functions_loads_errors_are_caught_and_logged()
+    {
+        $this->usingThemeFolder(function ($themeFolder) {
+            $functionsFile = theme_path('functions.php');
+            file_put_contents($functionsFile, "<?php\n\\BookStack\\Biscuits::eat();");
+
+            $this->expectException(ThemeException::class);
+            $this->expectExceptionMessageMatches('/Failed loading theme functions file at ".*?" with error: Class "BookStack\\\\Biscuits" not found/');
+
+            $this->runWithEnv('APP_THEME', $themeFolder, fn() => null);
+        });
+    }
+
     public function test_event_commonmark_environment_configure()
     {
         $callbackCalled = false;
@@ -177,9 +188,7 @@ class ThemeTest extends TestCase
         };
         Theme::listen(ThemeEvents::WEBHOOK_CALL_BEFORE, $callback);
 
-        Http::fake([
-            '*' => Http::response('', 200),
-        ]);
+        $responses = $this->mockHttpClient([new \GuzzleHttp\Psr7\Response(200, [], '')]);
 
         $webhook = new Webhook(['name' => 'Test webhook', 'endpoint' => 'https://example.com']);
         $webhook->save();
@@ -193,9 +202,10 @@ class ThemeTest extends TestCase
         $this->assertEquals($webhook->id, $args[1]->id);
         $this->assertEquals($detail->id, $args[2]->id);
 
-        Http::assertSent(function (HttpClientRequest $request) {
-            return $request->isJson() && $request->data()['test'] === 'hello!';
-        });
+        $this->assertEquals(1, $responses->requestCount());
+        $request = $responses->latestRequest();
+        $reqData = json_decode($request->getBody(), true);
+        $this->assertEquals('hello!', $reqData['test']);
     }
 
     public function test_event_activity_logged()
index 363c1fa95420b858d42980f27e05d8e38e10a07a..f5b49a9fc21c245288b19cbd7966637b7dbb6bec 100644 (file)
@@ -3,9 +3,11 @@
 namespace Tests\Uploads;
 
 use BookStack\Exceptions\HttpFetchException;
-use BookStack\Uploads\HttpFetcher;
 use BookStack\Uploads\UserAvatars;
 use BookStack\Users\Models\User;
+use GuzzleHttp\Exception\ConnectException;
+use GuzzleHttp\Psr7\Request;
+use GuzzleHttp\Psr7\Response;
 use Tests\TestCase;
 
 class AvatarTest extends TestCase
@@ -22,27 +24,16 @@ class AvatarTest extends TestCase
         return User::query()->where('email', '=', $user->email)->first();
     }
 
-    protected function assertImageFetchFrom(string $url)
-    {
-        $http = $this->mock(HttpFetcher::class);
-
-        $http->shouldReceive('fetch')
-            ->once()->with($url)
-            ->andReturn($this->files->pngImageData());
-    }
-
-    protected function deleteUserImage(User $user)
+    protected function deleteUserImage(User $user): void
     {
         $this->files->deleteAtRelativePath($user->avatar->path);
     }
 
     public function test_gravatar_fetched_on_user_create()
     {
-        config()->set([
-            'services.disable_services' => false,
-        ]);
+        $requests = $this->mockHttpClient([new Response(200, ['Content-Type' => 'image/png'], $this->files->pngImageData())]);
+        config()->set(['services.disable_services' => false]);
         $user = User::factory()->make();
-        $this->assertImageFetchFrom('https://www.gravatar.com/avatar/' . md5(strtolower($user->email)) . '?s=500&d=identicon');
 
         $user = $this->createUserRequest($user);
         $this->assertDatabaseHas('images', [
@@ -50,6 +41,9 @@ class AvatarTest extends TestCase
             'created_by' => $user->id,
         ]);
         $this->deleteUserImage($user);
+
+        $expectedUri = 'https://www.gravatar.com/avatar/' . md5(strtolower($user->email)) . '?s=500&d=identicon';
+        $this->assertEquals($expectedUri, $requests->latestRequest()->getUri());
     }
 
     public function test_custom_url_used_if_set()
@@ -61,24 +55,22 @@ class AvatarTest extends TestCase
 
         $user = User::factory()->make();
         $url = 'https://example.com/' . urlencode(strtolower($user->email)) . '/' . md5(strtolower($user->email)) . '/500';
-        $this->assertImageFetchFrom($url);
+        $requests = $this->mockHttpClient([new Response(200, ['Content-Type' => 'image/png'], $this->files->pngImageData())]);
 
         $user = $this->createUserRequest($user);
+        $this->assertEquals($url, $requests->latestRequest()->getUri());
         $this->deleteUserImage($user);
     }
 
     public function test_avatar_not_fetched_if_no_custom_url_and_services_disabled()
     {
-        config()->set([
-            'services.disable_services' => true,
-        ]);
-
+        config()->set(['services.disable_services' => true]);
         $user = User::factory()->make();
-
-        $http = $this->mock(HttpFetcher::class);
-        $http->shouldNotReceive('fetch');
+        $requests = $this->mockHttpClient([new Response()]);
 
         $this->createUserRequest($user);
+
+        $this->assertEquals(0, $requests->requestCount());
     }
 
     public function test_avatar_not_fetched_if_avatar_url_option_set_to_false()
@@ -89,21 +81,18 @@ class AvatarTest extends TestCase
         ]);
 
         $user = User::factory()->make();
-
-        $http = $this->mock(HttpFetcher::class);
-        $http->shouldNotReceive('fetch');
+        $requests = $this->mockHttpClient([new Response()]);
 
         $this->createUserRequest($user);
+
+        $this->assertEquals(0, $requests->requestCount());
     }
 
     public function test_no_failure_but_error_logged_on_failed_avatar_fetch()
     {
-        config()->set([
-            'services.disable_services' => false,
-        ]);
+        config()->set(['services.disable_services' => false]);
 
-        $http = $this->mock(HttpFetcher::class);
-        $http->shouldReceive('fetch')->andThrow(new HttpFetchException());
+        $this->mockHttpClient([new ConnectException('Failed to connect', new Request('GET', ''))]);
 
         $logger = $this->withTestLogger();
 
@@ -122,17 +111,16 @@ class AvatarTest extends TestCase
 
         $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();
+        $this->mockHttpClient([new ConnectException('Could not resolve host http_malformed_url', new Request('GET', ''))]);
 
         $avatar->fetchAndAssignToUser($user);
 
+        $url = 'http_malformed_url/' . urlencode(strtolower($user->email)) . '/' . md5(strtolower($user->email)) . '/500';
         $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);
+        $this->assertInstanceOf(HttpFetchException::class, $exception);
+        $this->assertEquals('Cannot get image from ' . $url, $exception->getMessage());
+        $this->assertEquals('Could not resolve host http_malformed_url', $exception->getPrevious()->getMessage());
     }
 }
index 1b16b0b4544994d5e34f0bad54b190a92fbd0a85..f5dae3e763472bfd8ee9e73fe638837a47c8f7a4 100644 (file)
@@ -242,6 +242,22 @@ class UserPreferencesTest extends TestCase
         $this->withHtml($home)->assertElementExists('.dark-mode');
     }
 
+    public function test_dark_mode_toggle_endpoint_changes_to_light_when_dark_by_default()
+    {
+        config()->set('setting-defaults.user.dark-mode-enabled', true);
+        $editor = $this->users->editor();
+
+        $this->assertEquals(true, setting()->getUser($editor, 'dark-mode-enabled'));
+        $prefChange = $this->actingAs($editor)->patch('/preferences/toggle-dark-mode');
+        $prefChange->assertRedirect();
+        $this->assertEquals(false, setting()->getUser($editor, 'dark-mode-enabled'));
+
+        $home = $this->get('/');
+        $this->withHtml($home)->assertElementNotExists('.dark-mode');
+        $home->assertDontSee('Light Mode');
+        $home->assertSee('Dark Mode');
+    }
+
     public function test_books_view_type_preferences_when_list()
     {
         $editor = $this->users->editor();