mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-08-07 23:03:00 +03:00
Merge branch 'BookStackApp:development' into add-priority
This commit is contained in:
@@ -42,6 +42,7 @@ class CommentController extends Controller
|
||||
$comment = $this->commentRepo->create($page, $request->get('text'), $request->get('parent_id'));
|
||||
|
||||
return view('comments.comment-branch', [
|
||||
'readOnly' => false,
|
||||
'branch' => [
|
||||
'comment' => $comment,
|
||||
'children' => [],
|
||||
@@ -66,7 +67,7 @@ class CommentController extends Controller
|
||||
|
||||
$comment = $this->commentRepo->update($comment, $request->get('text'));
|
||||
|
||||
return view('comments.comment', ['comment' => $comment]);
|
||||
return view('comments.comment', ['comment' => $comment, 'readOnly' => false]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -19,6 +19,8 @@ use Illuminate\Support\Str;
|
||||
* @property string $entity_type
|
||||
* @property int $entity_id
|
||||
* @property int $user_id
|
||||
* @property Carbon $created_at
|
||||
* @property Carbon $updated_at
|
||||
*/
|
||||
class Activity extends Model
|
||||
{
|
||||
|
@@ -16,8 +16,8 @@ use ReflectionMethod;
|
||||
|
||||
class ApiDocsGenerator
|
||||
{
|
||||
protected $reflectionClasses = [];
|
||||
protected $controllerClasses = [];
|
||||
protected array $reflectionClasses = [];
|
||||
protected array $controllerClasses = [];
|
||||
|
||||
/**
|
||||
* Load the docs form the cache if existing
|
||||
@@ -139,9 +139,10 @@ class ApiDocsGenerator
|
||||
protected function parseDescriptionFromMethodComment(string $comment): string
|
||||
{
|
||||
$matches = [];
|
||||
preg_match_all('/^\s*?\*\s((?![@\s]).*?)$/m', $comment, $matches);
|
||||
preg_match_all('/^\s*?\*\s?($|((?![\/@\s]).*?))$/m', $comment, $matches);
|
||||
|
||||
return implode(' ', $matches[1] ?? []);
|
||||
$text = implode(' ', $matches[1] ?? []);
|
||||
return str_replace(' ', "\n", $text);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -8,6 +8,10 @@
|
||||
* Do not edit this file unless you're happy to maintain any changes yourself.
|
||||
*/
|
||||
|
||||
// Configured mail encryption method.
|
||||
// STARTTLS should still be attempted, but tls/ssl forces TLS usage.
|
||||
$mailEncryption = env('MAIL_ENCRYPTION', null);
|
||||
|
||||
return [
|
||||
|
||||
// Mail driver to use.
|
||||
@@ -27,14 +31,15 @@ return [
|
||||
'mailers' => [
|
||||
'smtp' => [
|
||||
'transport' => 'smtp',
|
||||
'scheme' => 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),
|
||||
'timeout' => null,
|
||||
'local_domain' => env('MAIL_EHLO_DOMAIN'),
|
||||
'tls_required' => ($mailEncryption === 'tls' || $mailEncryption === 'ssl'),
|
||||
],
|
||||
|
||||
'sendmail' => [
|
||||
|
@@ -30,7 +30,7 @@ class BookshelfController extends Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a listing of the book.
|
||||
* Display a listing of bookshelves.
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
@@ -111,8 +111,9 @@ class BookshelfController extends Controller
|
||||
]);
|
||||
|
||||
$sort = $listOptions->getSort();
|
||||
$sortedVisibleShelfBooks = $shelf->visibleBooks()->get()
|
||||
->sortBy($sort === 'default' ? 'pivot.order' : $sort, SORT_REGULAR, $listOptions->getOrder() === 'desc')
|
||||
$sortedVisibleShelfBooks = $shelf->visibleBooks()
|
||||
->reorder($sort === 'default' ? 'order' : $sort, $listOptions->getOrder())
|
||||
->get()
|
||||
->values()
|
||||
->all();
|
||||
|
||||
|
@@ -13,8 +13,6 @@ use Illuminate\Http\Request;
|
||||
|
||||
class PageApiController extends ApiController
|
||||
{
|
||||
protected PageRepo $pageRepo;
|
||||
|
||||
protected $rules = [
|
||||
'create' => [
|
||||
'book_id' => ['required_without:chapter_id', 'integer'],
|
||||
@@ -36,9 +34,9 @@ class PageApiController extends ApiController
|
||||
],
|
||||
];
|
||||
|
||||
public function __construct(PageRepo $pageRepo)
|
||||
{
|
||||
$this->pageRepo = $pageRepo;
|
||||
public function __construct(
|
||||
protected PageRepo $pageRepo
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -86,10 +84,14 @@ class PageApiController extends ApiController
|
||||
|
||||
/**
|
||||
* View the details of a single page.
|
||||
*
|
||||
* Pages will always have HTML content. They may have markdown content
|
||||
* if the markdown editor was used to last update the page.
|
||||
*
|
||||
* The 'html' property is the fully rendered & escaped HTML content that BookStack
|
||||
* would show on page view, with page includes handled.
|
||||
* The 'raw_html' property is the direct database stored HTML content, which would be
|
||||
* what BookStack shows on page edit.
|
||||
*
|
||||
* See the "Content Security" section of these docs for security considerations when using
|
||||
* the page content returned from this endpoint.
|
||||
*/
|
||||
|
@@ -24,16 +24,10 @@ use Throwable;
|
||||
|
||||
class PageController extends Controller
|
||||
{
|
||||
protected PageRepo $pageRepo;
|
||||
protected ReferenceFetcher $referenceFetcher;
|
||||
|
||||
/**
|
||||
* PageController constructor.
|
||||
*/
|
||||
public function __construct(PageRepo $pageRepo, ReferenceFetcher $referenceFetcher)
|
||||
{
|
||||
$this->pageRepo = $pageRepo;
|
||||
$this->referenceFetcher = $referenceFetcher;
|
||||
public function __construct(
|
||||
protected PageRepo $pageRepo,
|
||||
protected ReferenceFetcher $referenceFetcher
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -139,6 +139,7 @@ class Page extends BookChild
|
||||
{
|
||||
$refreshed = $this->refresh()->unsetRelations()->load(['tags', 'createdBy', 'updatedBy', 'ownedBy']);
|
||||
$refreshed->setHidden(array_diff($refreshed->getHidden(), ['html', 'markdown']));
|
||||
$refreshed->setAttribute('raw_html', $refreshed->html);
|
||||
$refreshed->html = (new PageContent($refreshed))->render();
|
||||
|
||||
return $refreshed;
|
||||
|
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace BookStack\Entities\Tools;
|
||||
|
||||
use BookStack\Activity\Tools\CommentTree;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Entities\Repos\PageRepo;
|
||||
use BookStack\Entities\Tools\Markdown\HtmlToMarkdown;
|
||||
@@ -9,19 +10,14 @@ use BookStack\Entities\Tools\Markdown\MarkdownToHtml;
|
||||
|
||||
class PageEditorData
|
||||
{
|
||||
protected Page $page;
|
||||
protected PageRepo $pageRepo;
|
||||
protected string $requestedEditor;
|
||||
|
||||
protected array $viewData;
|
||||
protected array $warnings;
|
||||
|
||||
public function __construct(Page $page, PageRepo $pageRepo, string $requestedEditor)
|
||||
{
|
||||
$this->page = $page;
|
||||
$this->pageRepo = $pageRepo;
|
||||
$this->requestedEditor = $requestedEditor;
|
||||
|
||||
public function __construct(
|
||||
protected Page $page,
|
||||
protected PageRepo $pageRepo,
|
||||
protected string $requestedEditor
|
||||
) {
|
||||
$this->viewData = $this->build();
|
||||
}
|
||||
|
||||
@@ -69,6 +65,7 @@ class PageEditorData
|
||||
'draftsEnabled' => $draftsEnabled,
|
||||
'templates' => $templates,
|
||||
'editor' => $editorType,
|
||||
'comments' => new CommentTree($page),
|
||||
];
|
||||
}
|
||||
|
||||
|
@@ -55,9 +55,9 @@ class PermissionsUpdater
|
||||
}
|
||||
|
||||
if (isset($data['fallback_permissions']['inheriting']) && $data['fallback_permissions']['inheriting'] !== true) {
|
||||
$data = $data['fallback_permissions'];
|
||||
$data['role_id'] = 0;
|
||||
$rolePermissionData = $this->formatPermissionsFromApiRequestToEntityPermissions([$data], true);
|
||||
$fallbackData = $data['fallback_permissions'];
|
||||
$fallbackData['role_id'] = 0;
|
||||
$rolePermissionData = $this->formatPermissionsFromApiRequestToEntityPermissions([$fallbackData], true);
|
||||
$entity->permissions()->createMany($rolePermissionData);
|
||||
}
|
||||
|
||||
|
@@ -2,6 +2,25 @@
|
||||
|
||||
namespace BookStack\Exceptions;
|
||||
|
||||
class ApiAuthException extends UnauthorizedException
|
||||
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
|
||||
|
||||
class ApiAuthException extends \Exception implements HttpExceptionInterface
|
||||
{
|
||||
protected int $status;
|
||||
|
||||
public function __construct(string $message, int $statusCode = 401)
|
||||
{
|
||||
$this->status = $statusCode;
|
||||
parent::__construct($message, $statusCode);
|
||||
}
|
||||
|
||||
public function getStatusCode(): int
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
public function getHeaders(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
@@ -9,7 +9,7 @@ use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
|
||||
use Throwable;
|
||||
|
||||
class Handler extends ExceptionHandler
|
||||
@@ -82,7 +82,7 @@ class Handler extends ExceptionHandler
|
||||
$code = 500;
|
||||
$headers = [];
|
||||
|
||||
if ($e instanceof HttpException) {
|
||||
if ($e instanceof HttpExceptionInterface) {
|
||||
$code = $e->getStatusCode();
|
||||
$headers = $e->getHeaders();
|
||||
}
|
||||
@@ -103,10 +103,6 @@ class Handler extends ExceptionHandler
|
||||
$code = $e->status;
|
||||
}
|
||||
|
||||
if (method_exists($e, 'getStatus')) {
|
||||
$code = $e->getStatus();
|
||||
}
|
||||
|
||||
$responseData['error']['code'] = $code;
|
||||
|
||||
return new JsonResponse($responseData, $code, $headers);
|
||||
|
@@ -4,8 +4,9 @@ namespace BookStack\Exceptions;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Contracts\Support\Responsable;
|
||||
|
||||
class JsonDebugException extends Exception
|
||||
class JsonDebugException extends Exception implements Responsable
|
||||
{
|
||||
protected array $data;
|
||||
|
||||
@@ -22,7 +23,7 @@ class JsonDebugException extends Exception
|
||||
* Convert this exception into a response.
|
||||
* We add a manual data conversion to UTF8 to ensure any binary data is presentable as a JSON string.
|
||||
*/
|
||||
public function render(): JsonResponse
|
||||
public function toResponse($request): JsonResponse
|
||||
{
|
||||
$cleaned = mb_convert_encoding($this->data, 'UTF-8');
|
||||
|
||||
|
@@ -4,29 +4,39 @@ namespace BookStack\Exceptions;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Contracts\Support\Responsable;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
|
||||
|
||||
class NotifyException extends Exception implements Responsable
|
||||
class NotifyException extends Exception implements Responsable, HttpExceptionInterface
|
||||
{
|
||||
public $message;
|
||||
public $redirectLocation;
|
||||
protected $status;
|
||||
public string $redirectLocation;
|
||||
protected int $status;
|
||||
|
||||
public function __construct(string $message, string $redirectLocation = '/', int $status = 500)
|
||||
{
|
||||
$this->message = $message;
|
||||
$this->redirectLocation = $redirectLocation;
|
||||
$this->status = $status;
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the desired status code for this exception.
|
||||
* Get the desired HTTP status code for this exception.
|
||||
*/
|
||||
public function getStatus(): int
|
||||
public function getStatusCode(): int
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the desired HTTP headers for this exception.
|
||||
*/
|
||||
public function getHeaders(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the response for this type of exception.
|
||||
*
|
||||
@@ -38,7 +48,7 @@ class NotifyException extends Exception implements Responsable
|
||||
|
||||
// Front-end JSON handling. API-side handling managed via handler.
|
||||
if ($request->wantsJson()) {
|
||||
return response()->json(['error' => $message], 403);
|
||||
return response()->json(['error' => $message], $this->getStatusCode());
|
||||
}
|
||||
|
||||
if (!empty($message)) {
|
||||
|
@@ -4,18 +4,12 @@ namespace BookStack\Exceptions;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Contracts\Support\Responsable;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
|
||||
|
||||
class PrettyException extends Exception implements Responsable
|
||||
class PrettyException extends Exception implements Responsable, HttpExceptionInterface
|
||||
{
|
||||
/**
|
||||
* @var ?string
|
||||
*/
|
||||
protected $subtitle = null;
|
||||
|
||||
/**
|
||||
* @var ?string
|
||||
*/
|
||||
protected $details = null;
|
||||
protected ?string $subtitle = null;
|
||||
protected ?string $details = null;
|
||||
|
||||
/**
|
||||
* Render a response for when this exception occurs.
|
||||
@@ -24,7 +18,7 @@ class PrettyException extends Exception implements Responsable
|
||||
*/
|
||||
public function toResponse($request)
|
||||
{
|
||||
$code = ($this->getCode() === 0) ? 500 : $this->getCode();
|
||||
$code = $this->getStatusCode();
|
||||
|
||||
return response()->view('errors.' . $code, [
|
||||
'message' => $this->getMessage(),
|
||||
@@ -46,4 +40,20 @@ class PrettyException extends Exception implements Responsable
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the desired HTTP status code for this exception.
|
||||
*/
|
||||
public function getStatusCode(): int
|
||||
{
|
||||
return ($this->getCode() === 0) ? 500 : $this->getCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the desired HTTP headers for this exception.
|
||||
*/
|
||||
public function getHeaders(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
@@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class UnauthorizedException extends Exception
|
||||
{
|
||||
/**
|
||||
* ApiAuthException constructor.
|
||||
*/
|
||||
public function __construct($message, $code = 401)
|
||||
{
|
||||
parent::__construct($message, $code);
|
||||
}
|
||||
}
|
@@ -3,7 +3,6 @@
|
||||
namespace BookStack\Http\Middleware;
|
||||
|
||||
use BookStack\Exceptions\ApiAuthException;
|
||||
use BookStack\Exceptions\UnauthorizedException;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
@@ -11,15 +10,13 @@ class ApiAuthenticate
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @throws ApiAuthException
|
||||
*/
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
// Validate the token and it's users API access
|
||||
try {
|
||||
$this->ensureAuthorizedBySessionOrToken();
|
||||
} catch (UnauthorizedException $exception) {
|
||||
return $this->unauthorisedResponse($exception->getMessage(), $exception->getCode());
|
||||
}
|
||||
$this->ensureAuthorizedBySessionOrToken();
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
@@ -28,7 +25,7 @@ class ApiAuthenticate
|
||||
* Ensure the current user can access authenticated API routes, either via existing session
|
||||
* authentication or via API Token authentication.
|
||||
*
|
||||
* @throws UnauthorizedException
|
||||
* @throws ApiAuthException
|
||||
*/
|
||||
protected function ensureAuthorizedBySessionOrToken(): void
|
||||
{
|
||||
@@ -58,17 +55,4 @@ class ApiAuthenticate
|
||||
|
||||
return $hasApiPermission && hasAppAccess();
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a standard API unauthorised response.
|
||||
*/
|
||||
protected function unauthorisedResponse(string $message, int $code)
|
||||
{
|
||||
return response()->json([
|
||||
'error' => [
|
||||
'code' => $code,
|
||||
'message' => $message,
|
||||
],
|
||||
], $code);
|
||||
}
|
||||
}
|
||||
|
@@ -38,8 +38,10 @@ class ContentPermissionApiController extends ApiController
|
||||
|
||||
/**
|
||||
* Read the configured content-level permissions for the item of the given type and ID.
|
||||
*
|
||||
* 'contentType' should be one of: page, book, chapter, bookshelf.
|
||||
* 'contentId' should be the relevant ID of that item type you'd like to handle permissions for.
|
||||
*
|
||||
* The permissions shown are those that override the default for just the specified item, they do not show the
|
||||
* full evaluated permission for a role, nor do they reflect permissions inherited from other items in the hierarchy.
|
||||
* Fallback permission values may be `null` when inheriting is active.
|
||||
@@ -57,6 +59,7 @@ class ContentPermissionApiController extends ApiController
|
||||
/**
|
||||
* Update the configured content-level permission overrides for the item of the given type and ID.
|
||||
* 'contentType' should be one of: page, book, chapter, bookshelf.
|
||||
*
|
||||
* 'contentId' should be the relevant ID of that item type you'd like to handle permissions for.
|
||||
* Providing an empty `role_permissions` array will remove any existing configured role permissions,
|
||||
* so you may want to fetch existing permissions beforehand if just adding/removing a single item.
|
||||
|
@@ -52,8 +52,10 @@ class ImageGalleryApiController extends ApiController
|
||||
|
||||
/**
|
||||
* Create a new image in the system.
|
||||
*
|
||||
* Since "image" is expected to be a file, this needs to be a 'multipart/form-data' type request.
|
||||
* The provided "uploaded_to" should be an existing page ID in the system.
|
||||
*
|
||||
* If the "name" parameter is omitted, the filename of the provided image file will be used instead.
|
||||
* The "type" parameter should be 'gallery' for page content images, and 'drawio' should only be used
|
||||
* when the file is a PNG file with diagrams.net image data embedded within.
|
||||
|
@@ -29,7 +29,8 @@ class HttpFetcher
|
||||
curl_close($ch);
|
||||
|
||||
if ($err) {
|
||||
throw new HttpFetchException($err);
|
||||
$errno = curl_errno($ch);
|
||||
throw new HttpFetchException($err, $errno);
|
||||
}
|
||||
|
||||
return $data;
|
||||
|
@@ -177,6 +177,7 @@ class ImageRepo
|
||||
|
||||
$image->refresh();
|
||||
$image->updated_by = user()->id;
|
||||
$image->touch();
|
||||
$image->save();
|
||||
$this->imageService->replaceExistingFromUpload($image->path, $image->type, $file);
|
||||
$this->loadThumbs($image, true);
|
||||
|
@@ -34,7 +34,7 @@ class UserAvatars
|
||||
$user->avatar()->associate($avatar);
|
||||
$user->save();
|
||||
} catch (Exception $e) {
|
||||
Log::error('Failed to save user avatar image');
|
||||
Log::error('Failed to save user avatar image', ['exception' => $e]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ class UserAvatars
|
||||
$user->avatar()->associate($avatar);
|
||||
$user->save();
|
||||
} catch (Exception $e) {
|
||||
Log::error('Failed to save user avatar image');
|
||||
Log::error('Failed to save user avatar image', ['exception' => $e]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,14 +107,14 @@ class UserAvatars
|
||||
/**
|
||||
* Gets an image from url and returns it as a string of image data.
|
||||
*
|
||||
* @throws Exception
|
||||
* @throws HttpFetchException
|
||||
*/
|
||||
protected function getAvatarImageData(string $url): string
|
||||
{
|
||||
try {
|
||||
$imageData = $this->http->fetch($url);
|
||||
} catch (HttpFetchException $exception) {
|
||||
throw new Exception(trans('errors.cannot_get_image_from_url', ['url' => $url]));
|
||||
throw new HttpFetchException(trans('errors.cannot_get_image_from_url', ['url' => $url]), $exception->getCode(), $exception);
|
||||
}
|
||||
|
||||
return $imageData;
|
||||
|
@@ -73,7 +73,7 @@ class UserApiController extends ApiController
|
||||
*/
|
||||
public function list()
|
||||
{
|
||||
$users = User::query()->select(['*'])
|
||||
$users = User::query()->select(['users.*'])
|
||||
->scopes('withLastActivityAt')
|
||||
->with(['avatar']);
|
||||
|
||||
|
@@ -15,7 +15,7 @@ class RolesAllPaginatedAndSorted
|
||||
{
|
||||
$sort = $listOptions->getSort();
|
||||
if ($sort === 'created_at') {
|
||||
$sort = 'users.created_at';
|
||||
$sort = 'roles.created_at';
|
||||
}
|
||||
|
||||
$query = Role::query()->select(['*'])
|
||||
|
Reference in New Issue
Block a user