# 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
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:
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 '....'
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
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
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
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:
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
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
@Jokuna :: Korean
@smartshogu :: German; German Informal
@samadha56 :: Persian
+@mrmuminov :: Uzbek
cipi1965 :: Italian
Mykola Ronik (Mantikor) :: Ukrainian
furkanoyk :: Turkish
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
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
Péter Péli (peter.peli) :: Hungarian
TWME :: Chinese Traditional
Sascha (Man-in-Black) :: German
+Mohammadreza Madadi (madadi.efl) :: Persian
+Konstantin Kovacheli (kkovacheli) :: Ukrainian
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
$this->deleteByUser($user);
$token = $this->createTokenForUser($user);
- $user->notify(new ConfirmEmail($token));
+ $user->notify(new ConfirmEmailNotification($token));
}
/**
<?php
-namespace BookStack\Notifications;
+namespace BookStack\Access\Notifications;
+use BookStack\App\MailNotification;
use BookStack\Users\Models\User;
use Illuminate\Notifications\Messages\MailMessage;
-class ConfirmEmail extends MailNotification
+class ConfirmEmailNotification extends MailNotification
{
public function __construct(
public string $token
<?php
-namespace BookStack\Notifications;
+namespace BookStack\Access\Notifications;
+use BookStack\App\MailNotification;
use BookStack\Users\Models\User;
use Illuminate\Notifications\Messages\MailMessage;
-class ResetPassword extends MailNotification
+class ResetPasswordNotification extends MailNotification
{
public function __construct(
public string $token
<?php
-namespace BookStack\Notifications;
+namespace BookStack\Access\Notifications;
+use BookStack\App\MailNotification;
use BookStack\Users\Models\User;
use Illuminate\Notifications\Messages\MailMessage;
-class UserInvite extends MailNotification
+class UserInviteNotification extends MailNotification
{
public function __construct(
public string $token
{
use BearerAuthorizationTrait;
- /**
- * @var string
- */
- protected $authorizationEndpoint;
-
- /**
- * @var string
- */
- protected $tokenEndpoint;
+ protected string $authorizationEndpoint;
+ protected string $tokenEndpoint;
/**
* Scopes to use for the OIDC authorization call.
}
/**
- * Add an additional scope to this provider upon the default.
+ * Add another scope to this provider upon the default.
*/
public function addScope(string $scope): void
{
}
}
- if (strpos($this->issuer, 'https://') !== 0) {
+ if (!str_starts_with($this->issuer, 'https://')) {
throw new InvalidArgumentException('Issuer value must start with https://');
}
}
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
public function __construct(
protected RegistrationService $registrationService,
protected LoginService $loginService,
- protected HttpClient $httpClient,
+ protected HttpRequestService $http,
protected GroupSyncService $groupService
) {
}
// 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());
}
protected function getProvider(OidcProviderSettings $settings): OidcOAuthProvider
{
$provider = new OidcOAuthProvider($settings->arrayForProvider(), [
- 'httpClient' => $this->httpClient,
+ 'httpClient' => $this->http->buildClient(5),
'optionProvider' => new HttpBasicAuthOptionProvider(),
]);
*/
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;
namespace BookStack\Access;
-use BookStack\Notifications\UserInvite;
+use BookStack\Access\Notifications\UserInviteNotification;
use BookStack\Users\Models\User;
class UserInviteService extends UserTokenService
{
$this->deleteByUser($user);
$token = $this->createTokenForUser($user);
- $user->notify(new UserInvite($token));
+ $user->notify(new UserInviteNotification($token));
}
}
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.
*/
*/
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();
*/
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;
- }
}
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']);
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;
- }
}
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;
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
*
* @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();
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;
<?php
-namespace BookStack\Notifications;
+namespace BookStack\App;
use BookStack\Users\Models\User;
use Illuminate\Bus\Queueable;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Page;
use BookStack\Exceptions\BookStackExceptionHandlerPage;
+use BookStack\Http\HttpRequestService;
use BookStack\Permissions\PermissionApplicator;
use BookStack\Settings\SettingService;
use BookStack\Util\CspService;
-use GuzzleHttp\Client;
use Illuminate\Contracts\Foundation\ExceptionRenderer;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\ServiceProvider;
-use Psr\Http\Client\ClientInterface as HttpClientInterface;
class AppServiceProvider extends ServiceProvider
{
SettingService::class => SettingService::class,
SocialAuthService::class => SocialAuthService::class,
CspService::class => CspService::class,
+ HttpRequestService::class => HttpRequestService::class,
];
/**
// 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');
}
*/
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);
});
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
{
*/
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',
],
];
// 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'),
],
'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'),
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;
}
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');
}
$this->showDeletedImages($deleted);
- $this->comment($deleteCount . ' images deleted');
+ $this->comment("{$deleteCount} image(s) deleted");
+
return 0;
}
}
if (count($paths) > 0) {
- $this->line('Images to delete:');
+ $this->line('Image(s) to delete:');
}
foreach ($paths as $path) {
--- /dev/null
+<?php
+
+namespace BookStack\Entities\Tools;
+
+use BookStack\Entities\EntityProvider;
+use BookStack\Entities\Models\Entity;
+
+class MixedEntityRequestHelper
+{
+ public function __construct(
+ protected EntityProvider $entities,
+ ) {
+ }
+
+ /**
+ * Query out an entity, visible to the current user, for the given
+ * entity request details (this provided in a request validated by
+ * this classes' validationRules method).
+ * @param array{type: string, id: string} $requestData
+ */
+ public function getVisibleEntityFromRequestData(array $requestData): Entity
+ {
+ $entityType = $this->entities->get($requestData['type']);
+
+ return $entityType->newQuery()->scopes(['visible'])->findOrFail($requestData['id']);
+ }
+
+ /**
+ * Get the validation rules for an abstract entity request.
+ * @return array{type: string[], id: string[]}
+ */
+ public function validationRules(): array
+ {
+ return [
+ 'type' => ['required', 'string'],
+ 'id' => ['required', 'integer'],
+ ];
+ }
+}
--- /dev/null
+<?php
+
+namespace BookStack\Exceptions;
+
+class ThemeException extends \Exception
+{
+}
--- /dev/null
+<?php
+
+namespace BookStack\Http;
+
+use GuzzleHttp\Psr7\Request as GuzzleRequest;
+
+class HttpClientHistory
+{
+ public function __construct(
+ protected &$container
+ ) {
+ }
+
+ public function requestCount(): int
+ {
+ return count($this->container);
+ }
+
+ public function requestAt(int $index): ?GuzzleRequest
+ {
+ return $this->container[$index]['request'] ?? null;
+ }
+
+ public function latestRequest(): ?GuzzleRequest
+ {
+ return $this->requestAt($this->requestCount() - 1);
+ }
+
+ public function all(): array
+ {
+ return $this->container;
+ }
+}
--- /dev/null
+<?php
+
+namespace BookStack\Http;
+
+use GuzzleHttp\Client;
+use GuzzleHttp\Handler\MockHandler;
+use GuzzleHttp\HandlerStack;
+use GuzzleHttp\Middleware;
+use GuzzleHttp\Psr7\Request as GuzzleRequest;
+use GuzzleHttp\Psr7\Response;
+use Psr\Http\Client\ClientInterface;
+
+class HttpRequestService
+{
+ protected ?HandlerStack $handler = null;
+
+ /**
+ * Build a new http client for sending requests on.
+ */
+ public function buildClient(int $timeout, array $options = []): ClientInterface
+ {
+ $defaultOptions = [
+ 'timeout' => $timeout,
+ 'handler' => $this->handler,
+ ];
+
+ return new Client(array_merge($options, $defaultOptions));
+ }
+
+ /**
+ * Create a new JSON http request for use with a client.
+ */
+ public function jsonRequest(string $method, string $uri, array $data): GuzzleRequest
+ {
+ $headers = ['Content-Type' => 'application/json'];
+ return new GuzzleRequest($method, $uri, $headers, json_encode($data));
+ }
+
+ /**
+ * Mock any http clients built from this service, and response with the given responses.
+ * Returns history which can then be queried.
+ * @link https://docs.guzzlephp.org/en/stable/testing.html#history-middleware
+ */
+ public function mockClient(array $responses = [], bool $pad = true): HttpClientHistory
+ {
+ // By default, we pad out the responses with 10 successful values so that requests will be
+ // properly recorded for inspection. Otherwise, we can't later check if we're received
+ // too many requests.
+ if ($pad) {
+ $response = new Response(200, [], 'success');
+ $responses = array_merge($responses, array_fill(0, 10, $response));
+ }
+
+ $container = [];
+ $history = Middleware::history($container);
+ $mock = new MockHandler($responses);
+ $this->handler = HandlerStack::create($mock);
+ $this->handler->push($history, 'history');
+
+ return new HttpClientHistory($container);
+ }
+
+ /**
+ * Clear mocking that has been set up for clients.
+ */
+ public function clearMocking(): void
+ {
+ $this->handler = null;
+ }
+}
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;
$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();
<?php
-namespace BookStack\Notifications;
+namespace BookStack\Settings;
+use BookStack\App\MailNotification;
use BookStack\Users\Models\User;
use Illuminate\Notifications\Messages\MailMessage;
-class TestEmail extends MailNotification
+class TestEmailNotification extends MailNotification
{
public function toMail(User $notifiable): MailMessage
{
namespace BookStack\Theming;
use BookStack\Access\SocialAuthService;
+use BookStack\Exceptions\ThemeException;
use Illuminate\Console\Application;
use Illuminate\Console\Application as Artisan;
use Symfony\Component\Console\Command\Command;
class ThemeService
{
- protected $listeners = [];
+ /**
+ * @var array<string, callable[]>
+ */
+ protected array $listeners = [];
/**
* Listen to a given custom theme event,
* setting up the action to be ran when the event occurs.
*/
- public function listen(string $event, callable $action)
+ public function listen(string $event, callable $action): void
{
if (!isset($this->listeners[$event])) {
$this->listeners[$event] = [];
*
* 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);
/**
* 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]);
/**
* 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);
'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)'],
+++ /dev/null
-<?php
-
-namespace BookStack\Uploads;
-
-use BookStack\Exceptions\HttpFetchException;
-
-class HttpFetcher
-{
- /**
- * Fetch content from an external URI.
- *
- * @param string $uri
- *
- * @throws HttpFetchException
- *
- * @return bool|string
- */
- public function fetch(string $uri)
- {
- $ch = curl_init();
- curl_setopt_array($ch, [
- CURLOPT_URL => $uri,
- CURLOPT_RETURNTRANSFER => 1,
- CURLOPT_CONNECTTIMEOUT => 5,
- ]);
-
- $data = curl_exec($ch);
- $err = curl_error($ch);
- curl_close($ch);
-
- if ($err) {
- $errno = curl_errno($ch);
- throw new HttpFetchException($err, $errno);
- }
-
- return $data;
- }
-}
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
+ ) {
}
/**
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);
}
{
$fetchUrl = $this->getAvatarUrl();
- return is_string($fetchUrl) && strpos($fetchUrl, 'http') === 0;
+ return str_starts_with($fetchUrl, 'http');
}
/**
*/
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();
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;
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;
return $splitName[0];
}
- return '';
+ return mb_substr($this->name, 0, max($chars - 2, 0)) . '…';
}
/**
*/
public function sendPasswordResetNotification($token)
{
- $this->notify(new ResetPassword($token));
+ $this->notify(new ResetPasswordNotification($token));
}
/**
"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",
"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"
},
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "d010cf625b58a0dc43addda7881ea42f",
+ "content-hash": "db3cca7a93d7d097c9f517b6fe00ee7b",
"packages": [
{
"name": "aws/aws-crt-php",
},
{
"name": "aws/aws-sdk-php",
- "version": "3.279.2",
+ "version": "3.281.3",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
- "reference": "ebd5e47c5be0425bb5cf4f80737850ed74767107"
+ "reference": "a3cfb20dbfb11117b7174b2594fc597745252e0c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/ebd5e47c5be0425bb5cf4f80737850ed74767107",
- "reference": "ebd5e47c5be0425bb5cf4f80737850ed74767107",
+ "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/a3cfb20dbfb11117b7174b2594fc597745252e0c",
+ "reference": "a3cfb20dbfb11117b7174b2594fc597745252e0c",
"shasum": ""
},
"require": {
"support": {
"forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
"issues": "https://github.com/aws/aws-sdk-php/issues",
- "source": "https://github.com/aws/aws-sdk-php/tree/3.279.2"
+ "source": "https://github.com/aws/aws-sdk-php/tree/3.281.3"
},
- "time": "2023-08-18T18:13:09+00:00"
+ "time": "2023-09-08T18:06:26+00:00"
},
{
"name": "bacon/bacon-qr-code",
},
{
"name": "dasprid/enum",
- "version": "1.0.4",
+ "version": "1.0.5",
"source": {
"type": "git",
"url": "https://github.com/DASPRiD/Enum.git",
- "reference": "8e6b6ea76eabbf19ea2bf5b67b98e1860474012f"
+ "reference": "6faf451159fb8ba4126b925ed2d78acfce0dc016"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/8e6b6ea76eabbf19ea2bf5b67b98e1860474012f",
- "reference": "8e6b6ea76eabbf19ea2bf5b67b98e1860474012f",
+ "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/6faf451159fb8ba4126b925ed2d78acfce0dc016",
+ "reference": "6faf451159fb8ba4126b925ed2d78acfce0dc016",
"shasum": ""
},
"require": {
],
"support": {
"issues": "https://github.com/DASPRiD/Enum/issues",
- "source": "https://github.com/DASPRiD/Enum/tree/1.0.4"
+ "source": "https://github.com/DASPRiD/Enum/tree/1.0.5"
},
- "time": "2023-03-01T18:44:03+00:00"
+ "time": "2023-08-25T16:18:39+00:00"
},
{
"name": "dflydev/dot-access-data",
},
{
"name": "guzzlehttp/guzzle",
- "version": "7.7.0",
+ "version": "7.8.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/guzzle.git",
- "reference": "fb7566caccf22d74d1ab270de3551f72a58399f5"
+ "reference": "1110f66a6530a40fe7aea0378fe608ee2b2248f9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/guzzle/guzzle/zipball/fb7566caccf22d74d1ab270de3551f72a58399f5",
- "reference": "fb7566caccf22d74d1ab270de3551f72a58399f5",
+ "url": "https://api.github.com/repos/guzzle/guzzle/zipball/1110f66a6530a40fe7aea0378fe608ee2b2248f9",
+ "reference": "1110f66a6530a40fe7aea0378fe608ee2b2248f9",
"shasum": ""
},
"require": {
"ext-json": "*",
- "guzzlehttp/promises": "^1.5.3 || ^2.0",
- "guzzlehttp/psr7": "^1.9.1 || ^2.4.5",
+ "guzzlehttp/promises": "^1.5.3 || ^2.0.1",
+ "guzzlehttp/psr7": "^1.9.1 || ^2.5.1",
"php": "^7.2.5 || ^8.0",
"psr/http-client": "^1.0",
"symfony/deprecation-contracts": "^2.2 || ^3.0"
],
"support": {
"issues": "https://github.com/guzzle/guzzle/issues",
- "source": "https://github.com/guzzle/guzzle/tree/7.7.0"
+ "source": "https://github.com/guzzle/guzzle/tree/7.8.0"
},
"funding": [
{
"type": "tidelift"
}
],
- "time": "2023-05-21T14:04:53+00:00"
+ "time": "2023-08-27T10:20:53+00:00"
},
{
"name": "guzzlehttp/promises",
},
{
"name": "guzzlehttp/psr7",
- "version": "2.6.0",
+ "version": "2.6.1",
"source": {
"type": "git",
"url": "https://github.com/guzzle/psr7.git",
- "reference": "8bd7c33a0734ae1c5d074360512beb716bef3f77"
+ "reference": "be45764272e8873c72dbe3d2edcfdfcc3bc9f727"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/guzzle/psr7/zipball/8bd7c33a0734ae1c5d074360512beb716bef3f77",
- "reference": "8bd7c33a0734ae1c5d074360512beb716bef3f77",
+ "url": "https://api.github.com/repos/guzzle/psr7/zipball/be45764272e8873c72dbe3d2edcfdfcc3bc9f727",
+ "reference": "be45764272e8873c72dbe3d2edcfdfcc3bc9f727",
"shasum": ""
},
"require": {
],
"support": {
"issues": "https://github.com/guzzle/psr7/issues",
- "source": "https://github.com/guzzle/psr7/tree/2.6.0"
+ "source": "https://github.com/guzzle/psr7/tree/2.6.1"
},
"funding": [
{
"type": "tidelift"
}
],
- "time": "2023-08-03T15:06:02+00:00"
+ "time": "2023-08-27T10:13:57+00:00"
},
{
"name": "guzzlehttp/uri-template",
- "version": "v1.0.1",
+ "version": "v1.0.2",
"source": {
"type": "git",
"url": "https://github.com/guzzle/uri-template.git",
- "reference": "b945d74a55a25a949158444f09ec0d3c120d69e2"
+ "reference": "61bf437fc2197f587f6857d3ff903a24f1731b5d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/guzzle/uri-template/zipball/b945d74a55a25a949158444f09ec0d3c120d69e2",
- "reference": "b945d74a55a25a949158444f09ec0d3c120d69e2",
+ "url": "https://api.github.com/repos/guzzle/uri-template/zipball/61bf437fc2197f587f6857d3ff903a24f1731b5d",
+ "reference": "61bf437fc2197f587f6857d3ff903a24f1731b5d",
"shasum": ""
},
"require": {
"symfony/polyfill-php80": "^1.17"
},
"require-dev": {
+ "bamarni/composer-bin-plugin": "^1.8.1",
"phpunit/phpunit": "^8.5.19 || ^9.5.8",
"uri-template/tests": "1.0.0"
},
"type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.0-dev"
- }
- },
"autoload": {
"psr-4": {
"GuzzleHttp\\UriTemplate\\": "src"
],
"support": {
"issues": "https://github.com/guzzle/uri-template/issues",
- "source": "https://github.com/guzzle/uri-template/tree/v1.0.1"
+ "source": "https://github.com/guzzle/uri-template/tree/v1.0.2"
},
"funding": [
{
"type": "tidelift"
}
],
- "time": "2021-10-07T12:57:01+00:00"
+ "time": "2023-08-27T10:19:19+00:00"
},
{
"name": "intervention/image",
},
{
"name": "knplabs/knp-snappy",
- "version": "v1.4.2",
+ "version": "v1.4.3",
"source": {
"type": "git",
"url": "https://github.com/KnpLabs/snappy.git",
- "reference": "b66f79334421c26d9c244427963fa2d92980b5d3"
+ "reference": "d3b742d61a68bf93866032c2c0a7f1486128b67e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/KnpLabs/snappy/zipball/b66f79334421c26d9c244427963fa2d92980b5d3",
- "reference": "b66f79334421c26d9c244427963fa2d92980b5d3",
+ "url": "https://api.github.com/repos/KnpLabs/snappy/zipball/d3b742d61a68bf93866032c2c0a7f1486128b67e",
+ "reference": "d3b742d61a68bf93866032c2c0a7f1486128b67e",
"shasum": ""
},
"require": {
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.16||^3.0",
"pedrotroller/php-cs-custom-fixer": "^2.19",
- "phpstan/phpstan": "^0.12.7",
- "phpstan/phpstan-phpunit": "^0.12.6",
+ "phpstan/phpstan": "^1.0.0",
+ "phpstan/phpstan-phpunit": "^1.0.0",
"phpunit/phpunit": "~7.4||~8.5"
},
"suggest": {
],
"support": {
"issues": "https://github.com/KnpLabs/snappy/issues",
- "source": "https://github.com/KnpLabs/snappy/tree/v1.4.2"
+ "source": "https://github.com/KnpLabs/snappy/tree/v1.4.3"
},
- "time": "2023-03-17T14:47:54+00:00"
+ "time": "2023-09-06T15:24:48+00:00"
},
{
"name": "laravel/framework",
},
{
"name": "laravel/socialite",
- "version": "v5.8.0",
+ "version": "v5.9.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/socialite.git",
- "reference": "50148edf24b6cd3e428aa9bc06a5d915b24376bb"
+ "reference": "14acfa3262875f180fba51efe3c7aaa089a9ef24"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/socialite/zipball/50148edf24b6cd3e428aa9bc06a5d915b24376bb",
- "reference": "50148edf24b6cd3e428aa9bc06a5d915b24376bb",
+ "url": "https://api.github.com/repos/laravel/socialite/zipball/14acfa3262875f180fba51efe3c7aaa089a9ef24",
+ "reference": "14acfa3262875f180fba51efe3c7aaa089a9ef24",
"shasum": ""
},
"require": {
"issues": "https://github.com/laravel/socialite/issues",
"source": "https://github.com/laravel/socialite"
},
- "time": "2023-07-14T14:22:58+00:00"
+ "time": "2023-09-05T15:20:21+00:00"
},
{
"name": "laravel/tinker",
- "version": "v2.8.1",
+ "version": "v2.8.2",
"source": {
"type": "git",
"url": "https://github.com/laravel/tinker.git",
- "reference": "04a2d3bd0d650c0764f70bf49d1ee39393e4eb10"
+ "reference": "b936d415b252b499e8c3b1f795cd4fc20f57e1f3"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/tinker/zipball/04a2d3bd0d650c0764f70bf49d1ee39393e4eb10",
- "reference": "04a2d3bd0d650c0764f70bf49d1ee39393e4eb10",
+ "url": "https://api.github.com/repos/laravel/tinker/zipball/b936d415b252b499e8c3b1f795cd4fc20f57e1f3",
+ "reference": "b936d415b252b499e8c3b1f795cd4fc20f57e1f3",
"shasum": ""
},
"require": {
},
"require-dev": {
"mockery/mockery": "~1.3.3|^1.4.2",
+ "phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^8.5.8|^9.3.3"
},
"suggest": {
],
"support": {
"issues": "https://github.com/laravel/tinker/issues",
- "source": "https://github.com/laravel/tinker/tree/v2.8.1"
+ "source": "https://github.com/laravel/tinker/tree/v2.8.2"
},
- "time": "2023-02-15T16:40:09+00:00"
+ "time": "2023-08-15T14:27:00+00:00"
},
{
"name": "league/commonmark",
- "version": "2.4.0",
+ "version": "2.4.1",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/commonmark.git",
- "reference": "d44a24690f16b8c1808bf13b1bd54ae4c63ea048"
+ "reference": "3669d6d5f7a47a93c08ddff335e6d945481a1dd5"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/d44a24690f16b8c1808bf13b1bd54ae4c63ea048",
- "reference": "d44a24690f16b8c1808bf13b1bd54ae4c63ea048",
+ "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/3669d6d5f7a47a93c08ddff335e6d945481a1dd5",
+ "reference": "3669d6d5f7a47a93c08ddff335e6d945481a1dd5",
"shasum": ""
},
"require": {
"type": "tidelift"
}
],
- "time": "2023-03-24T15:16:10+00:00"
+ "time": "2023-08-30T16:55:00+00:00"
},
{
"name": "league/config",
},
{
"name": "league/flysystem",
- "version": "3.15.1",
+ "version": "3.16.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/flysystem.git",
- "reference": "a141d430414fcb8bf797a18716b09f759a385bed"
+ "reference": "4fdf372ca6b63c6e281b1c01a624349ccb757729"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/a141d430414fcb8bf797a18716b09f759a385bed",
- "reference": "a141d430414fcb8bf797a18716b09f759a385bed",
+ "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/4fdf372ca6b63c6e281b1c01a624349ccb757729",
+ "reference": "4fdf372ca6b63c6e281b1c01a624349ccb757729",
"shasum": ""
},
"require": {
"php": "^8.0.2"
},
"conflict": {
+ "async-aws/core": "<1.19.0",
+ "async-aws/s3": "<1.14.0",
"aws/aws-sdk-php": "3.209.31 || 3.210.0",
"guzzlehttp/guzzle": "<7.0",
"guzzlehttp/ringphp": "<1.1.1",
"microsoft/azure-storage-blob": "^1.1",
"phpseclib/phpseclib": "^3.0.14",
"phpstan/phpstan": "^0.12.26",
- "phpunit/phpunit": "^9.5.11",
+ "phpunit/phpunit": "^9.5.11|^10.0",
"sabre/dav": "^4.3.1"
},
"type": "library",
],
"support": {
"issues": "https://github.com/thephpleague/flysystem/issues",
- "source": "https://github.com/thephpleague/flysystem/tree/3.15.1"
+ "source": "https://github.com/thephpleague/flysystem/tree/3.16.0"
},
"funding": [
{
"type": "github"
}
],
- "time": "2023-05-04T09:04:26+00:00"
+ "time": "2023-09-07T19:22:17+00:00"
},
{
"name": "league/flysystem-aws-s3-v3",
- "version": "3.15.0",
+ "version": "3.16.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git",
- "reference": "d8de61ee10b6a607e7996cff388c5a3a663e8c8a"
+ "reference": "ded9ba346bb01cb9cc4cc7f2743c2c0e14d18e1c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/d8de61ee10b6a607e7996cff388c5a3a663e8c8a",
- "reference": "d8de61ee10b6a607e7996cff388c5a3a663e8c8a",
+ "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/ded9ba346bb01cb9cc4cc7f2743c2c0e14d18e1c",
+ "reference": "ded9ba346bb01cb9cc4cc7f2743c2c0e14d18e1c",
"shasum": ""
},
"require": {
],
"support": {
"issues": "https://github.com/thephpleague/flysystem-aws-s3-v3/issues",
- "source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/3.15.0"
+ "source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/3.16.0"
},
"funding": [
{
"type": "github"
}
],
- "time": "2023-05-02T20:02:14+00:00"
+ "time": "2023-08-30T10:14:57+00:00"
},
{
"name": "league/flysystem-local",
- "version": "3.15.0",
+ "version": "3.16.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/flysystem-local.git",
- "reference": "543f64c397fefdf9cfeac443ffb6beff602796b3"
+ "reference": "ec7383f25642e6fd4bb0c9554fc2311245391781"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/543f64c397fefdf9cfeac443ffb6beff602796b3",
- "reference": "543f64c397fefdf9cfeac443ffb6beff602796b3",
+ "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/ec7383f25642e6fd4bb0c9554fc2311245391781",
+ "reference": "ec7383f25642e6fd4bb0c9554fc2311245391781",
"shasum": ""
},
"require": {
],
"support": {
"issues": "https://github.com/thephpleague/flysystem-local/issues",
- "source": "https://github.com/thephpleague/flysystem-local/tree/3.15.0"
+ "source": "https://github.com/thephpleague/flysystem-local/tree/3.16.0"
},
"funding": [
{
"type": "github"
}
],
- "time": "2023-05-02T20:02:14+00:00"
+ "time": "2023-08-30T10:23:59+00:00"
},
{
"name": "league/html-to-markdown",
},
{
"name": "mtdowling/jmespath.php",
- "version": "2.6.1",
+ "version": "2.7.0",
"source": {
"type": "git",
"url": "https://github.com/jmespath/jmespath.php.git",
- "reference": "9b87907a81b87bc76d19a7fb2d61e61486ee9edb"
+ "reference": "bbb69a935c2cbb0c03d7f481a238027430f6440b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/9b87907a81b87bc76d19a7fb2d61e61486ee9edb",
- "reference": "9b87907a81b87bc76d19a7fb2d61e61486ee9edb",
+ "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/bbb69a935c2cbb0c03d7f481a238027430f6440b",
+ "reference": "bbb69a935c2cbb0c03d7f481a238027430f6440b",
"shasum": ""
},
"require": {
- "php": "^5.4 || ^7.0 || ^8.0",
+ "php": "^7.2.5 || ^8.0",
"symfony/polyfill-mbstring": "^1.17"
},
"require-dev": {
- "composer/xdebug-handler": "^1.4 || ^2.0",
- "phpunit/phpunit": "^4.8.36 || ^7.5.15"
+ "composer/xdebug-handler": "^3.0.3",
+ "phpunit/phpunit": "^8.5.33"
},
"bin": [
"bin/jp.php"
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.6-dev"
+ "dev-master": "2.7-dev"
}
},
"autoload": {
"MIT"
],
"authors": [
+ {
+ "name": "Graham Campbell",
+ "homepage": "https://github.com/GrahamCampbell"
+ },
{
"name": "Michael Dowling",
],
"support": {
"issues": "https://github.com/jmespath/jmespath.php/issues",
- "source": "https://github.com/jmespath/jmespath.php/tree/2.6.1"
+ "source": "https://github.com/jmespath/jmespath.php/tree/2.7.0"
},
- "time": "2021-06-14T00:11:39+00:00"
+ "time": "2023-08-25T10:54:48+00:00"
},
{
"name": "nesbot/carbon",
- "version": "2.69.0",
+ "version": "2.70.0",
"source": {
"type": "git",
"url": "https://github.com/briannesbitt/Carbon.git",
- "reference": "4308217830e4ca445583a37d1bf4aff4153fa81c"
+ "reference": "d3298b38ea8612e5f77d38d1a99438e42f70341d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/4308217830e4ca445583a37d1bf4aff4153fa81c",
- "reference": "4308217830e4ca445583a37d1bf4aff4153fa81c",
+ "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/d3298b38ea8612e5f77d38d1a99438e42f70341d",
+ "reference": "d3298b38ea8612e5f77d38d1a99438e42f70341d",
"shasum": ""
},
"require": {
"type": "tidelift"
}
],
- "time": "2023-08-03T09:00:52+00:00"
+ "time": "2023-09-07T16:43:50+00:00"
},
{
"name": "nette/schema",
},
{
"name": "socialiteproviders/manager",
- "version": "v4.3.0",
+ "version": "v4.4.0",
"source": {
"type": "git",
"url": "https://github.com/SocialiteProviders/Manager.git",
- "reference": "47402cbc5b7ef445317e799bf12fd5a12062206c"
+ "reference": "df5e45b53d918ec3d689f014d98a6c838b98ed96"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/SocialiteProviders/Manager/zipball/47402cbc5b7ef445317e799bf12fd5a12062206c",
- "reference": "47402cbc5b7ef445317e799bf12fd5a12062206c",
+ "url": "https://api.github.com/repos/SocialiteProviders/Manager/zipball/df5e45b53d918ec3d689f014d98a6c838b98ed96",
+ "reference": "df5e45b53d918ec3d689f014d98a6c838b98ed96",
"shasum": ""
},
"require": {
"illuminate/support": "^6.0 || ^7.0 || ^8.0 || ^9.0 || ^10.0",
"laravel/socialite": "~5.0",
- "php": "^7.4 || ^8.0"
+ "php": "^8.0"
},
"require-dev": {
"mockery/mockery": "^1.2",
"issues": "https://github.com/socialiteproviders/manager/issues",
"source": "https://github.com/socialiteproviders/manager"
},
- "time": "2023-01-26T23:11:27+00:00"
+ "time": "2023-08-27T23:46:34+00:00"
},
{
"name": "socialiteproviders/microsoft-azure",
},
"time": "2022-09-06T03:39:26+00:00"
},
- {
- "name": "socialiteproviders/slack",
- "version": "4.1.1",
- "source": {
- "type": "git",
- "url": "https://github.com/SocialiteProviders/Slack.git",
- "reference": "2b781c95daf06ec87a8f3deba2ab613d6bea5e8d"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/SocialiteProviders/Slack/zipball/2b781c95daf06ec87a8f3deba2ab613d6bea5e8d",
- "reference": "2b781c95daf06ec87a8f3deba2ab613d6bea5e8d",
- "shasum": ""
- },
- "require": {
- "ext-json": "*",
- "php": "^7.2 || ^8.0",
- "socialiteproviders/manager": "~4.0"
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "SocialiteProviders\\Slack\\": ""
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Brian Faust",
- }
- ],
- "description": "Slack OAuth2 Provider for Laravel Socialite",
- "support": {
- "source": "https://github.com/SocialiteProviders/Slack/tree/4.1.1"
- },
- "abandoned": "laravel/socialite",
- "time": "2021-03-26T04:10:10+00:00"
- },
{
"name": "socialiteproviders/twitch",
"version": "5.3.1",
},
{
"name": "symfony/polyfill-ctype",
- "version": "v1.27.0",
+ "version": "v1.28.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
- "reference": "5bbc823adecdae860bb64756d639ecfec17b050a"
+ "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/5bbc823adecdae860bb64756d639ecfec17b050a",
- "reference": "5bbc823adecdae860bb64756d639ecfec17b050a",
+ "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb",
+ "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb",
"shasum": ""
},
"require": {
"type": "library",
"extra": {
"branch-alias": {
- "dev-main": "1.27-dev"
+ "dev-main": "1.28-dev"
},
"thanks": {
"name": "symfony/polyfill",
"portable"
],
"support": {
- "source": "https://github.com/symfony/polyfill-ctype/tree/v1.27.0"
+ "source": "https://github.com/symfony/polyfill-ctype/tree/v1.28.0"
},
"funding": [
{
"type": "tidelift"
}
],
- "time": "2022-11-03T14:55:06+00:00"
+ "time": "2023-01-26T09:26:14+00:00"
},
{
"name": "symfony/polyfill-intl-grapheme",
- "version": "v1.27.0",
+ "version": "v1.28.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-grapheme.git",
- "reference": "511a08c03c1960e08a883f4cffcacd219b758354"
+ "reference": "875e90aeea2777b6f135677f618529449334a612"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/511a08c03c1960e08a883f4cffcacd219b758354",
- "reference": "511a08c03c1960e08a883f4cffcacd219b758354",
+ "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/875e90aeea2777b6f135677f618529449334a612",
+ "reference": "875e90aeea2777b6f135677f618529449334a612",
"shasum": ""
},
"require": {
"type": "library",
"extra": {
"branch-alias": {
- "dev-main": "1.27-dev"
+ "dev-main": "1.28-dev"
},
"thanks": {
"name": "symfony/polyfill",
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.27.0"
+ "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.28.0"
},
"funding": [
{
"type": "tidelift"
}
],
- "time": "2022-11-03T14:55:06+00:00"
+ "time": "2023-01-26T09:26:14+00:00"
},
{
"name": "symfony/polyfill-intl-idn",
- "version": "v1.27.0",
+ "version": "v1.28.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-idn.git",
- "reference": "639084e360537a19f9ee352433b84ce831f3d2da"
+ "reference": "ecaafce9f77234a6a449d29e49267ba10499116d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/639084e360537a19f9ee352433b84ce831f3d2da",
- "reference": "639084e360537a19f9ee352433b84ce831f3d2da",
+ "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/ecaafce9f77234a6a449d29e49267ba10499116d",
+ "reference": "ecaafce9f77234a6a449d29e49267ba10499116d",
"shasum": ""
},
"require": {
"type": "library",
"extra": {
"branch-alias": {
- "dev-main": "1.27-dev"
+ "dev-main": "1.28-dev"
},
"thanks": {
"name": "symfony/polyfill",
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.27.0"
+ "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.28.0"
},
"funding": [
{
"type": "tidelift"
}
],
- "time": "2022-11-03T14:55:06+00:00"
+ "time": "2023-01-26T09:30:37+00:00"
},
{
"name": "symfony/polyfill-intl-normalizer",
- "version": "v1.27.0",
+ "version": "v1.28.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
- "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6"
+ "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/19bd1e4fcd5b91116f14d8533c57831ed00571b6",
- "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6",
+ "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92",
+ "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92",
"shasum": ""
},
"require": {
"type": "library",
"extra": {
"branch-alias": {
- "dev-main": "1.27-dev"
+ "dev-main": "1.28-dev"
},
"thanks": {
"name": "symfony/polyfill",
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.27.0"
+ "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.28.0"
},
"funding": [
{
"type": "tidelift"
}
],
- "time": "2022-11-03T14:55:06+00:00"
+ "time": "2023-01-26T09:26:14+00:00"
},
{
"name": "symfony/polyfill-mbstring",
- "version": "v1.27.0",
+ "version": "v1.28.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
- "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534"
+ "reference": "42292d99c55abe617799667f454222c54c60e229"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/8ad114f6b39e2c98a8b0e3bd907732c207c2b534",
- "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534",
+ "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229",
+ "reference": "42292d99c55abe617799667f454222c54c60e229",
"shasum": ""
},
"require": {
"type": "library",
"extra": {
"branch-alias": {
- "dev-main": "1.27-dev"
+ "dev-main": "1.28-dev"
},
"thanks": {
"name": "symfony/polyfill",
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.27.0"
+ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.28.0"
},
"funding": [
{
"type": "tidelift"
}
],
- "time": "2022-11-03T14:55:06+00:00"
+ "time": "2023-07-28T09:04:16+00:00"
},
{
"name": "symfony/polyfill-php72",
- "version": "v1.27.0",
+ "version": "v1.28.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php72.git",
- "reference": "869329b1e9894268a8a61dabb69153029b7a8c97"
+ "reference": "70f4aebd92afca2f865444d30a4d2151c13c3179"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/869329b1e9894268a8a61dabb69153029b7a8c97",
- "reference": "869329b1e9894268a8a61dabb69153029b7a8c97",
+ "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/70f4aebd92afca2f865444d30a4d2151c13c3179",
+ "reference": "70f4aebd92afca2f865444d30a4d2151c13c3179",
"shasum": ""
},
"require": {
"type": "library",
"extra": {
"branch-alias": {
- "dev-main": "1.27-dev"
+ "dev-main": "1.28-dev"
},
"thanks": {
"name": "symfony/polyfill",
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-php72/tree/v1.27.0"
+ "source": "https://github.com/symfony/polyfill-php72/tree/v1.28.0"
},
"funding": [
{
"type": "tidelift"
}
],
- "time": "2022-11-03T14:55:06+00:00"
+ "time": "2023-01-26T09:26:14+00:00"
},
{
"name": "symfony/polyfill-php80",
- "version": "v1.27.0",
+ "version": "v1.28.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
- "reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936"
+ "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936",
- "reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936",
+ "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/6caa57379c4aec19c0a12a38b59b26487dcfe4b5",
+ "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5",
"shasum": ""
},
"require": {
"type": "library",
"extra": {
"branch-alias": {
- "dev-main": "1.27-dev"
+ "dev-main": "1.28-dev"
},
"thanks": {
"name": "symfony/polyfill",
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-php80/tree/v1.27.0"
+ "source": "https://github.com/symfony/polyfill-php80/tree/v1.28.0"
},
"funding": [
{
"type": "tidelift"
}
],
- "time": "2022-11-03T14:55:06+00:00"
+ "time": "2023-01-26T09:26:14+00:00"
},
{
"name": "symfony/polyfill-php81",
- "version": "v1.27.0",
+ "version": "v1.28.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php81.git",
- "reference": "707403074c8ea6e2edaf8794b0157a0bfa52157a"
+ "reference": "7581cd600fa9fd681b797d00b02f068e2f13263b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/707403074c8ea6e2edaf8794b0157a0bfa52157a",
- "reference": "707403074c8ea6e2edaf8794b0157a0bfa52157a",
+ "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/7581cd600fa9fd681b797d00b02f068e2f13263b",
+ "reference": "7581cd600fa9fd681b797d00b02f068e2f13263b",
"shasum": ""
},
"require": {
"type": "library",
"extra": {
"branch-alias": {
- "dev-main": "1.27-dev"
+ "dev-main": "1.28-dev"
},
"thanks": {
"name": "symfony/polyfill",
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-php81/tree/v1.27.0"
+ "source": "https://github.com/symfony/polyfill-php81/tree/v1.28.0"
},
"funding": [
{
"type": "tidelift"
}
],
- "time": "2022-11-03T14:55:06+00:00"
+ "time": "2023-01-26T09:26:14+00:00"
},
{
"name": "symfony/polyfill-uuid",
- "version": "v1.27.0",
+ "version": "v1.28.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-uuid.git",
- "reference": "f3cf1a645c2734236ed1e2e671e273eeb3586166"
+ "reference": "9c44518a5aff8da565c8a55dbe85d2769e6f630e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/f3cf1a645c2734236ed1e2e671e273eeb3586166",
- "reference": "f3cf1a645c2734236ed1e2e671e273eeb3586166",
+ "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/9c44518a5aff8da565c8a55dbe85d2769e6f630e",
+ "reference": "9c44518a5aff8da565c8a55dbe85d2769e6f630e",
"shasum": ""
},
"require": {
"type": "library",
"extra": {
"branch-alias": {
- "dev-main": "1.27-dev"
+ "dev-main": "1.28-dev"
},
"thanks": {
"name": "symfony/polyfill",
"uuid"
],
"support": {
- "source": "https://github.com/symfony/polyfill-uuid/tree/v1.27.0"
+ "source": "https://github.com/symfony/polyfill-uuid/tree/v1.28.0"
},
"funding": [
{
"type": "tidelift"
}
],
- "time": "2022-11-03T14:55:06+00:00"
+ "time": "2023-01-26T09:26:14+00:00"
},
{
"name": "symfony/process",
},
{
"name": "phpstan/phpstan",
- "version": "1.10.29",
+ "version": "1.10.33",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
- "reference": "ee5d8f2d3977fb09e55603eee6fb53bdd76ee9c1"
+ "reference": "03b1cf9f814ba0863c4e9affea49a4d1ed9a2ed1"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpstan/phpstan/zipball/ee5d8f2d3977fb09e55603eee6fb53bdd76ee9c1",
- "reference": "ee5d8f2d3977fb09e55603eee6fb53bdd76ee9c1",
+ "url": "https://api.github.com/repos/phpstan/phpstan/zipball/03b1cf9f814ba0863c4e9affea49a4d1ed9a2ed1",
+ "reference": "03b1cf9f814ba0863c4e9affea49a4d1ed9a2ed1",
"shasum": ""
},
"require": {
"type": "tidelift"
}
],
- "time": "2023-08-14T13:24:11+00:00"
+ "time": "2023-09-04T12:20:53+00:00"
},
{
"name": "phpunit/php-code-coverage",
'sv' => 'Svenska',
'tr' => 'Türkçe',
'uk' => 'Українська',
+ 'uz' => 'O‘zbekcha',
'vi' => 'Tiếng Việt',
'zh_CN' => '简体中文',
'zh_TW' => '繁體中文',
[](https://discord.gg/ztkBqR2)
[](https://fosstodon.org/@bookstack)
[](https://twitter.com/bookstack_app)
+[](https://foss.video/c/bookstack)
[](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/.
## 🌎 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).
@endif
@if(count($activity) > 0)
- <div class="mb-xl">
+ <div id="recent-activity" class="mb-xl">
<h5>{{ trans('entities.recent_activity') }}</h5>
@include('common.activity-list', ['activity' => $activity])
</div>
@endphp
<form action="{{ url('/favourites/' . ($isFavourite ? 'remove' : 'add')) }}" method="POST">
{{ csrf_field() }}
- <input type="hidden" name="type" value="{{ get_class($entity) }}">
+ <input type="hidden" name="type" value="{{ $entity->getMorphClass() }}">
<input type="hidden" name="id" value="{{ $entity->id }}">
<button type="submit" data-shortcut="favourite" class="icon-list-item text-link">
<span>@icon($isFavourite ? 'star' : 'star-outline')</span>
<form action="{{ url('/watching/update') }}" method="POST">
{{ csrf_field() }}
{{ method_field('PUT') }}
- <input type="hidden" name="type" value="{{ get_class($entity) }}">
+ <input type="hidden" name="type" value="{{ $entity->getMorphClass() }}">
<input type="hidden" name="id" value="{{ $entity->id }}">
<button type="submit"
name="level"
<form action="{{ url('/watching/update') }}" method="POST">
{{ method_field('PUT') }}
{{ csrf_field() }}
- <input type="hidden" name="type" value="{{ get_class($entity) }}">
+ <input type="hidden" name="type" value="{{ $entity->getMorphClass() }}">
<input type="hidden" name="id" value="{{ $entity->id }}">
<ul refs="dropdown@menu" class="dropdown-menu xl-limited anchor-left pb-none">
</div>
<div>
- <div id="recent-activity">
- <div class="card mb-xl">
- <h3 class="card-title">{{ trans('entities.recent_activity') }}</h3>
- @include('common.activity-list', ['activity' => $activity])
- </div>
+ <div id="recent-activity" class="card mb-xl">
+ <h3 class="card-title">{{ trans('entities.recent_activity') }}</h3>
+ @include('common.activity-list', ['activity' => $activity])
</div>
</div>
</div>
@yield('bottom')
- <script src="{{ versioned_asset('dist/app.js') }}" nonce="{{ $cspNonce }}"></script>
+ @if($cspNonce ?? false)
+ <script src="{{ versioned_asset('dist/app.js') }}" nonce="{{ $cspNonce }}"></script>
+ @endif
@yield('scripts')
@include('layouts.parts.base-body-end')
</div>
@if(count($activity) > 0)
- <div class="mb-xl">
+ <div id="recent-activity" class="mb-xl">
<h5>{{ trans('entities.recent_activity') }}</h5>
@include('common.activity-list', ['activity' => $activity])
</div>
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
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());
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);
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']);
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);
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)
$this->actingAs($editor)->get($book->getUrl());
$resp = $this->put('/watching/update', [
- 'type' => get_class($book),
+ 'type' => $book->getMorphClass(),
'id' => $book->id,
'level' => 'comments'
]);
]);
$resp = $this->put('/watching/update', [
- 'type' => get_class($book),
+ 'type' => $book->getMorphClass(),
'id' => $book->id,
'level' => 'default'
]);
$book = $this->entities->book();
$resp = $this->put('/watching/update', [
- 'type' => get_class($book),
+ 'type' => $book->getMorphClass(),
'id' => $book->id,
'level' => 'comments'
]);
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;
$resp->assertStatus(200);
/** @var User $user */
- Notification::assertSentTo($user, UserInvite::class);
+ Notification::assertSentTo($user, UserInviteNotification::class);
}
public function test_create_name_and_email_validation()
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;
'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,
$this->post('/oidc/login');
$state = session()->get('oidc_state');
- $transactions = &$this->mockHttpClient([$this->getMockAuthorizationResponse([
+ $transactions = $this->mockHttpClient([$this->getMockAuthorizationResponse([
'sub' => 'benny1010101',
])]);
// 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]);
{
$this->withAutodiscovery();
- $transactions = &$this->mockHttpClient([
+ $transactions = $this->mockHttpClient([
$this->getAutoDiscoveryResponse(),
$this->getJwksResponse(),
]);
$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());
{
$this->withAutodiscovery();
- $transactions = &$this->mockHttpClient([
+ $transactions = $this->mockHttpClient([
$this->getAutoDiscoveryResponse(),
$this->getJwksResponse(),
$this->getAutoDiscoveryResponse([
// 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()
$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([
+ 'sub' => 'benny1010101',
+ 'first_name' => 'Benny',
+ 'last_name' => 'Jenkins'
+ ]);
+
+ $this->assertDatabaseHas('users', [
+ 'name' => 'Benny Jenkins',
+ ]);
+ }
+
public function test_login_group_sync()
{
config()->set([
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;
// 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]);
// 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;
});
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;
/** @var User $user */
- 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()
$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.');
}
}
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;
$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,
]);
$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);
$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);
$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]);
$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]);
+ }
}
$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…');
+ }
}
$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());
$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());
$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());
namespace Tests\Settings;
-use BookStack\Notifications\TestEmail;
+use BookStack\Settings\TestEmailNotification;
use Illuminate\Contracts\Notifications\Dispatcher;
use Illuminate\Support\Facades\Notification;
use Tests\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()
$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);
}
}
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;
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;
}
/**
- * 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);
}
/**
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
});
}
+ public function test_theme_functions_loads_errors_are_caught_and_logged()
+ {
+ $this->usingThemeFolder(function ($themeFolder) {
+ $functionsFile = theme_path('functions.php');
+ file_put_contents($functionsFile, "<?php\n\\BookStack\\Biscuits::eat();");
+
+ $this->expectException(ThemeException::class);
+ $this->expectExceptionMessageMatches('/Failed loading theme functions file at ".*?" with error: Class "BookStack\\\\Biscuits" not found/');
+
+ $this->runWithEnv('APP_THEME', $themeFolder, fn() => null);
+ });
+ }
+
public function test_event_commonmark_environment_configure()
{
$callbackCalled = false;
};
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();
$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()
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
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', [
'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()
$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()
]);
$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();
$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());
}
}
$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();