From: Dan Brown Date: Sat, 16 Sep 2023 10:55:57 +0000 (+0100) Subject: Merge branch 'v23-08' into development X-Git-Tag: v23.10~1^2~36 X-Git-Url: http://source.bookstackapp.com/bookstack/commitdiff_plain/9ac932fc28ac7085eb956c2c217e9b8612462d4a?hp=45b8d6cd0c214118a3745050b64805be75cec9d8 Merge branch 'v23-08' into development --- diff --git a/.env.example.complete b/.env.example.complete index 547e81818..0853bd1fe 100644 --- a/.env.example.complete +++ b/.env.example.complete @@ -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 diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 5c8734fd6..d301826c9 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -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 diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index fb1e0b3b0..0ebb8e72f 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -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: diff --git a/.github/ISSUE_TEMPLATE/support_request.yml b/.github/ISSUE_TEMPLATE/support_request.yml index cb2476546..4db8d1ced 100644 --- a/.github/ISSUE_TEMPLATE/support_request.yml +++ b/.github/ISSUE_TEMPLATE/support_request.yml @@ -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 diff --git a/.github/translators.txt b/.github/translators.txt index 3f73d7183..e07730183 100644 --- a/.github/translators.txt +++ b/.github/translators.txt @@ -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 diff --git a/app/Access/EmailConfirmationService.php b/app/Access/EmailConfirmationService.php index 79220f996..de9ea69b8 100644 --- a/app/Access/EmailConfirmationService.php +++ b/app/Access/EmailConfirmationService.php @@ -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)); } /** diff --git a/app/Notifications/ConfirmEmail.php b/app/Access/Notifications/ConfirmEmailNotification.php similarity index 82% rename from app/Notifications/ConfirmEmail.php rename to app/Access/Notifications/ConfirmEmailNotification.php index 6b395e137..c67e1cf62 100644 --- a/app/Notifications/ConfirmEmail.php +++ b/app/Access/Notifications/ConfirmEmailNotification.php @@ -1,11 +1,12 @@ issuer, 'https://') !== 0) { + if (!str_starts_with($this->issuer, 'https://')) { throw new InvalidArgumentException('Issuer value must start with https://'); } } diff --git a/app/Access/Oidc/OidcService.php b/app/Access/Oidc/OidcService.php index 6d13fe8f1..8778cbd98 100644 --- a/app/Access/Oidc/OidcService.php +++ b/app/Access/Oidc/OidcService.php @@ -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; diff --git a/app/Access/UserInviteService.php b/app/Access/UserInviteService.php index 00e361e82..7d955f385 100644 --- a/app/Access/UserInviteService.php +++ b/app/Access/UserInviteService.php @@ -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)); } } diff --git a/app/Activity/Controllers/FavouriteController.php b/app/Activity/Controllers/FavouriteController.php index 1b88ffd64..d2534ddfe 100644 --- a/app/Activity/Controllers/FavouriteController.php +++ b/app/Activity/Controllers/FavouriteController.php @@ -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; - } } diff --git a/app/Activity/Controllers/WatchController.php b/app/Activity/Controllers/WatchController.php index 3d7e18116..c0b1c5872 100644 --- a/app/Activity/Controllers/WatchController.php +++ b/app/Activity/Controllers/WatchController.php @@ -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; - } } diff --git a/app/Activity/DispatchWebhookJob.php b/app/Activity/DispatchWebhookJob.php index 405bca49c..e1771b114 100644 --- a/app/Activity/DispatchWebhookJob.php +++ b/app/Activity/DispatchWebhookJob.php @@ -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(); diff --git a/app/Activity/Notifications/Messages/BaseActivityNotification.php b/app/Activity/Notifications/Messages/BaseActivityNotification.php index a2045c8dc..414859091 100644 --- a/app/Activity/Notifications/Messages/BaseActivityNotification.php +++ b/app/Activity/Notifications/Messages/BaseActivityNotification.php @@ -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; diff --git a/app/Notifications/MailNotification.php b/app/App/MailNotification.php similarity index 96% rename from app/Notifications/MailNotification.php rename to app/App/MailNotification.php index 1f40219f9..8c57b5621 100644 --- a/app/Notifications/MailNotification.php +++ b/app/App/MailNotification.php @@ -1,6 +1,6 @@ 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); }); diff --git a/app/App/Providers/EventServiceProvider.php b/app/App/Providers/EventServiceProvider.php index bf502d7da..4ec9facdf 100644 --- a/app/App/Providers/EventServiceProvider.php +++ b/app/App/Providers/EventServiceProvider.php @@ -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', ], ]; diff --git a/app/Config/mail.php b/app/Config/mail.php index 6400211e8..2906d769a 100644 --- a/app/Config/mail.php +++ b/app/Config/mail.php @@ -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'), ], diff --git a/app/Config/oidc.php b/app/Config/oidc.php index 1f73fb688..b28b8a41a 100644 --- a/app/Config/oidc.php +++ b/app/Config/oidc.php @@ -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'), diff --git a/app/Console/Commands/CleanupImagesCommand.php b/app/Console/Commands/CleanupImagesCommand.php index fe924b0f4..18e60ff17 100644 --- a/app/Console/Commands/CleanupImagesCommand.php +++ b/app/Console/Commands/CleanupImagesCommand.php @@ -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 index 000000000..8319f6aa0 --- /dev/null +++ b/app/Entities/Tools/MixedEntityRequestHelper.php @@ -0,0 +1,39 @@ +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 index 000000000..b721effe2 --- /dev/null +++ b/app/Exceptions/ThemeException.php @@ -0,0 +1,7 @@ +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 index 000000000..f59c298a6 --- /dev/null +++ b/app/Http/HttpRequestService.php @@ -0,0 +1,70 @@ + $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; + } +} diff --git a/app/Settings/MaintenanceController.php b/app/Settings/MaintenanceController.php index 9c48a4323..60e5fee28 100644 --- a/app/Settings/MaintenanceController.php +++ b/app/Settings/MaintenanceController.php @@ -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(); diff --git a/app/Notifications/TestEmail.php b/app/Settings/TestEmailNotification.php similarity index 78% rename from app/Notifications/TestEmail.php rename to app/Settings/TestEmailNotification.php index af9e5847f..3b13543e4 100644 --- a/app/Notifications/TestEmail.php +++ b/app/Settings/TestEmailNotification.php @@ -1,11 +1,12 @@ + */ + 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); diff --git a/app/Translation/LanguageManager.php b/app/Translation/LanguageManager.php index 5132929b1..ec116fd70 100644 --- a/app/Translation/LanguageManager.php +++ b/app/Translation/LanguageManager.php @@ -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 index fcb4147e9..000000000 --- a/app/Uploads/HttpFetcher.php +++ /dev/null @@ -1,38 +0,0 @@ - $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; - } -} diff --git a/app/Uploads/UserAvatars.php b/app/Uploads/UserAvatars.php index 3cd37812a..9692b3f38 100644 --- a/app/Uploads/UserAvatars.php +++ b/app/Uploads/UserAvatars.php @@ -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'); } /** diff --git a/app/Users/Controllers/UserPreferencesController.php b/app/Users/Controllers/UserPreferencesController.php index 9c38ff2af..08d65743b 100644 --- a/app/Users/Controllers/UserPreferencesController.php +++ b/app/Users/Controllers/UserPreferencesController.php @@ -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(); diff --git a/app/Users/Models/User.php b/app/Users/Models/User.php index e3d856a8d..2479521a2 100644 --- a/app/Users/Models/User.php +++ b/app/Users/Models/User.php @@ -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)); } /** diff --git a/composer.json b/composer.json index 28bdd5489..037f8984e 100644 --- a/composer.json +++ b/composer.json @@ -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" }, diff --git a/composer.lock b/composer.lock index 04232b1a6..5c3ad09a8 100644 --- a/composer.lock +++ b/composer.lock @@ -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", @@ -62,16 +62,16 @@ }, { "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": { @@ -151,9 +151,9 @@ "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", @@ -421,16 +421,16 @@ }, { "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": { @@ -465,9 +465,9 @@ ], "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", @@ -1383,22 +1383,22 @@ }, { "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" @@ -1489,7 +1489,7 @@ ], "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": [ { @@ -1505,7 +1505,7 @@ "type": "tidelift" } ], - "time": "2023-05-21T14:04:53+00:00" + "time": "2023-08-27T10:20:53+00:00" }, { "name": "guzzlehttp/promises", @@ -1592,16 +1592,16 @@ }, { "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": { @@ -1688,7 +1688,7 @@ ], "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": [ { @@ -1704,20 +1704,20 @@ "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": { @@ -1725,15 +1725,11 @@ "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" @@ -1772,7 +1768,7 @@ ], "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": [ { @@ -1788,7 +1784,7 @@ "type": "tidelift" } ], - "time": "2021-10-07T12:57:01+00:00" + "time": "2023-08-27T10:19:19+00:00" }, { "name": "intervention/image", @@ -1876,16 +1872,16 @@ }, { "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": { @@ -1896,8 +1892,8 @@ "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": { @@ -1944,9 +1940,9 @@ ], "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", @@ -2208,16 +2204,16 @@ }, { "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": { @@ -2274,20 +2270,20 @@ "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": { @@ -2300,6 +2296,7 @@ }, "require-dev": { "mockery/mockery": "~1.3.3|^1.4.2", + "phpstan/phpstan": "^1.10", "phpunit/phpunit": "^8.5.8|^9.3.3" }, "suggest": { @@ -2340,22 +2337,22 @@ ], "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": { @@ -2448,7 +2445,7 @@ "type": "tidelift" } ], - "time": "2023-03-24T15:16:10+00:00" + "time": "2023-08-30T16:55:00+00:00" }, { "name": "league/config", @@ -2534,16 +2531,16 @@ }, { "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": { @@ -2552,6 +2549,8 @@ "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", @@ -2571,7 +2570,7 @@ "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", @@ -2606,7 +2605,7 @@ ], "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": [ { @@ -2618,20 +2617,20 @@ "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": { @@ -2672,7 +2671,7 @@ ], "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": [ { @@ -2684,20 +2683,20 @@ "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": { @@ -2732,7 +2731,7 @@ ], "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": [ { @@ -2744,7 +2743,7 @@ "type": "github" } ], - "time": "2023-05-02T20:02:14+00:00" + "time": "2023-08-30T10:23:59+00:00" }, { "name": "league/html-to-markdown", @@ -3208,25 +3207,25 @@ }, { "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" @@ -3234,7 +3233,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.6-dev" + "dev-master": "2.7-dev" } }, "autoload": { @@ -3250,6 +3249,11 @@ "MIT" ], "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, { "name": "Michael Dowling", "email": "mtdowling@gmail.com", @@ -3263,22 +3267,22 @@ ], "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": { @@ -3371,7 +3375,7 @@ "type": "tidelift" } ], - "time": "2023-08-03T09:00:52+00:00" + "time": "2023-09-07T16:43:50+00:00" }, { "name": "nette/schema", @@ -5176,22 +5180,22 @@ }, { "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", @@ -5246,7 +5250,7 @@ "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", @@ -5349,48 +5353,6 @@ }, "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": "hello@brianfaust.de" - } - ], - "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", @@ -6342,16 +6304,16 @@ }, { "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": { @@ -6366,7 +6328,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -6404,7 +6366,7 @@ "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": [ { @@ -6420,20 +6382,20 @@ "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": { @@ -6445,7 +6407,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -6485,7 +6447,7 @@ "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": [ { @@ -6501,20 +6463,20 @@ "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": { @@ -6528,7 +6490,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -6572,7 +6534,7 @@ "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": [ { @@ -6588,20 +6550,20 @@ "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": { @@ -6613,7 +6575,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -6656,7 +6618,7 @@ "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": [ { @@ -6672,20 +6634,20 @@ "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": { @@ -6700,7 +6662,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -6739,7 +6701,7 @@ "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": [ { @@ -6755,20 +6717,20 @@ "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": { @@ -6777,7 +6739,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -6815,7 +6777,7 @@ "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": [ { @@ -6831,20 +6793,20 @@ "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": { @@ -6853,7 +6815,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -6898,7 +6860,7 @@ "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": [ { @@ -6914,20 +6876,20 @@ "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": { @@ -6936,7 +6898,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -6977,7 +6939,7 @@ "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": [ { @@ -6993,20 +6955,20 @@ "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": { @@ -7021,7 +6983,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -7059,7 +7021,7 @@ "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": [ { @@ -7075,7 +7037,7 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { "name": "symfony/process", @@ -8855,16 +8817,16 @@ }, { "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": { @@ -8913,7 +8875,7 @@ "type": "tidelift" } ], - "time": "2023-08-14T13:24:11+00:00" + "time": "2023-09-04T12:20:53+00:00" }, { "name": "phpunit/php-code-coverage", diff --git a/lang/en/settings.php b/lang/en/settings.php index 8821c77f0..9f60606ac 100644 --- a/lang/en/settings.php +++ b/lang/en/settings.php @@ -314,6 +314,7 @@ return [ 'sv' => 'Svenska', 'tr' => 'Türkçe', 'uk' => 'Українська', + 'uz' => 'O‘zbekcha', 'vi' => 'Tiếng Việt', 'zh_CN' => '简体中文', 'zh_TW' => '繁體中文', diff --git a/readme.md b/readme.md index 72f7d8a35..aa335f9da 100644 --- 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&message=bookstack@foss.video&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). diff --git a/resources/views/books/show.blade.php b/resources/views/books/show.blade.php index 75b01a379..e3aef845c 100644 --- a/resources/views/books/show.blade.php +++ b/resources/views/books/show.blade.php @@ -171,7 +171,7 @@ @endif @if(count($activity) > 0) -
+
{{ trans('entities.recent_activity') }}
@include('common.activity-list', ['activity' => $activity])
diff --git a/resources/views/entities/favourite-action.blade.php b/resources/views/entities/favourite-action.blade.php index 35189044b..e596fbdce 100644 --- a/resources/views/entities/favourite-action.blade.php +++ b/resources/views/entities/favourite-action.blade.php @@ -3,7 +3,7 @@ @endphp
{{ csrf_field() }} - +
-
-
-

