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