1
0
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:
Jean-René Rouet
2023-07-11 08:57:14 +02:00
committed by GitHub
378 changed files with 4422 additions and 1890 deletions

View File

@@ -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]);
}
/**

View File

@@ -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
{

View File

@@ -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);
}
/**

View File

@@ -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' => [

View File

@@ -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();

View File

@@ -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.
*/

View File

@@ -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
) {
}
/**

View File

@@ -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;

View File

@@ -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),
];
}

View File

@@ -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);
}

View File

@@ -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 [];
}
}

View File

@@ -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);

View File

@@ -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');

View File

@@ -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)) {

View File

@@ -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 [];
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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.

View File

@@ -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.

View File

@@ -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;

View File

@@ -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);

View File

@@ -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;

View File

@@ -73,7 +73,7 @@ class UserApiController extends ApiController
*/
public function list()
{
$users = User::query()->select(['*'])
$users = User::query()->select(['users.*'])
->scopes('withLastActivityAt')
->with(['avatar']);

View File

@@ -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(['*'])