{{ trans('entities.recent_activity') }}

- @include('common.activity-list', ['activity' => $activity]) -
+
+

{{ trans('entities.recent_activity') }}

+ @include('common.activity-list', ['activity' => $activity])
diff --git a/resources/views/layouts/base.blade.php b/resources/views/layouts/base.blade.php index e0a6f46d0..8875788a6 100644 --- a/resources/views/layouts/base.blade.php +++ b/resources/views/layouts/base.blade.php @@ -65,7 +65,9 @@
@yield('bottom') - + @if($cspNonce ?? false) + + @endif @yield('scripts') @include('layouts.parts.base-body-end') diff --git a/resources/views/shelves/show.blade.php b/resources/views/shelves/show.blade.php index 8019a557f..86dd6326d 100644 --- a/resources/views/shelves/show.blade.php +++ b/resources/views/shelves/show.blade.php @@ -99,7 +99,7 @@ @if(count($activity) > 0) -
+
{{ trans('entities.recent_activity') }}
@include('common.activity-list', ['activity' => $activity])
diff --git a/tests/Actions/WebhookCallTest.php b/tests/Actions/WebhookCallTest.php index 0746aa3a1..81bd7e7e8 100644 --- a/tests/Actions/WebhookCallTest.php +++ b/tests/Actions/WebhookCallTest.php @@ -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) diff --git a/tests/Activity/WatchTest.php b/tests/Activity/WatchTest.php index 464886155..5b9ae5a4c 100644 --- a/tests/Activity/WatchTest.php +++ b/tests/Activity/WatchTest.php @@ -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' ]); diff --git a/tests/Api/UsersApiTest.php b/tests/Api/UsersApiTest.php index e2a04b528..6ad727257 100644 --- a/tests/Api/UsersApiTest.php +++ b/tests/Api/UsersApiTest.php @@ -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', '=', 'bboris@example.com')->first(); - Notification::assertSentTo($user, UserInvite::class); + Notification::assertSentTo($user, UserInviteNotification::class); } public function test_create_name_and_email_validation() diff --git a/tests/Auth/OidcTest.php b/tests/Auth/OidcTest.php index 191a25f88..204a3bb5f 100644 --- a/tests/Auth/OidcTest.php +++ b/tests/Auth/OidcTest.php @@ -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' => 'benny@example.com', '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' => 'benny@example.com', + 'sub' => 'benny1010101', + 'first_name' => 'Benny', + 'last_name' => 'Jenkins' + ]); + + $this->assertDatabaseHas('users', [ + 'name' => 'Benny Jenkins', + 'email' => 'benny@example.com', + ]); + } + public function test_login_group_sync() { config()->set([ diff --git a/tests/Auth/RegistrationTest.php b/tests/Auth/RegistrationTest.php index bc190afd8..ff1a9d66b 100644 --- a/tests/Auth/RegistrationTest.php +++ b/tests/Auth/RegistrationTest.php @@ -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; }); diff --git a/tests/Auth/ResetPasswordTest.php b/tests/Auth/ResetPasswordTest.php index b97a2f2d3..e60ac5643 100644 --- a/tests/Auth/ResetPasswordTest.php +++ b/tests/Auth/ResetPasswordTest.php @@ -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', '=', 'admin@admin.com')->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.'); } } diff --git a/tests/Auth/UserInviteTest.php b/tests/Auth/UserInviteTest.php index 8d6143877..a9dee0007 100644 --- a/tests/Auth/UserInviteTest.php +++ b/tests/Auth/UserInviteTest.php @@ -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); diff --git a/tests/Commands/CleanupImagesCommandTest.php b/tests/Commands/CleanupImagesCommandTest.php index a1a5ab985..36fd51e96 100644 --- a/tests/Commands/CleanupImagesCommandTest.php +++ b/tests/Commands/CleanupImagesCommandTest.php @@ -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]); + } } diff --git a/tests/Entity/CommentTest.php b/tests/Entity/CommentTest.php index 0a71bb6ef..23fc68197 100644 --- a/tests/Entity/CommentTest.php +++ b/tests/Entity/CommentTest.php @@ -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…'); + } } diff --git a/tests/FavouriteTest.php b/tests/FavouriteTest.php index 0e30cbd58..48048e284 100644 --- a/tests/FavouriteTest.php +++ b/tests/FavouriteTest.php @@ -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()); diff --git a/tests/Settings/TestEmailTest.php b/tests/Settings/TestEmailTest.php index 322f90107..e96024e7b 100644 --- a/tests/Settings/TestEmailTest.php +++ b/tests/Settings/TestEmailTest.php @@ -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); } } diff --git a/tests/TestCase.php b/tests/TestCase.php index 0ab0792bd..f8f59977a 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -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); } /** diff --git a/tests/ThemeTest.php b/tests/ThemeTest.php index 6976f2384..f0266cd0c 100644 --- a/tests/ThemeTest.php +++ b/tests/ThemeTest.php @@ -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, "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() diff --git a/tests/Uploads/AvatarTest.php b/tests/Uploads/AvatarTest.php index 363c1fa95..f5b49a9fc 100644 --- a/tests/Uploads/AvatarTest.php +++ b/tests/Uploads/AvatarTest.php @@ -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()); } } diff --git a/tests/User/UserPreferencesTest.php b/tests/User/UserPreferencesTest.php index 1b16b0b45..f5dae3e76 100644 --- a/tests/User/UserPreferencesTest.php +++ b/tests/User/UserPreferencesTest.php @@ -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();