mirror of
https://github.com/BookStackApp/BookStack.git
synced 2026-02-06 00:59:39 +03:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf0ba9f756 | ||
|
|
05f8034439 | ||
|
|
1d1186c901 | ||
|
|
641a26cdf7 | ||
|
|
5fd8e7e0e9 | ||
|
|
d926ca5f71 | ||
|
|
b69722c3b5 | ||
|
|
c9aa1c979f |
@@ -143,10 +143,6 @@ STORAGE_URL=false
|
||||
# Can be 'standard', 'ldap', 'saml2' or 'oidc'
|
||||
AUTH_METHOD=standard
|
||||
|
||||
# Automatically initiate login via external auth system if it's the only auth method.
|
||||
# Works with saml2 or oidc auth methods.
|
||||
AUTH_AUTO_INITIATE=false
|
||||
|
||||
# Social authentication configuration
|
||||
# All disabled by default.
|
||||
# Refer to https://www.bookstackapp.com/docs/admin/third-party-auth/
|
||||
@@ -357,11 +353,3 @@ API_REQUESTS_PER_MIN=180
|
||||
# user identifier (Username or email).
|
||||
LOG_FAILED_LOGIN_MESSAGE=false
|
||||
LOG_FAILED_LOGIN_CHANNEL=errorlog_plain_webserver
|
||||
|
||||
# Alter the precision of IP addresses stored by BookStack.
|
||||
# Should be a number between 0 and 4, where 4 retains the full IP address
|
||||
# and 0 completely hides the IP address. As an example, a value of 2 for the
|
||||
# IP address '146.191.42.4' would result in '146.191.x.x' being logged.
|
||||
# For the IPv6 address '2001:db8:85a3:8d3:1319:8a2e:370:7348' this would result as:
|
||||
# '2001:db8:85a3:8d3:x:x:x:x'
|
||||
IP_ADDRESS_PRECISION=4
|
||||
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@@ -1,4 +1,3 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [ssddanbrown]
|
||||
ko_fi: ssddanbrown
|
||||
4
.github/ISSUE_TEMPLATE/language_request.yml
vendored
4
.github/ISSUE_TEMPLATE/language_request.yml
vendored
@@ -1,5 +1,5 @@
|
||||
name: Language Request
|
||||
description: Request a new language to be added to Crowdin for you to translate
|
||||
description: Request a new language to be added to CrowdIn for you to translate
|
||||
labels: [":earth_africa: Translations"]
|
||||
assignees:
|
||||
- ssddanbrown
|
||||
@@ -23,7 +23,7 @@ body:
|
||||
This issue template is to request a new language be added to our [Crowdin translation management project](https://crowdin.com/project/bookstack).
|
||||
Please don't use this template to request a new language that you are not prepared to provide translations for.
|
||||
options:
|
||||
- label: I confirm I'm offering to help translate for this new language via Crowdin.
|
||||
- label: I confirm I'm offering to help translate for this new language via CrowdIn.
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
||||
29
.github/translators.txt
vendored
29
.github/translators.txt
vendored
@@ -55,8 +55,6 @@ Name :: Languages
|
||||
@Baptistou :: French
|
||||
@arcoai :: Spanish
|
||||
@Jokuna :: Korean
|
||||
@smartshogu :: German; German Informal
|
||||
@samadha56 :: Persian
|
||||
cipi1965 :: Italian
|
||||
Mykola Ronik (Mantikor) :: Ukrainian
|
||||
furkanoyk :: Turkish
|
||||
@@ -176,7 +174,7 @@ Alexander Predl (Harveyhase68) :: German
|
||||
Rem (Rem9000) :: Dutch
|
||||
Michał Stelmach (stelmach-web) :: Polish
|
||||
arniom :: French
|
||||
REMOVED_USER :: Dutch; Turkish
|
||||
REMOVED_USER :: Turkish
|
||||
林祖年 (contagion) :: Chinese Traditional
|
||||
Siamak Guodarzi (siamakgoudarzi88) :: Persian
|
||||
Lis Maestrelo (lismtrl) :: Portuguese, Brazilian
|
||||
@@ -245,28 +243,3 @@ Shukrullo (vodiylik) :: Uzbek
|
||||
William W. (Nevnt) :: Chinese Traditional
|
||||
eamaro :: Portuguese
|
||||
Ypsilon-dev :: Arabic
|
||||
Hieu Vuong Trung (vuongtrunghieu) :: Vietnamese
|
||||
David Clubb (davidoclubb) :: Welsh
|
||||
welles freire (wellesximenes) :: Portuguese, Brazilian
|
||||
Magnus Jensen (MagnusHJensen) :: Danish
|
||||
Hesley Magno (hesleymagno) :: Portuguese, Brazilian
|
||||
Éric Gaspar (erga) :: French
|
||||
Fr3shlama :: German
|
||||
DSR :: Spanish, Argentina
|
||||
Andrii Bodnar (andrii-bodnar) :: Ukrainian
|
||||
Younes el Anjri (younesea28) :: Dutch
|
||||
Guclu Ozturk (gucluoz) :: Turkish
|
||||
Atmis :: French
|
||||
redjack666 :: Chinese Traditional
|
||||
Ashita007 :: Russian
|
||||
lihaorr :: Chinese Simplified
|
||||
Marcus Silber (marcus.silber82) :: German
|
||||
PellNet :: Croatian
|
||||
Winetradr :: German
|
||||
Sebastian Klaus (sebklaus) :: German
|
||||
Filip Antala (AntalaFilip) :: Slovak
|
||||
mcgong (GongMingCai) :: Chinese Simplified; Chinese Traditional
|
||||
Nanang Setia Budi (sefidananang) :: Indonesian
|
||||
Андрей Павлов (andrei.pavlov) :: Russian
|
||||
Alex Navarro (alex.n.navarro) :: Portuguese, Brazilian
|
||||
Ji-Hyeon Gim (PotatoGim) :: Korean
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -5,10 +5,10 @@ Homestead.yaml
|
||||
.idea
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
/public/dist/*.map
|
||||
/public/dist
|
||||
/public/plugins
|
||||
/public/css/*.map
|
||||
/public/js/*.map
|
||||
/public/css
|
||||
/public/js
|
||||
/public/bower
|
||||
/public/build/
|
||||
/storage/images
|
||||
|
||||
@@ -2,15 +2,21 @@
|
||||
|
||||
namespace BookStack\Actions;
|
||||
|
||||
use BookStack\Auth\Permissions\PermissionService;
|
||||
use BookStack\Entities\Models\Entity;
|
||||
use BookStack\Facades\Theme;
|
||||
use BookStack\Interfaces\Loggable;
|
||||
use BookStack\Theming\ThemeEvents;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class ActivityLogger
|
||||
{
|
||||
protected $permissionService;
|
||||
|
||||
public function __construct(PermissionService $permissionService)
|
||||
{
|
||||
$this->permissionService = $permissionService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a generic activity event to the database.
|
||||
*
|
||||
@@ -29,10 +35,8 @@ class ActivityLogger
|
||||
}
|
||||
|
||||
$activity->save();
|
||||
|
||||
$this->setNotification($type);
|
||||
$this->dispatchWebhooks($type, $detail);
|
||||
Theme::dispatch(ThemeEvents::ACTIVITY_LOGGED, $type, $detail);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -40,10 +44,12 @@ class ActivityLogger
|
||||
*/
|
||||
protected function newActivityForUser(string $type): Activity
|
||||
{
|
||||
$ip = request()->ip() ?? '';
|
||||
|
||||
return (new Activity())->forceFill([
|
||||
'type' => strtolower($type),
|
||||
'user_id' => user()->id,
|
||||
'ip' => IpFormatter::fromCurrentRequest()->format(),
|
||||
'ip' => config('app.env') === 'demo' ? '127.0.0.1' : $ip,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace BookStack\Actions;
|
||||
|
||||
use BookStack\Auth\Permissions\PermissionApplicator;
|
||||
use BookStack\Auth\Permissions\PermissionService;
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Entities\Models\Book;
|
||||
use BookStack\Entities\Models\Chapter;
|
||||
@@ -13,11 +13,11 @@ use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
|
||||
class ActivityQueries
|
||||
{
|
||||
protected PermissionApplicator $permissions;
|
||||
protected $permissionService;
|
||||
|
||||
public function __construct(PermissionApplicator $permissions)
|
||||
public function __construct(PermissionService $permissionService)
|
||||
{
|
||||
$this->permissions = $permissions;
|
||||
$this->permissionService = $permissionService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -25,8 +25,8 @@ class ActivityQueries
|
||||
*/
|
||||
public function latest(int $count = 20, int $page = 0): array
|
||||
{
|
||||
$activityList = $this->permissions
|
||||
->restrictEntityRelationQuery(Activity::query(), 'activities', 'entity_id', 'entity_type')
|
||||
$activityList = $this->permissionService
|
||||
->filterRestrictedEntityRelations(Activity::query(), 'activities', 'entity_id', 'entity_type')
|
||||
->orderBy('created_at', 'desc')
|
||||
->with(['user', 'entity'])
|
||||
->skip($count * $page)
|
||||
@@ -78,8 +78,8 @@ class ActivityQueries
|
||||
*/
|
||||
public function userActivity(User $user, int $count = 20, int $page = 0): array
|
||||
{
|
||||
$activityList = $this->permissions
|
||||
->restrictEntityRelationQuery(Activity::query(), 'activities', 'entity_id', 'entity_type')
|
||||
$activityList = $this->permissionService
|
||||
->filterRestrictedEntityRelations(Activity::query(), 'activities', 'entity_id', 'entity_type')
|
||||
->orderBy('created_at', 'desc')
|
||||
->where('user_id', '=', $user->id)
|
||||
->skip($count * $page)
|
||||
|
||||
@@ -16,22 +16,17 @@ class ActivityType
|
||||
const CHAPTER_MOVE = 'chapter_move';
|
||||
|
||||
const BOOK_CREATE = 'book_create';
|
||||
const BOOK_CREATE_FROM_CHAPTER = 'book_create_from_chapter';
|
||||
const BOOK_UPDATE = 'book_update';
|
||||
const BOOK_DELETE = 'book_delete';
|
||||
const BOOK_SORT = 'book_sort';
|
||||
|
||||
const BOOKSHELF_CREATE = 'bookshelf_create';
|
||||
const BOOKSHELF_CREATE_FROM_BOOK = 'bookshelf_create_from_book';
|
||||
const BOOKSHELF_UPDATE = 'bookshelf_update';
|
||||
const BOOKSHELF_DELETE = 'bookshelf_delete';
|
||||
|
||||
const COMMENTED_ON = 'commented_on';
|
||||
const PERMISSIONS_UPDATE = 'permissions_update';
|
||||
|
||||
const REVISION_RESTORE = 'revision_restore';
|
||||
const REVISION_DELETE = 'revision_delete';
|
||||
|
||||
const SETTINGS_UPDATE = 'settings_update';
|
||||
const MAINTENANCE_ACTION_RUN = 'maintenance_action_run';
|
||||
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Actions;
|
||||
|
||||
class IpFormatter
|
||||
{
|
||||
protected string $ip;
|
||||
protected int $precision;
|
||||
|
||||
public function __construct(string $ip, int $precision)
|
||||
{
|
||||
$this->ip = trim($ip);
|
||||
$this->precision = max(0, min($precision, 4));
|
||||
}
|
||||
|
||||
public function format(): string
|
||||
{
|
||||
if (empty($this->ip) || $this->precision === 4) {
|
||||
return $this->ip;
|
||||
}
|
||||
|
||||
return $this->isIpv6() ? $this->maskIpv6() : $this->maskIpv4();
|
||||
}
|
||||
|
||||
protected function maskIpv4(): string
|
||||
{
|
||||
$exploded = $this->explodeAndExpandIp('.', 4);
|
||||
$maskGroupCount = min(4 - $this->precision, count($exploded));
|
||||
|
||||
for ($i = 0; $i < $maskGroupCount; $i++) {
|
||||
$exploded[3 - $i] = 'x';
|
||||
}
|
||||
|
||||
return implode('.', $exploded);
|
||||
}
|
||||
|
||||
protected function maskIpv6(): string
|
||||
{
|
||||
$exploded = $this->explodeAndExpandIp(':', 8);
|
||||
$maskGroupCount = min(8 - ($this->precision * 2), count($exploded));
|
||||
|
||||
for ($i = 0; $i < $maskGroupCount; $i++) {
|
||||
$exploded[7 - $i] = 'x';
|
||||
}
|
||||
|
||||
return implode(':', $exploded);
|
||||
}
|
||||
|
||||
protected function isIpv6(): bool
|
||||
{
|
||||
return strpos($this->ip, ':') !== false;
|
||||
}
|
||||
|
||||
protected function explodeAndExpandIp(string $separator, int $targetLength): array
|
||||
{
|
||||
$exploded = explode($separator, $this->ip);
|
||||
|
||||
while (count($exploded) < $targetLength) {
|
||||
$emptyIndex = array_search('', $exploded) ?: count($exploded) - 1;
|
||||
array_splice($exploded, $emptyIndex, 0, '0');
|
||||
}
|
||||
|
||||
$emptyIndex = array_search('', $exploded);
|
||||
if ($emptyIndex !== false) {
|
||||
$exploded[$emptyIndex] = '0';
|
||||
}
|
||||
|
||||
return $exploded;
|
||||
}
|
||||
|
||||
public static function fromCurrentRequest(): self
|
||||
{
|
||||
$ip = request()->ip() ?? '';
|
||||
|
||||
if (config('app.env') === 'demo') {
|
||||
$ip = '127.0.0.1';
|
||||
}
|
||||
|
||||
return new self($ip, config('app.ip_address_precision'));
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Actions;
|
||||
|
||||
class TagClassGenerator
|
||||
{
|
||||
protected array $tags;
|
||||
|
||||
/**
|
||||
* @param Tag[] $tags
|
||||
*/
|
||||
public function __construct(array $tags)
|
||||
{
|
||||
$this->tags = $tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function generate(): array
|
||||
{
|
||||
$classes = [];
|
||||
|
||||
foreach ($this->tags as $tag) {
|
||||
$name = $this->normalizeTagClassString($tag->name);
|
||||
$value = $this->normalizeTagClassString($tag->value);
|
||||
$classes[] = 'tag-name-' . $name;
|
||||
if ($value) {
|
||||
$classes[] = 'tag-value-' . $value;
|
||||
$classes[] = 'tag-pair-' . $name . '-' . $value;
|
||||
}
|
||||
}
|
||||
|
||||
return array_unique($classes);
|
||||
}
|
||||
|
||||
public function generateAsString(): string
|
||||
{
|
||||
return implode(' ', $this->generate());
|
||||
}
|
||||
|
||||
protected function normalizeTagClassString(string $value): string
|
||||
{
|
||||
$value = str_replace(' ', '', strtolower($value));
|
||||
$value = str_replace('-', '', strtolower($value));
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace BookStack\Actions;
|
||||
|
||||
use BookStack\Auth\Permissions\PermissionApplicator;
|
||||
use BookStack\Auth\Permissions\PermissionService;
|
||||
use BookStack\Entities\Models\Entity;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Collection;
|
||||
@@ -10,11 +10,12 @@ use Illuminate\Support\Facades\DB;
|
||||
|
||||
class TagRepo
|
||||
{
|
||||
protected PermissionApplicator $permissions;
|
||||
protected $tag;
|
||||
protected $permissionService;
|
||||
|
||||
public function __construct(PermissionApplicator $permissions)
|
||||
public function __construct(PermissionService $ps)
|
||||
{
|
||||
$this->permissions = $permissions;
|
||||
$this->permissionService = $ps;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -50,7 +51,7 @@ class TagRepo
|
||||
});
|
||||
}
|
||||
|
||||
return $this->permissions->restrictEntityRelationQuery($query, 'tags', 'entity_id', 'entity_type');
|
||||
return $this->permissionService->filterRestrictedEntityRelations($query, 'tags', 'entity_id', 'entity_type');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,7 +70,7 @@ class TagRepo
|
||||
$query = $query->orderBy('count', 'desc')->take(50);
|
||||
}
|
||||
|
||||
$query = $this->permissions->restrictEntityRelationQuery($query, 'tags', 'entity_id', 'entity_type');
|
||||
$query = $this->permissionService->filterRestrictedEntityRelations($query, 'tags', 'entity_id', 'entity_type');
|
||||
|
||||
return $query->get(['name'])->pluck('name');
|
||||
}
|
||||
@@ -95,7 +96,7 @@ class TagRepo
|
||||
$query = $query->where('name', '=', $tagName);
|
||||
}
|
||||
|
||||
$query = $this->permissions->restrictEntityRelationQuery($query, 'tags', 'entity_id', 'entity_type');
|
||||
$query = $this->permissionService->filterRestrictedEntityRelations($query, 'tags', 'entity_id', 'entity_type');
|
||||
|
||||
return $query->get(['value'])->pluck('value');
|
||||
}
|
||||
|
||||
@@ -28,8 +28,10 @@ class GroupSyncService
|
||||
*/
|
||||
protected function externalIdMatchesGroupNames(string $externalId, array $groupNames): bool
|
||||
{
|
||||
foreach ($this->parseRoleExternalAuthId($externalId) as $externalAuthId) {
|
||||
if (in_array($externalAuthId, $groupNames)) {
|
||||
$externalAuthIds = explode(',', strtolower($externalId));
|
||||
|
||||
foreach ($externalAuthIds as $externalAuthId) {
|
||||
if (in_array(trim($externalAuthId), $groupNames)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -37,18 +39,6 @@ class GroupSyncService
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function parseRoleExternalAuthId(string $externalId): array
|
||||
{
|
||||
$inputIds = preg_split('/(?<!\\\),/', strtolower($externalId));
|
||||
$cleanIds = [];
|
||||
|
||||
foreach ($inputIds as $inputId) {
|
||||
$cleanIds[] = str_replace('\,', ',', trim($inputId));
|
||||
}
|
||||
|
||||
return $cleanIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Match an array of group names to BookStack system roles.
|
||||
* Formats group names to be lower-case and hyphenated.
|
||||
|
||||
@@ -1,405 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Auth\Permissions;
|
||||
|
||||
use BookStack\Auth\Role;
|
||||
use BookStack\Entities\Models\Book;
|
||||
use BookStack\Entities\Models\BookChild;
|
||||
use BookStack\Entities\Models\Bookshelf;
|
||||
use BookStack\Entities\Models\Chapter;
|
||||
use BookStack\Entities\Models\Entity;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* Joint permissions provide a pre-query "cached" table of view permissions for all core entity
|
||||
* types for all roles in the system. This class generates out that table for different scenarios.
|
||||
*/
|
||||
class JointPermissionBuilder
|
||||
{
|
||||
/**
|
||||
* @var array<string, array<int, SimpleEntityData>>
|
||||
*/
|
||||
protected $entityCache;
|
||||
|
||||
/**
|
||||
* Re-generate all entity permission from scratch.
|
||||
*/
|
||||
public function rebuildForAll()
|
||||
{
|
||||
JointPermission::query()->truncate();
|
||||
|
||||
// Get all roles (Should be the most limited dimension)
|
||||
$roles = Role::query()->with('permissions')->get()->all();
|
||||
|
||||
// Chunk through all books
|
||||
$this->bookFetchQuery()->chunk(5, function (EloquentCollection $books) use ($roles) {
|
||||
$this->buildJointPermissionsForBooks($books, $roles);
|
||||
});
|
||||
|
||||
// Chunk through all bookshelves
|
||||
Bookshelf::query()->withTrashed()->select(['id', 'restricted', 'owned_by'])
|
||||
->chunk(50, function (EloquentCollection $shelves) use ($roles) {
|
||||
$this->createManyJointPermissions($shelves->all(), $roles);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild the entity jointPermissions for a particular entity.
|
||||
*/
|
||||
public function rebuildForEntity(Entity $entity)
|
||||
{
|
||||
$entities = [$entity];
|
||||
if ($entity instanceof Book) {
|
||||
$books = $this->bookFetchQuery()->where('id', '=', $entity->id)->get();
|
||||
$this->buildJointPermissionsForBooks($books, Role::query()->with('permissions')->get()->all(), true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var BookChild $entity */
|
||||
if ($entity->book) {
|
||||
$entities[] = $entity->book;
|
||||
}
|
||||
|
||||
if ($entity instanceof Page && $entity->chapter_id) {
|
||||
$entities[] = $entity->chapter;
|
||||
}
|
||||
|
||||
if ($entity instanceof Chapter) {
|
||||
foreach ($entity->pages as $page) {
|
||||
$entities[] = $page;
|
||||
}
|
||||
}
|
||||
|
||||
$this->buildJointPermissionsForEntities($entities);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the entity jointPermissions for a particular role.
|
||||
*/
|
||||
public function rebuildForRole(Role $role)
|
||||
{
|
||||
$roles = [$role];
|
||||
$role->jointPermissions()->delete();
|
||||
$role->load('permissions');
|
||||
|
||||
// Chunk through all books
|
||||
$this->bookFetchQuery()->chunk(20, function ($books) use ($roles) {
|
||||
$this->buildJointPermissionsForBooks($books, $roles);
|
||||
});
|
||||
|
||||
// Chunk through all bookshelves
|
||||
Bookshelf::query()->select(['id', 'restricted', 'owned_by'])
|
||||
->chunk(50, function ($shelves) use ($roles) {
|
||||
$this->createManyJointPermissions($shelves->all(), $roles);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the local entity cache and ensure it's empty.
|
||||
*
|
||||
* @param SimpleEntityData[] $entities
|
||||
*/
|
||||
protected function readyEntityCache(array $entities)
|
||||
{
|
||||
$this->entityCache = [];
|
||||
|
||||
foreach ($entities as $entity) {
|
||||
if (!isset($this->entityCache[$entity->type])) {
|
||||
$this->entityCache[$entity->type] = [];
|
||||
}
|
||||
|
||||
$this->entityCache[$entity->type][$entity->id] = $entity;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a book via ID, Checks local cache.
|
||||
*/
|
||||
protected function getBook(int $bookId): SimpleEntityData
|
||||
{
|
||||
return $this->entityCache['book'][$bookId];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a chapter via ID, Checks local cache.
|
||||
*/
|
||||
protected function getChapter(int $chapterId): SimpleEntityData
|
||||
{
|
||||
return $this->entityCache['chapter'][$chapterId];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a query for fetching a book with its children.
|
||||
*/
|
||||
protected function bookFetchQuery(): Builder
|
||||
{
|
||||
return Book::query()->withTrashed()
|
||||
->select(['id', 'restricted', 'owned_by'])->with([
|
||||
'chapters' => function ($query) {
|
||||
$query->withTrashed()->select(['id', 'restricted', 'owned_by', 'book_id']);
|
||||
},
|
||||
'pages' => function ($query) {
|
||||
$query->withTrashed()->select(['id', 'restricted', 'owned_by', 'book_id', 'chapter_id']);
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build joint permissions for the given book and role combinations.
|
||||
*/
|
||||
protected function buildJointPermissionsForBooks(EloquentCollection $books, array $roles, bool $deleteOld = false)
|
||||
{
|
||||
$entities = clone $books;
|
||||
|
||||
/** @var Book $book */
|
||||
foreach ($books->all() as $book) {
|
||||
foreach ($book->getRelation('chapters') as $chapter) {
|
||||
$entities->push($chapter);
|
||||
}
|
||||
foreach ($book->getRelation('pages') as $page) {
|
||||
$entities->push($page);
|
||||
}
|
||||
}
|
||||
|
||||
if ($deleteOld) {
|
||||
$this->deleteManyJointPermissionsForEntities($entities->all());
|
||||
}
|
||||
|
||||
$this->createManyJointPermissions($entities->all(), $roles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild the entity jointPermissions for a collection of entities.
|
||||
*/
|
||||
protected function buildJointPermissionsForEntities(array $entities)
|
||||
{
|
||||
$roles = Role::query()->get()->values()->all();
|
||||
$this->deleteManyJointPermissionsForEntities($entities);
|
||||
$this->createManyJointPermissions($entities, $roles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all the entity jointPermissions for a list of entities.
|
||||
*
|
||||
* @param Entity[] $entities
|
||||
*/
|
||||
protected function deleteManyJointPermissionsForEntities(array $entities)
|
||||
{
|
||||
$simpleEntities = $this->entitiesToSimpleEntities($entities);
|
||||
$idsByType = $this->entitiesToTypeIdMap($simpleEntities);
|
||||
|
||||
DB::transaction(function () use ($idsByType) {
|
||||
foreach ($idsByType as $type => $ids) {
|
||||
foreach (array_chunk($ids, 1000) as $idChunk) {
|
||||
DB::table('joint_permissions')
|
||||
->where('entity_type', '=', $type)
|
||||
->whereIn('entity_id', $idChunk)
|
||||
->delete();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Entity[] $entities
|
||||
*
|
||||
* @return SimpleEntityData[]
|
||||
*/
|
||||
protected function entitiesToSimpleEntities(array $entities): array
|
||||
{
|
||||
$simpleEntities = [];
|
||||
|
||||
foreach ($entities as $entity) {
|
||||
$attrs = $entity->getAttributes();
|
||||
$simple = new SimpleEntityData();
|
||||
$simple->id = $attrs['id'];
|
||||
$simple->type = $entity->getMorphClass();
|
||||
$simple->restricted = boolval($attrs['restricted'] ?? 0);
|
||||
$simple->owned_by = $attrs['owned_by'] ?? 0;
|
||||
$simple->book_id = $attrs['book_id'] ?? null;
|
||||
$simple->chapter_id = $attrs['chapter_id'] ?? null;
|
||||
$simpleEntities[] = $simple;
|
||||
}
|
||||
|
||||
return $simpleEntities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create & Save entity jointPermissions for many entities and roles.
|
||||
*
|
||||
* @param Entity[] $entities
|
||||
* @param Role[] $roles
|
||||
*/
|
||||
protected function createManyJointPermissions(array $originalEntities, array $roles)
|
||||
{
|
||||
$entities = $this->entitiesToSimpleEntities($originalEntities);
|
||||
$this->readyEntityCache($entities);
|
||||
$jointPermissions = [];
|
||||
|
||||
// Create a mapping of entity restricted statuses
|
||||
$entityRestrictedMap = [];
|
||||
foreach ($entities as $entity) {
|
||||
$entityRestrictedMap[$entity->type . ':' . $entity->id] = $entity->restricted;
|
||||
}
|
||||
|
||||
// Fetch related entity permissions
|
||||
$permissions = $this->getEntityPermissionsForEntities($entities);
|
||||
|
||||
// Create a mapping of explicit entity permissions
|
||||
$permissionMap = [];
|
||||
foreach ($permissions as $permission) {
|
||||
$key = $permission->restrictable_type . ':' . $permission->restrictable_id . ':' . $permission->role_id;
|
||||
$isRestricted = $entityRestrictedMap[$permission->restrictable_type . ':' . $permission->restrictable_id];
|
||||
$permissionMap[$key] = $isRestricted;
|
||||
}
|
||||
|
||||
// Create a mapping of role permissions
|
||||
$rolePermissionMap = [];
|
||||
foreach ($roles as $role) {
|
||||
foreach ($role->permissions as $permission) {
|
||||
$rolePermissionMap[$role->getRawAttribute('id') . ':' . $permission->getRawAttribute('name')] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Create Joint Permission Data
|
||||
foreach ($entities as $entity) {
|
||||
foreach ($roles as $role) {
|
||||
$jointPermissions[] = $this->createJointPermissionData(
|
||||
$entity,
|
||||
$role->getRawAttribute('id'),
|
||||
$permissionMap,
|
||||
$rolePermissionMap,
|
||||
$role->system_name === 'admin'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DB::transaction(function () use ($jointPermissions) {
|
||||
foreach (array_chunk($jointPermissions, 1000) as $jointPermissionChunk) {
|
||||
DB::table('joint_permissions')->insert($jointPermissionChunk);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* From the given entity list, provide back a mapping of entity types to
|
||||
* the ids of that given type. The type used is the DB morph class.
|
||||
*
|
||||
* @param SimpleEntityData[] $entities
|
||||
*
|
||||
* @return array<string, int[]>
|
||||
*/
|
||||
protected function entitiesToTypeIdMap(array $entities): array
|
||||
{
|
||||
$idsByType = [];
|
||||
|
||||
foreach ($entities as $entity) {
|
||||
if (!isset($idsByType[$entity->type])) {
|
||||
$idsByType[$entity->type] = [];
|
||||
}
|
||||
|
||||
$idsByType[$entity->type][] = $entity->id;
|
||||
}
|
||||
|
||||
return $idsByType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entity permissions for all the given entities.
|
||||
*
|
||||
* @param SimpleEntityData[] $entities
|
||||
*
|
||||
* @return EntityPermission[]
|
||||
*/
|
||||
protected function getEntityPermissionsForEntities(array $entities): array
|
||||
{
|
||||
$idsByType = $this->entitiesToTypeIdMap($entities);
|
||||
$permissionFetch = EntityPermission::query()
|
||||
->where('action', '=', 'view')
|
||||
->where(function (Builder $query) use ($idsByType) {
|
||||
foreach ($idsByType as $type => $ids) {
|
||||
$query->orWhere(function (Builder $query) use ($type, $ids) {
|
||||
$query->where('restrictable_type', '=', $type)->whereIn('restrictable_id', $ids);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return $permissionFetch->get()->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create entity permission data for an entity and role
|
||||
* for a particular action.
|
||||
*/
|
||||
protected function createJointPermissionData(SimpleEntityData $entity, int $roleId, array $permissionMap, array $rolePermissionMap, bool $isAdminRole): array
|
||||
{
|
||||
$permissionPrefix = $entity->type . '-view';
|
||||
$roleHasPermission = isset($rolePermissionMap[$roleId . ':' . $permissionPrefix . '-all']);
|
||||
$roleHasPermissionOwn = isset($rolePermissionMap[$roleId . ':' . $permissionPrefix . '-own']);
|
||||
|
||||
if ($isAdminRole) {
|
||||
return $this->createJointPermissionDataArray($entity, $roleId, true, true);
|
||||
}
|
||||
|
||||
if ($entity->restricted) {
|
||||
$hasAccess = $this->mapHasActiveRestriction($permissionMap, $entity, $roleId);
|
||||
|
||||
return $this->createJointPermissionDataArray($entity, $roleId, $hasAccess, $hasAccess);
|
||||
}
|
||||
|
||||
if ($entity->type === 'book' || $entity->type === 'bookshelf') {
|
||||
return $this->createJointPermissionDataArray($entity, $roleId, $roleHasPermission, $roleHasPermissionOwn);
|
||||
}
|
||||
|
||||
// For chapters and pages, Check if explicit permissions are set on the Book.
|
||||
$book = $this->getBook($entity->book_id);
|
||||
$hasExplicitAccessToParents = $this->mapHasActiveRestriction($permissionMap, $book, $roleId);
|
||||
$hasPermissiveAccessToParents = !$book->restricted;
|
||||
|
||||
// For pages with a chapter, Check if explicit permissions are set on the Chapter
|
||||
if ($entity->type === 'page' && $entity->chapter_id !== 0) {
|
||||
$chapter = $this->getChapter($entity->chapter_id);
|
||||
$hasPermissiveAccessToParents = $hasPermissiveAccessToParents && !$chapter->restricted;
|
||||
if ($chapter->restricted) {
|
||||
$hasExplicitAccessToParents = $this->mapHasActiveRestriction($permissionMap, $chapter, $roleId);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->createJointPermissionDataArray(
|
||||
$entity,
|
||||
$roleId,
|
||||
($hasExplicitAccessToParents || ($roleHasPermission && $hasPermissiveAccessToParents)),
|
||||
($hasExplicitAccessToParents || ($roleHasPermissionOwn && $hasPermissiveAccessToParents))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for an active restriction in an entity map.
|
||||
*/
|
||||
protected function mapHasActiveRestriction(array $entityMap, SimpleEntityData $entity, int $roleId): bool
|
||||
{
|
||||
$key = $entity->type . ':' . $entity->id . ':' . $roleId;
|
||||
|
||||
return $entityMap[$key] ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an array of data with the information of an entity jointPermissions.
|
||||
* Used to build data for bulk insertion.
|
||||
*/
|
||||
protected function createJointPermissionDataArray(SimpleEntityData $entity, int $roleId, bool $permissionAll, bool $permissionOwn): array
|
||||
{
|
||||
return [
|
||||
'entity_id' => $entity->id,
|
||||
'entity_type' => $entity->type,
|
||||
'has_permission' => $permissionAll,
|
||||
'has_permission_own' => $permissionOwn,
|
||||
'owned_by' => $entity->owned_by,
|
||||
'role_id' => $roleId,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,259 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Auth\Permissions;
|
||||
|
||||
use BookStack\Auth\Role;
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Entities\Models\Chapter;
|
||||
use BookStack\Entities\Models\Entity;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Model;
|
||||
use BookStack\Traits\HasCreatorAndUpdater;
|
||||
use BookStack\Traits\HasOwner;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Query\Builder as QueryBuilder;
|
||||
use InvalidArgumentException;
|
||||
|
||||
class PermissionApplicator
|
||||
{
|
||||
/**
|
||||
* Checks if an entity has a restriction set upon it.
|
||||
*
|
||||
* @param HasCreatorAndUpdater|HasOwner $ownable
|
||||
*/
|
||||
public function checkOwnableUserAccess(Model $ownable, string $permission): bool
|
||||
{
|
||||
$explodedPermission = explode('-', $permission);
|
||||
$action = $explodedPermission[1] ?? $explodedPermission[0];
|
||||
$fullPermission = count($explodedPermission) > 1 ? $permission : $ownable->getMorphClass() . '-' . $permission;
|
||||
|
||||
$user = $this->currentUser();
|
||||
$userRoleIds = $this->getCurrentUserRoleIds();
|
||||
|
||||
$allRolePermission = $user->can($fullPermission . '-all');
|
||||
$ownRolePermission = $user->can($fullPermission . '-own');
|
||||
$nonJointPermissions = ['restrictions', 'image', 'attachment', 'comment'];
|
||||
$ownerField = ($ownable instanceof Entity) ? 'owned_by' : 'created_by';
|
||||
$ownableFieldVal = $ownable->getAttribute($ownerField);
|
||||
|
||||
if (is_null($ownableFieldVal)) {
|
||||
throw new InvalidArgumentException("{$ownerField} field used but has not been loaded");
|
||||
}
|
||||
|
||||
$isOwner = $user->id === $ownableFieldVal;
|
||||
$hasRolePermission = $allRolePermission || ($isOwner && $ownRolePermission);
|
||||
|
||||
// Handle non entity specific jointPermissions
|
||||
if (in_array($explodedPermission[0], $nonJointPermissions)) {
|
||||
return $hasRolePermission;
|
||||
}
|
||||
|
||||
$hasApplicableEntityPermissions = $this->hasEntityPermission($ownable, $userRoleIds, $action);
|
||||
|
||||
return is_null($hasApplicableEntityPermissions) ? $hasRolePermission : $hasApplicableEntityPermissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there are permissions that are applicable for the given entity item, action and roles.
|
||||
* Returns null when no entity permissions are in force.
|
||||
*/
|
||||
protected function hasEntityPermission(Entity $entity, array $userRoleIds, string $action): ?bool
|
||||
{
|
||||
$adminRoleId = Role::getSystemRole('admin')->id;
|
||||
if (in_array($adminRoleId, $userRoleIds)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$chain = [$entity];
|
||||
if ($entity instanceof Page && $entity->chapter_id) {
|
||||
$chain[] = $entity->chapter;
|
||||
}
|
||||
|
||||
if ($entity instanceof Page || $entity instanceof Chapter) {
|
||||
$chain[] = $entity->book;
|
||||
}
|
||||
|
||||
foreach ($chain as $currentEntity) {
|
||||
|
||||
if (is_null($currentEntity->restricted)) {
|
||||
throw new InvalidArgumentException("Entity restricted field used but has not been loaded");
|
||||
}
|
||||
|
||||
if ($currentEntity->restricted) {
|
||||
return $currentEntity->permissions()
|
||||
->whereIn('role_id', $userRoleIds)
|
||||
->where('action', '=', $action)
|
||||
->count() > 0;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a user has the given permission for any items in the system.
|
||||
* Can be passed an entity instance to filter on a specific type.
|
||||
*/
|
||||
public function checkUserHasEntityPermissionOnAny(string $action, string $entityClass = ''): bool
|
||||
{
|
||||
if (strpos($action, '-') !== false) {
|
||||
throw new InvalidArgumentException('Action should be a simple entity permission action, not a role permission');
|
||||
}
|
||||
|
||||
$permissionQuery = EntityPermission::query()
|
||||
->where('action', '=', $action)
|
||||
->whereIn('role_id', $this->getCurrentUserRoleIds());
|
||||
|
||||
if (!empty($entityClass)) {
|
||||
/** @var Entity $entityInstance */
|
||||
$entityInstance = app()->make($entityClass);
|
||||
$permissionQuery = $permissionQuery->where('restrictable_type', '=', $entityInstance->getMorphClass());
|
||||
}
|
||||
|
||||
$hasPermission = $permissionQuery->count() > 0;
|
||||
|
||||
return $hasPermission;
|
||||
}
|
||||
|
||||
/**
|
||||
* Limit the given entity query so that the query will only
|
||||
* return items that the user has view permission for.
|
||||
*/
|
||||
public function restrictEntityQuery(Builder $query): Builder
|
||||
{
|
||||
return $query->where(function (Builder $parentQuery) {
|
||||
$parentQuery->whereHas('jointPermissions', function (Builder $permissionQuery) {
|
||||
$permissionQuery->whereIn('role_id', $this->getCurrentUserRoleIds())
|
||||
->where(function (Builder $query) {
|
||||
$this->addJointHasPermissionCheck($query, $this->currentUser()->id);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend the given page query to ensure draft items are not visible
|
||||
* unless created by the given user.
|
||||
*/
|
||||
public function restrictDraftsOnPageQuery(Builder $query): Builder
|
||||
{
|
||||
return $query->where(function (Builder $query) {
|
||||
$query->where('draft', '=', false)
|
||||
->orWhere(function (Builder $query) {
|
||||
$query->where('draft', '=', true)
|
||||
->where('owned_by', '=', $this->currentUser()->id);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter items that have entities set as a polymorphic relation.
|
||||
* For simplicity, this will not return results attached to draft pages.
|
||||
* Draft pages should never really have related items though.
|
||||
*
|
||||
* @param Builder|QueryBuilder $query
|
||||
*/
|
||||
public function restrictEntityRelationQuery($query, string $tableName, string $entityIdColumn, string $entityTypeColumn)
|
||||
{
|
||||
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
|
||||
$pageMorphClass = (new Page())->getMorphClass();
|
||||
|
||||
$q = $query->whereExists(function ($permissionQuery) use (&$tableDetails) {
|
||||
/** @var Builder $permissionQuery */
|
||||
$permissionQuery->select(['role_id'])->from('joint_permissions')
|
||||
->whereColumn('joint_permissions.entity_id', '=', $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
|
||||
->whereColumn('joint_permissions.entity_type', '=', $tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn'])
|
||||
->whereIn('joint_permissions.role_id', $this->getCurrentUserRoleIds())
|
||||
->where(function (QueryBuilder $query) {
|
||||
$this->addJointHasPermissionCheck($query, $this->currentUser()->id);
|
||||
});
|
||||
})->where(function ($query) use ($tableDetails, $pageMorphClass) {
|
||||
/** @var Builder $query */
|
||||
$query->where($tableDetails['entityTypeColumn'], '!=', $pageMorphClass)
|
||||
->orWhereExists(function (QueryBuilder $query) use ($tableDetails, $pageMorphClass) {
|
||||
$query->select('id')->from('pages')
|
||||
->whereColumn('pages.id', '=', $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
|
||||
->where($tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn'], '=', $pageMorphClass)
|
||||
->where('pages.draft', '=', false);
|
||||
});
|
||||
});
|
||||
|
||||
return $q;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add conditions to a query for a model that's a relation of a page, so only the model results
|
||||
* on visible pages are returned by the query.
|
||||
* Is effectively the same as "restrictEntityRelationQuery" but takes into account page drafts
|
||||
* while not expecting a polymorphic relation, Just a simpler one-page-to-many-relations set-up.
|
||||
*/
|
||||
public function restrictPageRelationQuery(Builder $query, string $tableName, string $pageIdColumn): Builder
|
||||
{
|
||||
$fullPageIdColumn = $tableName . '.' . $pageIdColumn;
|
||||
$morphClass = (new Page())->getMorphClass();
|
||||
|
||||
$existsQuery = function ($permissionQuery) use ($fullPageIdColumn, $morphClass) {
|
||||
/** @var Builder $permissionQuery */
|
||||
$permissionQuery->select('joint_permissions.role_id')->from('joint_permissions')
|
||||
->whereColumn('joint_permissions.entity_id', '=', $fullPageIdColumn)
|
||||
->where('joint_permissions.entity_type', '=', $morphClass)
|
||||
->whereIn('joint_permissions.role_id', $this->getCurrentUserRoleIds())
|
||||
->where(function (QueryBuilder $query) {
|
||||
$this->addJointHasPermissionCheck($query, $this->currentUser()->id);
|
||||
});
|
||||
};
|
||||
|
||||
$q = $query->where(function ($query) use ($existsQuery, $fullPageIdColumn) {
|
||||
$query->whereExists($existsQuery)
|
||||
->orWhere($fullPageIdColumn, '=', 0);
|
||||
});
|
||||
|
||||
// Prevent visibility of non-owned draft pages
|
||||
$q->whereExists(function (QueryBuilder $query) use ($fullPageIdColumn) {
|
||||
$query->select('id')->from('pages')
|
||||
->whereColumn('pages.id', '=', $fullPageIdColumn)
|
||||
->where(function (QueryBuilder $query) {
|
||||
$query->where('pages.draft', '=', false)
|
||||
->orWhere('pages.owned_by', '=', $this->currentUser()->id);
|
||||
});
|
||||
});
|
||||
|
||||
return $q;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the query for checking the given user id has permission
|
||||
* within the join_permissions table.
|
||||
*
|
||||
* @param QueryBuilder|Builder $query
|
||||
*/
|
||||
protected function addJointHasPermissionCheck($query, int $userIdToCheck)
|
||||
{
|
||||
$query->where('joint_permissions.has_permission', '=', true)->orWhere(function ($query) use ($userIdToCheck) {
|
||||
$query->where('joint_permissions.has_permission_own', '=', true)
|
||||
->where('joint_permissions.owned_by', '=', $userIdToCheck);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current user.
|
||||
*/
|
||||
protected function currentUser(): User
|
||||
{
|
||||
return user();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the roles for the current logged-in user.
|
||||
*
|
||||
* @return int[]
|
||||
*/
|
||||
protected function getCurrentUserRoleIds(): array
|
||||
{
|
||||
if (auth()->guest()) {
|
||||
return [Role::getSystemRole('public')->id];
|
||||
}
|
||||
|
||||
return $this->currentUser()->roles->pluck('id')->values()->all();
|
||||
}
|
||||
}
|
||||
719
app/Auth/Permissions/PermissionService.php
Normal file
719
app/Auth/Permissions/PermissionService.php
Normal file
@@ -0,0 +1,719 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Auth\Permissions;
|
||||
|
||||
use BookStack\Auth\Role;
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Entities\Models\Book;
|
||||
use BookStack\Entities\Models\BookChild;
|
||||
use BookStack\Entities\Models\Bookshelf;
|
||||
use BookStack\Entities\Models\Chapter;
|
||||
use BookStack\Entities\Models\Entity;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Model;
|
||||
use BookStack\Traits\HasCreatorAndUpdater;
|
||||
use BookStack\Traits\HasOwner;
|
||||
use Illuminate\Database\Connection;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
|
||||
use Illuminate\Database\Query\Builder as QueryBuilder;
|
||||
use Throwable;
|
||||
|
||||
class PermissionService
|
||||
{
|
||||
/**
|
||||
* @var ?array
|
||||
*/
|
||||
protected $userRoles = null;
|
||||
|
||||
/**
|
||||
* @var ?User
|
||||
*/
|
||||
protected $currentUserModel = null;
|
||||
|
||||
/**
|
||||
* @var Connection
|
||||
*/
|
||||
protected $db;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $entityCache;
|
||||
|
||||
/**
|
||||
* PermissionService constructor.
|
||||
*/
|
||||
public function __construct(Connection $db)
|
||||
{
|
||||
$this->db = $db;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the database connection.
|
||||
*/
|
||||
public function setConnection(Connection $connection)
|
||||
{
|
||||
$this->db = $connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the local entity cache and ensure it's empty.
|
||||
*
|
||||
* @param Entity[] $entities
|
||||
*/
|
||||
protected function readyEntityCache(array $entities = [])
|
||||
{
|
||||
$this->entityCache = [];
|
||||
|
||||
foreach ($entities as $entity) {
|
||||
$class = get_class($entity);
|
||||
if (!isset($this->entityCache[$class])) {
|
||||
$this->entityCache[$class] = collect();
|
||||
}
|
||||
$this->entityCache[$class]->put($entity->id, $entity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a book via ID, Checks local cache.
|
||||
*/
|
||||
protected function getBook(int $bookId): ?Book
|
||||
{
|
||||
if (isset($this->entityCache[Book::class]) && $this->entityCache[Book::class]->has($bookId)) {
|
||||
return $this->entityCache[Book::class]->get($bookId);
|
||||
}
|
||||
|
||||
return Book::query()->withTrashed()->find($bookId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a chapter via ID, Checks local cache.
|
||||
*/
|
||||
protected function getChapter(int $chapterId): ?Chapter
|
||||
{
|
||||
if (isset($this->entityCache[Chapter::class]) && $this->entityCache[Chapter::class]->has($chapterId)) {
|
||||
return $this->entityCache[Chapter::class]->get($chapterId);
|
||||
}
|
||||
|
||||
return Chapter::query()
|
||||
->withTrashed()
|
||||
->find($chapterId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the roles for the current logged in user.
|
||||
*/
|
||||
protected function getCurrentUserRoles(): array
|
||||
{
|
||||
if (!is_null($this->userRoles)) {
|
||||
return $this->userRoles;
|
||||
}
|
||||
|
||||
if (auth()->guest()) {
|
||||
$this->userRoles = [Role::getSystemRole('public')->id];
|
||||
} else {
|
||||
$this->userRoles = $this->currentUser()->roles->pluck('id')->values()->all();
|
||||
}
|
||||
|
||||
return $this->userRoles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-generate all entity permission from scratch.
|
||||
*/
|
||||
public function buildJointPermissions()
|
||||
{
|
||||
JointPermission::query()->truncate();
|
||||
$this->readyEntityCache();
|
||||
|
||||
// Get all roles (Should be the most limited dimension)
|
||||
$roles = Role::query()->with('permissions')->get()->all();
|
||||
|
||||
// Chunk through all books
|
||||
$this->bookFetchQuery()->chunk(5, function (EloquentCollection $books) use ($roles) {
|
||||
$this->buildJointPermissionsForBooks($books, $roles);
|
||||
});
|
||||
|
||||
// Chunk through all bookshelves
|
||||
Bookshelf::query()->withTrashed()->select(['id', 'restricted', 'owned_by'])
|
||||
->chunk(50, function (EloquentCollection $shelves) use ($roles) {
|
||||
$this->buildJointPermissionsForShelves($shelves, $roles);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a query for fetching a book with it's children.
|
||||
*/
|
||||
protected function bookFetchQuery(): Builder
|
||||
{
|
||||
return Book::query()->withTrashed()
|
||||
->select(['id', 'restricted', 'owned_by'])->with([
|
||||
'chapters' => function ($query) {
|
||||
$query->withTrashed()->select(['id', 'restricted', 'owned_by', 'book_id']);
|
||||
},
|
||||
'pages' => function ($query) {
|
||||
$query->withTrashed()->select(['id', 'restricted', 'owned_by', 'book_id', 'chapter_id']);
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build joint permissions for the given shelf and role combinations.
|
||||
*
|
||||
* @throws Throwable
|
||||
*/
|
||||
protected function buildJointPermissionsForShelves(EloquentCollection $shelves, array $roles, bool $deleteOld = false)
|
||||
{
|
||||
if ($deleteOld) {
|
||||
$this->deleteManyJointPermissionsForEntities($shelves->all());
|
||||
}
|
||||
$this->createManyJointPermissions($shelves->all(), $roles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build joint permissions for the given book and role combinations.
|
||||
*
|
||||
* @throws Throwable
|
||||
*/
|
||||
protected function buildJointPermissionsForBooks(EloquentCollection $books, array $roles, bool $deleteOld = false)
|
||||
{
|
||||
$entities = clone $books;
|
||||
|
||||
/** @var Book $book */
|
||||
foreach ($books->all() as $book) {
|
||||
foreach ($book->getRelation('chapters') as $chapter) {
|
||||
$entities->push($chapter);
|
||||
}
|
||||
foreach ($book->getRelation('pages') as $page) {
|
||||
$entities->push($page);
|
||||
}
|
||||
}
|
||||
|
||||
if ($deleteOld) {
|
||||
$this->deleteManyJointPermissionsForEntities($entities->all());
|
||||
}
|
||||
$this->createManyJointPermissions($entities->all(), $roles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild the entity jointPermissions for a particular entity.
|
||||
*
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function buildJointPermissionsForEntity(Entity $entity)
|
||||
{
|
||||
$entities = [$entity];
|
||||
if ($entity instanceof Book) {
|
||||
$books = $this->bookFetchQuery()->where('id', '=', $entity->id)->get();
|
||||
$this->buildJointPermissionsForBooks($books, Role::query()->get()->all(), true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var BookChild $entity */
|
||||
if ($entity->book) {
|
||||
$entities[] = $entity->book;
|
||||
}
|
||||
|
||||
if ($entity instanceof Page && $entity->chapter_id) {
|
||||
$entities[] = $entity->chapter;
|
||||
}
|
||||
|
||||
if ($entity instanceof Chapter) {
|
||||
foreach ($entity->pages as $page) {
|
||||
$entities[] = $page;
|
||||
}
|
||||
}
|
||||
|
||||
$this->buildJointPermissionsForEntities($entities);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild the entity jointPermissions for a collection of entities.
|
||||
*
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function buildJointPermissionsForEntities(array $entities)
|
||||
{
|
||||
$roles = Role::query()->get()->values()->all();
|
||||
$this->deleteManyJointPermissionsForEntities($entities);
|
||||
$this->createManyJointPermissions($entities, $roles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the entity jointPermissions for a particular role.
|
||||
*/
|
||||
public function buildJointPermissionForRole(Role $role)
|
||||
{
|
||||
$roles = [$role];
|
||||
$this->deleteManyJointPermissionsForRoles($roles);
|
||||
|
||||
// Chunk through all books
|
||||
$this->bookFetchQuery()->chunk(20, function ($books) use ($roles) {
|
||||
$this->buildJointPermissionsForBooks($books, $roles);
|
||||
});
|
||||
|
||||
// Chunk through all bookshelves
|
||||
Bookshelf::query()->select(['id', 'restricted', 'owned_by'])
|
||||
->chunk(50, function ($shelves) use ($roles) {
|
||||
$this->buildJointPermissionsForShelves($shelves, $roles);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the entity jointPermissions attached to a particular role.
|
||||
*/
|
||||
public function deleteJointPermissionsForRole(Role $role)
|
||||
{
|
||||
$this->deleteManyJointPermissionsForRoles([$role]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all of the entity jointPermissions for a list of entities.
|
||||
*
|
||||
* @param Role[] $roles
|
||||
*/
|
||||
protected function deleteManyJointPermissionsForRoles($roles)
|
||||
{
|
||||
$roleIds = array_map(function ($role) {
|
||||
return $role->id;
|
||||
}, $roles);
|
||||
JointPermission::query()->whereIn('role_id', $roleIds)->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the entity jointPermissions for a particular entity.
|
||||
*
|
||||
* @param Entity $entity
|
||||
*
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function deleteJointPermissionsForEntity(Entity $entity)
|
||||
{
|
||||
$this->deleteManyJointPermissionsForEntities([$entity]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all of the entity jointPermissions for a list of entities.
|
||||
*
|
||||
* @param Entity[] $entities
|
||||
*
|
||||
* @throws Throwable
|
||||
*/
|
||||
protected function deleteManyJointPermissionsForEntities(array $entities)
|
||||
{
|
||||
if (count($entities) === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->db->transaction(function () use ($entities) {
|
||||
foreach (array_chunk($entities, 1000) as $entityChunk) {
|
||||
$query = $this->db->table('joint_permissions');
|
||||
foreach ($entityChunk as $entity) {
|
||||
$query->orWhere(function (QueryBuilder $query) use ($entity) {
|
||||
$query->where('entity_id', '=', $entity->id)
|
||||
->where('entity_type', '=', $entity->getMorphClass());
|
||||
});
|
||||
}
|
||||
$query->delete();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create & Save entity jointPermissions for many entities and roles.
|
||||
*
|
||||
* @param Entity[] $entities
|
||||
* @param Role[] $roles
|
||||
*
|
||||
* @throws Throwable
|
||||
*/
|
||||
protected function createManyJointPermissions(array $entities, array $roles)
|
||||
{
|
||||
$this->readyEntityCache($entities);
|
||||
$jointPermissions = [];
|
||||
|
||||
// Fetch Entity Permissions and create a mapping of entity restricted statuses
|
||||
$entityRestrictedMap = [];
|
||||
$permissionFetch = EntityPermission::query();
|
||||
foreach ($entities as $entity) {
|
||||
$entityRestrictedMap[$entity->getMorphClass() . ':' . $entity->id] = boolval($entity->getRawAttribute('restricted'));
|
||||
$permissionFetch->orWhere(function ($query) use ($entity) {
|
||||
$query->where('restrictable_id', '=', $entity->id)->where('restrictable_type', '=', $entity->getMorphClass());
|
||||
});
|
||||
}
|
||||
$permissions = $permissionFetch->get();
|
||||
|
||||
// Create a mapping of explicit entity permissions
|
||||
$permissionMap = [];
|
||||
foreach ($permissions as $permission) {
|
||||
$key = $permission->restrictable_type . ':' . $permission->restrictable_id . ':' . $permission->role_id . ':' . $permission->action;
|
||||
$isRestricted = $entityRestrictedMap[$permission->restrictable_type . ':' . $permission->restrictable_id];
|
||||
$permissionMap[$key] = $isRestricted;
|
||||
}
|
||||
|
||||
// Create a mapping of role permissions
|
||||
$rolePermissionMap = [];
|
||||
foreach ($roles as $role) {
|
||||
foreach ($role->permissions as $permission) {
|
||||
$rolePermissionMap[$role->getRawAttribute('id') . ':' . $permission->getRawAttribute('name')] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Create Joint Permission Data
|
||||
foreach ($entities as $entity) {
|
||||
foreach ($roles as $role) {
|
||||
foreach ($this->getActions($entity) as $action) {
|
||||
$jointPermissions[] = $this->createJointPermissionData($entity, $role, $action, $permissionMap, $rolePermissionMap);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->db->transaction(function () use ($jointPermissions) {
|
||||
foreach (array_chunk($jointPermissions, 1000) as $jointPermissionChunk) {
|
||||
$this->db->table('joint_permissions')->insert($jointPermissionChunk);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the actions related to an entity.
|
||||
*/
|
||||
protected function getActions(Entity $entity): array
|
||||
{
|
||||
$baseActions = ['view', 'update', 'delete'];
|
||||
if ($entity instanceof Chapter || $entity instanceof Book) {
|
||||
$baseActions[] = 'page-create';
|
||||
}
|
||||
if ($entity instanceof Book) {
|
||||
$baseActions[] = 'chapter-create';
|
||||
}
|
||||
|
||||
return $baseActions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create entity permission data for an entity and role
|
||||
* for a particular action.
|
||||
*/
|
||||
protected function createJointPermissionData(Entity $entity, Role $role, string $action, array $permissionMap, array $rolePermissionMap): array
|
||||
{
|
||||
$permissionPrefix = (strpos($action, '-') === false ? ($entity->getType() . '-') : '') . $action;
|
||||
$roleHasPermission = isset($rolePermissionMap[$role->getRawAttribute('id') . ':' . $permissionPrefix . '-all']);
|
||||
$roleHasPermissionOwn = isset($rolePermissionMap[$role->getRawAttribute('id') . ':' . $permissionPrefix . '-own']);
|
||||
$explodedAction = explode('-', $action);
|
||||
$restrictionAction = end($explodedAction);
|
||||
|
||||
if ($role->system_name === 'admin') {
|
||||
return $this->createJointPermissionDataArray($entity, $role, $action, true, true);
|
||||
}
|
||||
|
||||
if ($entity->restricted) {
|
||||
$hasAccess = $this->mapHasActiveRestriction($permissionMap, $entity, $role, $restrictionAction);
|
||||
|
||||
return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
|
||||
}
|
||||
|
||||
if ($entity instanceof Book || $entity instanceof Bookshelf) {
|
||||
return $this->createJointPermissionDataArray($entity, $role, $action, $roleHasPermission, $roleHasPermissionOwn);
|
||||
}
|
||||
|
||||
// For chapters and pages, Check if explicit permissions are set on the Book.
|
||||
$book = $this->getBook($entity->book_id);
|
||||
$hasExplicitAccessToParents = $this->mapHasActiveRestriction($permissionMap, $book, $role, $restrictionAction);
|
||||
$hasPermissiveAccessToParents = !$book->restricted;
|
||||
|
||||
// For pages with a chapter, Check if explicit permissions are set on the Chapter
|
||||
if ($entity instanceof Page && intval($entity->chapter_id) !== 0) {
|
||||
$chapter = $this->getChapter($entity->chapter_id);
|
||||
$hasPermissiveAccessToParents = $hasPermissiveAccessToParents && !$chapter->restricted;
|
||||
if ($chapter->restricted) {
|
||||
$hasExplicitAccessToParents = $this->mapHasActiveRestriction($permissionMap, $chapter, $role, $restrictionAction);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->createJointPermissionDataArray(
|
||||
$entity,
|
||||
$role,
|
||||
$action,
|
||||
($hasExplicitAccessToParents || ($roleHasPermission && $hasPermissiveAccessToParents)),
|
||||
($hasExplicitAccessToParents || ($roleHasPermissionOwn && $hasPermissiveAccessToParents))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for an active restriction in an entity map.
|
||||
*/
|
||||
protected function mapHasActiveRestriction(array $entityMap, Entity $entity, Role $role, string $action): bool
|
||||
{
|
||||
$key = $entity->getMorphClass() . ':' . $entity->getRawAttribute('id') . ':' . $role->getRawAttribute('id') . ':' . $action;
|
||||
|
||||
return $entityMap[$key] ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an array of data with the information of an entity jointPermissions.
|
||||
* Used to build data for bulk insertion.
|
||||
*/
|
||||
protected function createJointPermissionDataArray(Entity $entity, Role $role, string $action, bool $permissionAll, bool $permissionOwn): array
|
||||
{
|
||||
return [
|
||||
'role_id' => $role->getRawAttribute('id'),
|
||||
'entity_id' => $entity->getRawAttribute('id'),
|
||||
'entity_type' => $entity->getMorphClass(),
|
||||
'action' => $action,
|
||||
'has_permission' => $permissionAll,
|
||||
'has_permission_own' => $permissionOwn,
|
||||
'owned_by' => $entity->getRawAttribute('owned_by'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an entity has a restriction set upon it.
|
||||
*
|
||||
* @param HasCreatorAndUpdater|HasOwner $ownable
|
||||
*/
|
||||
public function checkOwnableUserAccess(Model $ownable, string $permission): bool
|
||||
{
|
||||
$explodedPermission = explode('-', $permission);
|
||||
|
||||
$baseQuery = $ownable->newQuery()->where('id', '=', $ownable->id);
|
||||
$action = end($explodedPermission);
|
||||
$user = $this->currentUser();
|
||||
|
||||
$nonJointPermissions = ['restrictions', 'image', 'attachment', 'comment'];
|
||||
|
||||
// Handle non entity specific jointPermissions
|
||||
if (in_array($explodedPermission[0], $nonJointPermissions)) {
|
||||
$allPermission = $user && $user->can($permission . '-all');
|
||||
$ownPermission = $user && $user->can($permission . '-own');
|
||||
$ownerField = ($ownable instanceof Entity) ? 'owned_by' : 'created_by';
|
||||
$isOwner = $user && $user->id === $ownable->$ownerField;
|
||||
|
||||
return $allPermission || ($isOwner && $ownPermission);
|
||||
}
|
||||
|
||||
// Handle abnormal create jointPermissions
|
||||
if ($action === 'create') {
|
||||
$action = $permission;
|
||||
}
|
||||
|
||||
$hasAccess = $this->entityRestrictionQuery($baseQuery, $action)->count() > 0;
|
||||
$this->clean();
|
||||
|
||||
return $hasAccess;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a user has the given permission for any items in the system.
|
||||
* Can be passed an entity instance to filter on a specific type.
|
||||
*/
|
||||
public function checkUserHasPermissionOnAnything(string $permission, ?string $entityClass = null): bool
|
||||
{
|
||||
$userRoleIds = $this->currentUser()->roles()->select('id')->pluck('id')->toArray();
|
||||
$userId = $this->currentUser()->id;
|
||||
|
||||
$permissionQuery = JointPermission::query()
|
||||
->where('action', '=', $permission)
|
||||
->whereIn('role_id', $userRoleIds)
|
||||
->where(function (Builder $query) use ($userId) {
|
||||
$this->addJointHasPermissionCheck($query, $userId);
|
||||
});
|
||||
|
||||
if (!is_null($entityClass)) {
|
||||
$entityInstance = app($entityClass);
|
||||
$permissionQuery = $permissionQuery->where('entity_type', '=', $entityInstance->getMorphClass());
|
||||
}
|
||||
|
||||
$hasPermission = $permissionQuery->count() > 0;
|
||||
$this->clean();
|
||||
|
||||
return $hasPermission;
|
||||
}
|
||||
|
||||
/**
|
||||
* The general query filter to remove all entities
|
||||
* that the current user does not have access to.
|
||||
*/
|
||||
protected function entityRestrictionQuery(Builder $query, string $action): Builder
|
||||
{
|
||||
$q = $query->where(function ($parentQuery) use ($action) {
|
||||
$parentQuery->whereHas('jointPermissions', function ($permissionQuery) use ($action) {
|
||||
$permissionQuery->whereIn('role_id', $this->getCurrentUserRoles())
|
||||
->where('action', '=', $action)
|
||||
->where(function (Builder $query) {
|
||||
$this->addJointHasPermissionCheck($query, $this->currentUser()->id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
$this->clean();
|
||||
|
||||
return $q;
|
||||
}
|
||||
|
||||
/**
|
||||
* Limited the given entity query so that the query will only
|
||||
* return items that the user has permission for the given ability.
|
||||
*/
|
||||
public function restrictEntityQuery(Builder $query, string $ability = 'view'): Builder
|
||||
{
|
||||
$this->clean();
|
||||
|
||||
return $query->where(function (Builder $parentQuery) use ($ability) {
|
||||
$parentQuery->whereHas('jointPermissions', function (Builder $permissionQuery) use ($ability) {
|
||||
$permissionQuery->whereIn('role_id', $this->getCurrentUserRoles())
|
||||
->where('action', '=', $ability)
|
||||
->where(function (Builder $query) {
|
||||
$this->addJointHasPermissionCheck($query, $this->currentUser()->id);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend the given page query to ensure draft items are not visible
|
||||
* unless created by the given user.
|
||||
*/
|
||||
public function enforceDraftVisibilityOnQuery(Builder $query): Builder
|
||||
{
|
||||
return $query->where(function (Builder $query) {
|
||||
$query->where('draft', '=', false)
|
||||
->orWhere(function (Builder $query) {
|
||||
$query->where('draft', '=', true)
|
||||
->where('owned_by', '=', $this->currentUser()->id);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add restrictions for a generic entity.
|
||||
*/
|
||||
public function enforceEntityRestrictions(Entity $entity, Builder $query, string $action = 'view'): Builder
|
||||
{
|
||||
if ($entity instanceof Page) {
|
||||
// Prevent drafts being visible to others.
|
||||
$this->enforceDraftVisibilityOnQuery($query);
|
||||
}
|
||||
|
||||
return $this->entityRestrictionQuery($query, $action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter items that have entities set as a polymorphic relation.
|
||||
* For simplicity, this will not return results attached to draft pages.
|
||||
* Draft pages should never really have related items though.
|
||||
*
|
||||
* @param Builder|QueryBuilder $query
|
||||
*/
|
||||
public function filterRestrictedEntityRelations($query, string $tableName, string $entityIdColumn, string $entityTypeColumn, string $action = 'view')
|
||||
{
|
||||
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
|
||||
$pageMorphClass = (new Page())->getMorphClass();
|
||||
|
||||
$q = $query->whereExists(function ($permissionQuery) use (&$tableDetails, $action) {
|
||||
/** @var Builder $permissionQuery */
|
||||
$permissionQuery->select(['role_id'])->from('joint_permissions')
|
||||
->whereColumn('joint_permissions.entity_id', '=', $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
|
||||
->whereColumn('joint_permissions.entity_type', '=', $tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn'])
|
||||
->where('joint_permissions.action', '=', $action)
|
||||
->whereIn('joint_permissions.role_id', $this->getCurrentUserRoles())
|
||||
->where(function (QueryBuilder $query) {
|
||||
$this->addJointHasPermissionCheck($query, $this->currentUser()->id);
|
||||
});
|
||||
})->where(function ($query) use ($tableDetails, $pageMorphClass) {
|
||||
/** @var Builder $query */
|
||||
$query->where($tableDetails['entityTypeColumn'], '!=', $pageMorphClass)
|
||||
->orWhereExists(function (QueryBuilder $query) use ($tableDetails, $pageMorphClass) {
|
||||
$query->select('id')->from('pages')
|
||||
->whereColumn('pages.id', '=', $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
|
||||
->where($tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn'], '=', $pageMorphClass)
|
||||
->where('pages.draft', '=', false);
|
||||
});
|
||||
});
|
||||
|
||||
$this->clean();
|
||||
|
||||
return $q;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add conditions to a query to filter the selection to related entities
|
||||
* where view permissions are granted.
|
||||
*/
|
||||
public function filterRelatedEntity(string $entityClass, Builder $query, string $tableName, string $entityIdColumn): Builder
|
||||
{
|
||||
$fullEntityIdColumn = $tableName . '.' . $entityIdColumn;
|
||||
$instance = new $entityClass();
|
||||
$morphClass = $instance->getMorphClass();
|
||||
|
||||
$existsQuery = function ($permissionQuery) use ($fullEntityIdColumn, $morphClass) {
|
||||
/** @var Builder $permissionQuery */
|
||||
$permissionQuery->select('joint_permissions.role_id')->from('joint_permissions')
|
||||
->whereColumn('joint_permissions.entity_id', '=', $fullEntityIdColumn)
|
||||
->where('joint_permissions.entity_type', '=', $morphClass)
|
||||
->where('joint_permissions.action', '=', 'view')
|
||||
->whereIn('joint_permissions.role_id', $this->getCurrentUserRoles())
|
||||
->where(function (QueryBuilder $query) {
|
||||
$this->addJointHasPermissionCheck($query, $this->currentUser()->id);
|
||||
});
|
||||
};
|
||||
|
||||
$q = $query->where(function ($query) use ($existsQuery, $fullEntityIdColumn) {
|
||||
$query->whereExists($existsQuery)
|
||||
->orWhere($fullEntityIdColumn, '=', 0);
|
||||
});
|
||||
|
||||
if ($instance instanceof Page) {
|
||||
// Prevent visibility of non-owned draft pages
|
||||
$q->whereExists(function (QueryBuilder $query) use ($fullEntityIdColumn) {
|
||||
$query->select('id')->from('pages')
|
||||
->whereColumn('pages.id', '=', $fullEntityIdColumn)
|
||||
->where(function (QueryBuilder $query) {
|
||||
$query->where('pages.draft', '=', false)
|
||||
->orWhere('pages.owned_by', '=', $this->currentUser()->id);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$this->clean();
|
||||
|
||||
return $q;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the query for checking the given user id has permission
|
||||
* within the join_permissions table.
|
||||
*
|
||||
* @param QueryBuilder|Builder $query
|
||||
*/
|
||||
protected function addJointHasPermissionCheck($query, int $userIdToCheck)
|
||||
{
|
||||
$query->where('joint_permissions.has_permission', '=', true)->orWhere(function ($query) use ($userIdToCheck) {
|
||||
$query->where('joint_permissions.has_permission_own', '=', true)
|
||||
->where('joint_permissions.owned_by', '=', $userIdToCheck);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current user.
|
||||
*/
|
||||
private function currentUser(): User
|
||||
{
|
||||
if (is_null($this->currentUserModel)) {
|
||||
$this->currentUserModel = user();
|
||||
}
|
||||
|
||||
return $this->currentUserModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean the cached user elements.
|
||||
*/
|
||||
private function clean(): void
|
||||
{
|
||||
$this->currentUserModel = null;
|
||||
$this->userRoles = null;
|
||||
}
|
||||
}
|
||||
@@ -11,15 +11,20 @@ use Illuminate\Database\Eloquent\Collection;
|
||||
|
||||
class PermissionsRepo
|
||||
{
|
||||
protected JointPermissionBuilder $permissionBuilder;
|
||||
protected $permission;
|
||||
protected $role;
|
||||
protected $permissionService;
|
||||
|
||||
protected $systemRoles = ['admin', 'public'];
|
||||
|
||||
/**
|
||||
* PermissionsRepo constructor.
|
||||
*/
|
||||
public function __construct(JointPermissionBuilder $permissionBuilder)
|
||||
public function __construct(RolePermission $permission, Role $role, PermissionService $permissionService)
|
||||
{
|
||||
$this->permissionBuilder = $permissionBuilder;
|
||||
$this->permission = $permission;
|
||||
$this->role = $role;
|
||||
$this->permissionService = $permissionService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -27,7 +32,7 @@ class PermissionsRepo
|
||||
*/
|
||||
public function getAllRoles(): Collection
|
||||
{
|
||||
return Role::query()->get();
|
||||
return $this->role->all();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -35,7 +40,7 @@ class PermissionsRepo
|
||||
*/
|
||||
public function getAllRolesExcept(Role $role): Collection
|
||||
{
|
||||
return Role::query()->where('id', '!=', $role->id)->get();
|
||||
return $this->role->where('id', '!=', $role->id)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -43,7 +48,7 @@ class PermissionsRepo
|
||||
*/
|
||||
public function getRoleById($id): Role
|
||||
{
|
||||
return Role::query()->findOrFail($id);
|
||||
return $this->role->newQuery()->findOrFail($id);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,14 +56,13 @@ class PermissionsRepo
|
||||
*/
|
||||
public function saveNewRole(array $roleData): Role
|
||||
{
|
||||
$role = new Role($roleData);
|
||||
$role = $this->role->newInstance($roleData);
|
||||
$role->mfa_enforced = ($roleData['mfa_enforced'] ?? 'false') === 'true';
|
||||
$role->save();
|
||||
|
||||
$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
|
||||
$this->assignRolePermissions($role, $permissions);
|
||||
$this->permissionBuilder->rebuildForRole($role);
|
||||
|
||||
$this->permissionService->buildJointPermissionForRole($role);
|
||||
Activity::add(ActivityType::ROLE_CREATE, $role);
|
||||
|
||||
return $role;
|
||||
@@ -70,7 +74,8 @@ class PermissionsRepo
|
||||
*/
|
||||
public function updateRole($roleId, array $roleData)
|
||||
{
|
||||
$role = $this->getRoleById($roleId);
|
||||
/** @var Role $role */
|
||||
$role = $this->role->newQuery()->findOrFail($roleId);
|
||||
|
||||
$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
|
||||
if ($role->system_name === 'admin') {
|
||||
@@ -88,13 +93,12 @@ class PermissionsRepo
|
||||
$role->fill($roleData);
|
||||
$role->mfa_enforced = ($roleData['mfa_enforced'] ?? 'false') === 'true';
|
||||
$role->save();
|
||||
$this->permissionBuilder->rebuildForRole($role);
|
||||
|
||||
$this->permissionService->buildJointPermissionForRole($role);
|
||||
Activity::add(ActivityType::ROLE_UPDATE, $role);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign a list of permission names to a role.
|
||||
* Assign an list of permission names to an role.
|
||||
*/
|
||||
protected function assignRolePermissions(Role $role, array $permissionNameArray = [])
|
||||
{
|
||||
@@ -102,7 +106,7 @@ class PermissionsRepo
|
||||
$permissionNameArray = array_values($permissionNameArray);
|
||||
|
||||
if ($permissionNameArray) {
|
||||
$permissions = RolePermission::query()
|
||||
$permissions = $this->permission->newQuery()
|
||||
->whereIn('name', $permissionNameArray)
|
||||
->pluck('id')
|
||||
->toArray();
|
||||
@@ -122,7 +126,8 @@ class PermissionsRepo
|
||||
*/
|
||||
public function deleteRole($roleId, $migrateRoleId)
|
||||
{
|
||||
$role = $this->getRoleById($roleId);
|
||||
/** @var Role $role */
|
||||
$role = $this->role->newQuery()->findOrFail($roleId);
|
||||
|
||||
// Prevent deleting admin role or default registration role.
|
||||
if ($role->system_name && in_array($role->system_name, $this->systemRoles)) {
|
||||
@@ -132,14 +137,14 @@ class PermissionsRepo
|
||||
}
|
||||
|
||||
if ($migrateRoleId) {
|
||||
$newRole = Role::query()->find($migrateRoleId);
|
||||
$newRole = $this->role->newQuery()->find($migrateRoleId);
|
||||
if ($newRole) {
|
||||
$users = $role->users()->pluck('id')->toArray();
|
||||
$newRole->users()->sync($users);
|
||||
}
|
||||
}
|
||||
|
||||
$role->jointPermissions()->delete();
|
||||
$this->permissionService->deleteJointPermissionsForRole($role);
|
||||
Activity::add(ActivityType::ROLE_DELETE, $role);
|
||||
$role->delete();
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Auth\Permissions;
|
||||
|
||||
class SimpleEntityData
|
||||
{
|
||||
public int $id;
|
||||
public string $type;
|
||||
public bool $restricted;
|
||||
public int $owned_by;
|
||||
public ?int $book_id;
|
||||
public ?int $chapter_id;
|
||||
}
|
||||
@@ -80,11 +80,6 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
*/
|
||||
protected ?Collection $permissions;
|
||||
|
||||
/**
|
||||
* This holds the user's avatar URL when loaded to prevent re-calculating within the same request.
|
||||
*/
|
||||
protected string $avatarUrl = '';
|
||||
|
||||
/**
|
||||
* This holds the default user when loaded.
|
||||
*
|
||||
@@ -168,7 +163,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all permissions belonging to the current user.
|
||||
* Get all permissions belonging to a the current user.
|
||||
*/
|
||||
protected function permissions(): Collection
|
||||
{
|
||||
@@ -238,17 +233,12 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
return $default;
|
||||
}
|
||||
|
||||
if (!empty($this->avatarUrl)) {
|
||||
return $this->avatarUrl;
|
||||
}
|
||||
|
||||
try {
|
||||
$avatar = $this->avatar ? url($this->avatar->getThumb($size, $size, false)) : $default;
|
||||
} catch (Exception $err) {
|
||||
$avatar = $default;
|
||||
}
|
||||
|
||||
$this->avatarUrl = $avatar;
|
||||
return $avatar;
|
||||
}
|
||||
|
||||
|
||||
@@ -64,10 +64,6 @@ return [
|
||||
// Current host and source for the "DRAWIO" setting will be auto-appended to the sources configured.
|
||||
'iframe_sources' => env('ALLOWED_IFRAME_SOURCES', 'https://*.draw.io https://*.youtube.com https://*.youtube-nocookie.com https://*.vimeo.com'),
|
||||
|
||||
// Alter the precision of IP addresses stored by BookStack.
|
||||
// Integer value between 0 (IP hidden) to 4 (Full IP usage)
|
||||
'ip_address_precision' => env('IP_ADDRESS_PRECISION', 4),
|
||||
|
||||
// Application timezone for back-end date functions.
|
||||
'timezone' => env('APP_TIMEZONE', 'UTC'),
|
||||
|
||||
@@ -75,7 +71,7 @@ return [
|
||||
'locale' => env('APP_LANG', 'en'),
|
||||
|
||||
// Locales available
|
||||
'locales' => ['en', 'ar', 'bg', 'bs', 'ca', 'cs', 'cy', 'da', 'de', 'de_informal', 'es', 'es_AR', 'et', 'eu', 'fa', 'fr', 'he', 'hr', 'hu', 'id', 'it', 'ja', 'ko', 'lt', 'lv', 'nl', 'nb', 'pt', 'pt_BR', 'sk', 'sl', 'sv', 'pl', 'ru', 'th', 'tr', 'uk', 'uz', 'vi', 'zh_CN', 'zh_TW'],
|
||||
'locales' => ['en', 'ar', 'bg', 'bs', 'ca', 'cs', 'da', 'de', 'de_informal', 'es', 'es_AR', 'et', 'eu', 'fa', 'fr', 'he', 'hr', 'hu', 'id', 'it', 'ja', 'ko', 'lt', 'lv', 'nl', 'nb', 'pt', 'pt_BR', 'sk', 'sl', 'sv', 'pl', 'ru', 'th', 'tr', 'uk', 'uz', 'vi', 'zh_CN', 'zh_TW'],
|
||||
|
||||
// Application Fallback Locale
|
||||
'fallback_locale' => 'en',
|
||||
@@ -201,9 +197,12 @@ return [
|
||||
|
||||
// Third Party
|
||||
'ImageTool' => Intervention\Image\Facades\Image::class,
|
||||
'DomPDF' => Barryvdh\DomPDF\Facade::class,
|
||||
'SnappyPDF' => Barryvdh\Snappy\Facades\SnappyPdf::class,
|
||||
|
||||
// Custom BookStack
|
||||
'Activity' => BookStack\Facades\Activity::class,
|
||||
'Permissions' => BookStack\Facades\Permissions::class,
|
||||
'Theme' => BookStack\Facades\Theme::class,
|
||||
],
|
||||
|
||||
|
||||
@@ -13,10 +13,6 @@ return [
|
||||
// Options: standard, ldap, saml2, oidc
|
||||
'method' => env('AUTH_METHOD', 'standard'),
|
||||
|
||||
// Automatically initiate login via external auth system if it's the sole auth method.
|
||||
// Works with saml2 or oidc auth methods.
|
||||
'auto_initiate' => env('AUTH_AUTO_INITIATE', false),
|
||||
|
||||
// Authentication Defaults
|
||||
// This option controls the default authentication "guard" and password
|
||||
// reset options for your application.
|
||||
|
||||
@@ -15,8 +15,8 @@ $dompdfPaperSizeMap = [
|
||||
return [
|
||||
|
||||
'show_warnings' => false, // Throw an Exception on warnings from dompdf
|
||||
|
||||
'options' => [
|
||||
'orientation' => 'portrait',
|
||||
'defines' => [
|
||||
/**
|
||||
* The location of the DOMPDF font directory.
|
||||
*
|
||||
@@ -77,25 +77,15 @@ return [
|
||||
'chroot' => realpath(public_path()),
|
||||
|
||||
/**
|
||||
* Protocol whitelist.
|
||||
* Whether to use Unicode fonts or not.
|
||||
*
|
||||
* Protocols and PHP wrappers allowed in URIs, and the validation rules
|
||||
* that determine if a resouce may be loaded. Full support is not guaranteed
|
||||
* for the protocols/wrappers specified
|
||||
* by this array.
|
||||
* When set to true the PDF backend must be set to "CPDF" and fonts must be
|
||||
* loaded via load_font.php.
|
||||
*
|
||||
* @var array
|
||||
* When enabled, dompdf can support all Unicode glyphs. Any glyphs used in a
|
||||
* document must be present in your fonts, however.
|
||||
*/
|
||||
'allowed_protocols' => [
|
||||
'file://' => ['rules' => []],
|
||||
'http://' => ['rules' => []],
|
||||
'https://' => ['rules' => []],
|
||||
],
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
'log_output_file' => null,
|
||||
'unicode_enabled' => true,
|
||||
|
||||
/**
|
||||
* Whether to enable font subsetting or not.
|
||||
@@ -166,15 +156,6 @@ return [
|
||||
*/
|
||||
'default_paper_size' => $dompdfPaperSizeMap[env('EXPORT_PAGE_SIZE', 'a4')] ?? 'a4',
|
||||
|
||||
/**
|
||||
* The default paper orientation.
|
||||
*
|
||||
* The orientation of the page (portrait or landscape).
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
'default_paper_orientation' => 'portrait',
|
||||
|
||||
/**
|
||||
* The default font family.
|
||||
*
|
||||
@@ -277,13 +258,10 @@ return [
|
||||
'enable_css_float' => true,
|
||||
|
||||
/**
|
||||
* Use the HTML5 Lib parser.
|
||||
*
|
||||
* @deprecated This feature is now always on in dompdf 2.x
|
||||
*
|
||||
* @var bool
|
||||
* Use the more-than-experimental HTML5 Lib parser.
|
||||
*/
|
||||
'enable_html5_parser' => true,
|
||||
'enable_html5parser' => true,
|
||||
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
|
||||
namespace BookStack\Console\Commands;
|
||||
|
||||
use BookStack\Auth\Permissions\JointPermissionBuilder;
|
||||
use BookStack\Auth\Permissions\PermissionService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class RegeneratePermissions extends Command
|
||||
{
|
||||
@@ -22,14 +21,19 @@ class RegeneratePermissions extends Command
|
||||
*/
|
||||
protected $description = 'Regenerate all system permissions';
|
||||
|
||||
protected JointPermissionBuilder $permissionBuilder;
|
||||
/**
|
||||
* The service to handle the permission system.
|
||||
*
|
||||
* @var PermissionService
|
||||
*/
|
||||
protected $permissionService;
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*/
|
||||
public function __construct(JointPermissionBuilder $permissionBuilder)
|
||||
public function __construct(PermissionService $permissionService)
|
||||
{
|
||||
$this->permissionBuilder = $permissionBuilder;
|
||||
$this->permissionService = $permissionService;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
@@ -40,15 +44,15 @@ class RegeneratePermissions extends Command
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$connection = DB::getDefaultConnection();
|
||||
|
||||
if ($this->option('database')) {
|
||||
DB::setDefaultConnection($this->option('database'));
|
||||
$connection = \DB::getDefaultConnection();
|
||||
if ($this->option('database') !== null) {
|
||||
\DB::setDefaultConnection($this->option('database'));
|
||||
$this->permissionService->setConnection(\DB::connection($this->option('database')));
|
||||
}
|
||||
|
||||
$this->permissionBuilder->rebuildForAll();
|
||||
$this->permissionService->buildJointPermissions();
|
||||
|
||||
DB::setDefaultConnection($connection);
|
||||
\DB::setDefaultConnection($connection);
|
||||
$this->comment('Permissions regenerated');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +91,10 @@ class Bookshelf extends Entity implements HasCoverImage
|
||||
|
||||
/**
|
||||
* Check if this shelf contains the given book.
|
||||
*
|
||||
* @param Book $book
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function contains(Book $book): bool
|
||||
{
|
||||
@@ -99,6 +103,8 @@ class Bookshelf extends Entity implements HasCoverImage
|
||||
|
||||
/**
|
||||
* Add a book to the end of this shelf.
|
||||
*
|
||||
* @param Book $book
|
||||
*/
|
||||
public function appendBook(Book $book)
|
||||
{
|
||||
|
||||
@@ -9,10 +9,9 @@ use BookStack\Actions\Tag;
|
||||
use BookStack\Actions\View;
|
||||
use BookStack\Auth\Permissions\EntityPermission;
|
||||
use BookStack\Auth\Permissions\JointPermission;
|
||||
use BookStack\Auth\Permissions\JointPermissionBuilder;
|
||||
use BookStack\Auth\Permissions\PermissionApplicator;
|
||||
use BookStack\Entities\Tools\SearchIndex;
|
||||
use BookStack\Entities\Tools\SlugGenerator;
|
||||
use BookStack\Facades\Permissions;
|
||||
use BookStack\Interfaces\Deletable;
|
||||
use BookStack\Interfaces\Favouritable;
|
||||
use BookStack\Interfaces\Loggable;
|
||||
@@ -44,6 +43,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
* @property Collection $tags
|
||||
*
|
||||
* @method static Entity|Builder visible()
|
||||
* @method static Entity|Builder hasPermission(string $permission)
|
||||
* @method static Builder withLastView()
|
||||
* @method static Builder withViewCount()
|
||||
*/
|
||||
@@ -68,7 +68,15 @@ abstract class Entity extends Model implements Sluggable, Favouritable, Viewable
|
||||
*/
|
||||
public function scopeVisible(Builder $query): Builder
|
||||
{
|
||||
return app()->make(PermissionApplicator::class)->restrictEntityQuery($query);
|
||||
return $this->scopeHasPermission($query, 'view');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope the query to those entities that the current user has the given permission for.
|
||||
*/
|
||||
public function scopeHasPermission(Builder $query, string $permission)
|
||||
{
|
||||
return Permissions::restrictEntityQuery($query, $permission);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -276,7 +284,8 @@ abstract class Entity extends Model implements Sluggable, Favouritable, Viewable
|
||||
*/
|
||||
public function rebuildPermissions()
|
||||
{
|
||||
app()->make(JointPermissionBuilder::class)->rebuildForEntity(clone $this);
|
||||
/** @noinspection PhpUnhandledExceptionInspection */
|
||||
Permissions::buildJointPermissionsForEntity(clone $this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -284,7 +293,7 @@ abstract class Entity extends Model implements Sluggable, Favouritable, Viewable
|
||||
*/
|
||||
public function indexForSearch()
|
||||
{
|
||||
app()->make(SearchIndex::class)->indexEntity(clone $this);
|
||||
app(SearchIndex::class)->indexEntity(clone $this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -292,7 +301,7 @@ abstract class Entity extends Model implements Sluggable, Favouritable, Viewable
|
||||
*/
|
||||
public function refreshSlug(): string
|
||||
{
|
||||
$this->slug = app()->make(SlugGenerator::class)->generate($this);
|
||||
$this->slug = app(SlugGenerator::class)->generate($this);
|
||||
|
||||
return $this->slug;
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace BookStack\Entities\Models;
|
||||
|
||||
use BookStack\Auth\Permissions\PermissionApplicator;
|
||||
use BookStack\Entities\Tools\PageContent;
|
||||
use BookStack\Facades\Permissions;
|
||||
use BookStack\Uploads\Attachment;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
@@ -51,7 +51,7 @@ class Page extends BookChild
|
||||
*/
|
||||
public function scopeVisible(Builder $query): Builder
|
||||
{
|
||||
$query = app()->make(PermissionApplicator::class)->restrictDraftsOnPageQuery($query);
|
||||
$query = Permissions::enforceDraftVisibilityOnQuery($query);
|
||||
|
||||
return parent::scopeVisible($query);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace BookStack\Entities\Models;
|
||||
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Interfaces\Loggable;
|
||||
use BookStack\Model;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
@@ -28,7 +27,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
* @property Page $page
|
||||
* @property-read ?User $createdBy
|
||||
*/
|
||||
class PageRevision extends Model implements Loggable
|
||||
class PageRevision extends Model
|
||||
{
|
||||
protected $fillable = ['name', 'text', 'summary'];
|
||||
protected $hidden = ['html', 'markdown', 'restricted', 'text'];
|
||||
@@ -84,9 +83,4 @@ class PageRevision extends Model implements Loggable
|
||||
{
|
||||
return $type === 'revision';
|
||||
}
|
||||
|
||||
public function logDescriptor(): string
|
||||
{
|
||||
return "Revision #{$this->revision_number} (ID: {$this->id}) for page ID {$this->page_id}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
|
||||
namespace BookStack\Entities\Queries;
|
||||
|
||||
use BookStack\Auth\Permissions\PermissionApplicator;
|
||||
use BookStack\Auth\Permissions\PermissionService;
|
||||
use BookStack\Entities\EntityProvider;
|
||||
|
||||
abstract class EntityQuery
|
||||
{
|
||||
protected function permissionService(): PermissionApplicator
|
||||
protected function permissionService(): PermissionService
|
||||
{
|
||||
return app()->make(PermissionApplicator::class);
|
||||
return app()->make(PermissionService::class);
|
||||
}
|
||||
|
||||
protected function entityProvider(): EntityProvider
|
||||
|
||||
@@ -7,10 +7,10 @@ use Illuminate\Support\Facades\DB;
|
||||
|
||||
class Popular extends EntityQuery
|
||||
{
|
||||
public function run(int $count, int $page, array $filterModels = null)
|
||||
public function run(int $count, int $page, array $filterModels = null, string $action = 'view')
|
||||
{
|
||||
$query = $this->permissionService()
|
||||
->restrictEntityRelationQuery(View::query(), 'views', 'viewable_id', 'viewable_type')
|
||||
->filterRestrictedEntityRelations(View::query(), 'views', 'viewable_id', 'viewable_type', $action)
|
||||
->select('*', 'viewable_id', 'viewable_type', DB::raw('SUM(views) as view_count'))
|
||||
->groupBy('viewable_id', 'viewable_type')
|
||||
->orderBy('view_count', 'desc');
|
||||
|
||||
@@ -14,11 +14,12 @@ class RecentlyViewed extends EntityQuery
|
||||
return collect();
|
||||
}
|
||||
|
||||
$query = $this->permissionService()->restrictEntityRelationQuery(
|
||||
$query = $this->permissionService()->filterRestrictedEntityRelations(
|
||||
View::query(),
|
||||
'views',
|
||||
'viewable_id',
|
||||
'viewable_type'
|
||||
'viewable_type',
|
||||
'view'
|
||||
)
|
||||
->orderBy('views.updated_at', 'desc')
|
||||
->where('user_id', '=', user()->id);
|
||||
|
||||
@@ -15,7 +15,7 @@ class TopFavourites extends EntityQuery
|
||||
}
|
||||
|
||||
$query = $this->permissionService()
|
||||
->restrictEntityRelationQuery(Favourite::query(), 'favourites', 'favouritable_id', 'favouritable_type')
|
||||
->filterRestrictedEntityRelations(Favourite::query(), 'favourites', 'favouritable_id', 'favouritable_type', 'view')
|
||||
->select('favourites.*')
|
||||
->leftJoin('views', function (JoinClause $join) {
|
||||
$join->on('favourites.favouritable_id', '=', 'views.viewable_id');
|
||||
|
||||
@@ -38,7 +38,6 @@ class BaseRepo
|
||||
$this->tagRepo->saveTagsToEntity($entity, $input['tags']);
|
||||
}
|
||||
|
||||
$entity->refresh();
|
||||
$entity->rebuildPermissions();
|
||||
$entity->indexForSearch();
|
||||
}
|
||||
|
||||
@@ -91,7 +91,6 @@ class BookRepo
|
||||
{
|
||||
$book = new Book();
|
||||
$this->baseRepo->create($book, $input);
|
||||
$this->baseRepo->updateCoverImage($book, $input['image'] ?? null);
|
||||
Activity::add(ActivityType::BOOK_CREATE, $book);
|
||||
|
||||
return $book;
|
||||
@@ -103,11 +102,6 @@ class BookRepo
|
||||
public function update(Book $book, array $input): Book
|
||||
{
|
||||
$this->baseRepo->update($book, $input);
|
||||
|
||||
if (array_key_exists('image', $input)) {
|
||||
$this->baseRepo->updateCoverImage($book, $input['image'], $input['image'] === null);
|
||||
}
|
||||
|
||||
Activity::add(ActivityType::BOOK_UPDATE, $book);
|
||||
|
||||
return $book;
|
||||
|
||||
@@ -6,10 +6,12 @@ use BookStack\Actions\ActivityType;
|
||||
use BookStack\Entities\Models\Book;
|
||||
use BookStack\Entities\Models\Bookshelf;
|
||||
use BookStack\Entities\Tools\TrashCan;
|
||||
use BookStack\Exceptions\ImageUploadException;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use BookStack\Facades\Activity;
|
||||
use Exception;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class BookshelfRepo
|
||||
@@ -87,7 +89,6 @@ class BookshelfRepo
|
||||
{
|
||||
$shelf = new Bookshelf();
|
||||
$this->baseRepo->create($shelf, $input);
|
||||
$this->baseRepo->updateCoverImage($shelf, $input['image'] ?? null);
|
||||
$this->updateBooks($shelf, $bookIds);
|
||||
Activity::add(ActivityType::BOOKSHELF_CREATE, $shelf);
|
||||
|
||||
@@ -105,17 +106,14 @@ class BookshelfRepo
|
||||
$this->updateBooks($shelf, $bookIds);
|
||||
}
|
||||
|
||||
if (array_key_exists('image', $input)) {
|
||||
$this->baseRepo->updateCoverImage($shelf, $input['image'], $input['image'] === null);
|
||||
}
|
||||
|
||||
Activity::add(ActivityType::BOOKSHELF_UPDATE, $shelf);
|
||||
|
||||
return $shelf;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update which books are assigned to this shelf by syncing the given book ids.
|
||||
* Update which books are assigned to this shelf by
|
||||
* syncing the given book ids.
|
||||
* Function ensures the books are visible to the current user and existing.
|
||||
*/
|
||||
protected function updateBooks(Bookshelf $shelf, array $bookIds)
|
||||
@@ -134,13 +132,24 @@ class BookshelfRepo
|
||||
$shelf->books()->sync($syncData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the given shelf cover image, or clear it.
|
||||
*
|
||||
* @throws ImageUploadException
|
||||
* @throws Exception
|
||||
*/
|
||||
public function updateCoverImage(Bookshelf $shelf, ?UploadedFile $coverImage, bool $removeImage = false)
|
||||
{
|
||||
$this->baseRepo->updateCoverImage($shelf, $coverImage, $removeImage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy down the permissions of the given shelf to all child books.
|
||||
*/
|
||||
public function copyDownPermissions(Bookshelf $shelf, $checkUserPermissions = true): int
|
||||
{
|
||||
$shelfPermissions = $shelf->permissions()->get(['role_id', 'action'])->toArray();
|
||||
$shelfBooks = $shelf->books()->get(['id', 'restricted', 'owned_by']);
|
||||
$shelfBooks = $shelf->books()->get(['id', 'restricted']);
|
||||
$updatedBookCount = 0;
|
||||
|
||||
/** @var Book $book */
|
||||
|
||||
@@ -337,7 +337,6 @@ class PageRepo
|
||||
$this->savePageRevision($page, $summary);
|
||||
|
||||
Activity::add(ActivityType::PAGE_RESTORE, $page);
|
||||
Activity::add(ActivityType::REVISION_RESTORE, $revision);
|
||||
|
||||
return $page;
|
||||
}
|
||||
@@ -393,6 +392,23 @@ class PageRepo
|
||||
return $parentClass::visible()->where('id', '=', $entityId)->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the page's parent to the given entity.
|
||||
*/
|
||||
protected function changeParent(Page $page, Entity $parent)
|
||||
{
|
||||
$book = ($parent instanceof Chapter) ? $parent->book : $parent;
|
||||
$page->chapter_id = ($parent instanceof Chapter) ? $parent->id : 0;
|
||||
$page->save();
|
||||
|
||||
if ($page->book->id !== $book->id) {
|
||||
$page->changeBook($book->id);
|
||||
}
|
||||
|
||||
$page->load('book');
|
||||
$book->rebuildPermissions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a page revision to update for the given page.
|
||||
* Checks for an existing revisions before providing a fresh one.
|
||||
|
||||
@@ -16,10 +16,25 @@ use Illuminate\Http\UploadedFile;
|
||||
|
||||
class Cloner
|
||||
{
|
||||
protected PageRepo $pageRepo;
|
||||
protected ChapterRepo $chapterRepo;
|
||||
protected BookRepo $bookRepo;
|
||||
protected ImageService $imageService;
|
||||
/**
|
||||
* @var PageRepo
|
||||
*/
|
||||
protected $pageRepo;
|
||||
|
||||
/**
|
||||
* @var ChapterRepo
|
||||
*/
|
||||
protected $chapterRepo;
|
||||
|
||||
/**
|
||||
* @var BookRepo
|
||||
*/
|
||||
protected $bookRepo;
|
||||
|
||||
/**
|
||||
* @var ImageService
|
||||
*/
|
||||
protected $imageService;
|
||||
|
||||
public function __construct(PageRepo $pageRepo, ChapterRepo $chapterRepo, BookRepo $bookRepo, ImageService $imageService)
|
||||
{
|
||||
@@ -35,8 +50,11 @@ class Cloner
|
||||
public function clonePage(Page $original, Entity $parent, string $newName): Page
|
||||
{
|
||||
$copyPage = $this->pageRepo->getNewDraftPage($parent);
|
||||
$pageData = $this->entityToInputData($original);
|
||||
$pageData = $original->getAttributes();
|
||||
|
||||
// Update name & tags
|
||||
$pageData['name'] = $newName;
|
||||
$pageData['tags'] = $this->entityTagsToInputArray($original);
|
||||
|
||||
return $this->pageRepo->publishDraft($copyPage, $pageData);
|
||||
}
|
||||
@@ -47,8 +65,9 @@ class Cloner
|
||||
*/
|
||||
public function cloneChapter(Chapter $original, Book $parent, string $newName): Chapter
|
||||
{
|
||||
$chapterDetails = $this->entityToInputData($original);
|
||||
$chapterDetails = $original->getAttributes();
|
||||
$chapterDetails['name'] = $newName;
|
||||
$chapterDetails['tags'] = $this->entityTagsToInputArray($original);
|
||||
|
||||
$copyChapter = $this->chapterRepo->create($chapterDetails, $parent);
|
||||
|
||||
@@ -68,8 +87,9 @@ class Cloner
|
||||
*/
|
||||
public function cloneBook(Book $original, string $newName): Book
|
||||
{
|
||||
$bookDetails = $this->entityToInputData($original);
|
||||
$bookDetails = $original->getAttributes();
|
||||
$bookDetails['name'] = $newName;
|
||||
$bookDetails['tags'] = $this->entityTagsToInputArray($original);
|
||||
|
||||
$copyBook = $this->bookRepo->create($bookDetails);
|
||||
|
||||
@@ -84,48 +104,26 @@ class Cloner
|
||||
}
|
||||
}
|
||||
|
||||
return $copyBook;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an entity to a raw data array of input data.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function entityToInputData(Entity $entity): array
|
||||
{
|
||||
$inputData = $entity->getAttributes();
|
||||
$inputData['tags'] = $this->entityTagsToInputArray($entity);
|
||||
|
||||
// Add a cover to the data if existing on the original entity
|
||||
if ($entity->cover instanceof Image) {
|
||||
$uploadedFile = $this->imageToUploadedFile($entity->cover);
|
||||
$inputData['image'] = $uploadedFile;
|
||||
if ($original->cover) {
|
||||
try {
|
||||
$tmpImgFile = tmpfile();
|
||||
$uploadedFile = $this->imageToUploadedFile($original->cover, $tmpImgFile);
|
||||
$this->bookRepo->updateCoverImage($copyBook, $uploadedFile, false);
|
||||
} catch (\Exception $exception) {
|
||||
}
|
||||
}
|
||||
|
||||
return $inputData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the permission settings from the source entity to the target entity.
|
||||
*/
|
||||
public function copyEntityPermissions(Entity $sourceEntity, Entity $targetEntity): void
|
||||
{
|
||||
$targetEntity->restricted = $sourceEntity->restricted;
|
||||
$permissions = $sourceEntity->permissions()->get(['role_id', 'action'])->toArray();
|
||||
$targetEntity->permissions()->delete();
|
||||
$targetEntity->permissions()->createMany($permissions);
|
||||
$targetEntity->rebuildPermissions();
|
||||
return $copyBook;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an image instance to an UploadedFile instance to mimic
|
||||
* a file being uploaded.
|
||||
*/
|
||||
protected function imageToUploadedFile(Image $image): ?UploadedFile
|
||||
protected function imageToUploadedFile(Image $image, &$tmpFile): ?UploadedFile
|
||||
{
|
||||
$imgData = $this->imageService->getImageData($image);
|
||||
$tmpImgFilePath = tempnam(sys_get_temp_dir(), 'bs_cover_clone_');
|
||||
$tmpImgFilePath = stream_get_meta_data($tmpFile)['uri'];
|
||||
file_put_contents($tmpImgFilePath, $imgData);
|
||||
|
||||
return new UploadedFile($tmpImgFilePath, basename($image->path));
|
||||
|
||||
@@ -39,7 +39,7 @@ class ExportFormatter
|
||||
public function pageToContainedHtml(Page $page)
|
||||
{
|
||||
$page->html = (new PageContent($page))->render();
|
||||
$pageHtml = view('exports.page', [
|
||||
$pageHtml = view('pages.export', [
|
||||
'page' => $page,
|
||||
'format' => 'html',
|
||||
'cspContent' => $this->cspService->getCspMetaTagValue(),
|
||||
@@ -59,7 +59,7 @@ class ExportFormatter
|
||||
$pages->each(function ($page) {
|
||||
$page->html = (new PageContent($page))->render();
|
||||
});
|
||||
$html = view('exports.chapter', [
|
||||
$html = view('chapters.export', [
|
||||
'chapter' => $chapter,
|
||||
'pages' => $pages,
|
||||
'format' => 'html',
|
||||
@@ -77,7 +77,7 @@ class ExportFormatter
|
||||
public function bookToContainedHtml(Book $book)
|
||||
{
|
||||
$bookTree = (new BookContents($book))->getTree(false, true);
|
||||
$html = view('exports.book', [
|
||||
$html = view('books.export', [
|
||||
'book' => $book,
|
||||
'bookChildren' => $bookTree,
|
||||
'format' => 'html',
|
||||
@@ -95,7 +95,7 @@ class ExportFormatter
|
||||
public function pageToPdf(Page $page)
|
||||
{
|
||||
$page->html = (new PageContent($page))->render();
|
||||
$html = view('exports.page', [
|
||||
$html = view('pages.export', [
|
||||
'page' => $page,
|
||||
'format' => 'pdf',
|
||||
'engine' => $this->pdfGenerator->getActiveEngine(),
|
||||
@@ -116,7 +116,7 @@ class ExportFormatter
|
||||
$page->html = (new PageContent($page))->render();
|
||||
});
|
||||
|
||||
$html = view('exports.chapter', [
|
||||
$html = view('chapters.export', [
|
||||
'chapter' => $chapter,
|
||||
'pages' => $pages,
|
||||
'format' => 'pdf',
|
||||
@@ -134,7 +134,7 @@ class ExportFormatter
|
||||
public function bookToPdf(Book $book)
|
||||
{
|
||||
$bookTree = (new BookContents($book))->getTree(false, true);
|
||||
$html = view('exports.book', [
|
||||
$html = view('books.export', [
|
||||
'book' => $book,
|
||||
'bookChildren' => $bookTree,
|
||||
'format' => 'pdf',
|
||||
@@ -215,14 +215,16 @@ class ExportFormatter
|
||||
*/
|
||||
protected function containHtml(string $htmlContent): string
|
||||
{
|
||||
$imageTagsOutput = [];
|
||||
preg_match_all("/\<img.*?src\=(\'|\")(.*?)(\'|\").*?\>/i", $htmlContent, $imageTagsOutput);
|
||||
// Replace embed tags with images
|
||||
$htmlContent = preg_replace("/<embed (.*?)>/i", '<img $1>', $htmlContent);
|
||||
|
||||
// Replace image src with base64 encoded image strings
|
||||
// Replace image & embed src attributes with base64 encoded data strings
|
||||
$imageTagsOutput = [];
|
||||
preg_match_all("/<img .*?src=['\"](.*?)['\"].*?>/i", $htmlContent, $imageTagsOutput);
|
||||
if (isset($imageTagsOutput[0]) && count($imageTagsOutput[0]) > 0) {
|
||||
foreach ($imageTagsOutput[0] as $index => $imgMatch) {
|
||||
$oldImgTagString = $imgMatch;
|
||||
$srcString = $imageTagsOutput[2][$index];
|
||||
$srcString = $imageTagsOutput[1][$index];
|
||||
$imageEncoded = $this->imageService->imageUriToBase64($srcString);
|
||||
if ($imageEncoded === null) {
|
||||
$imageEncoded = $srcString;
|
||||
@@ -232,14 +234,13 @@ class ExportFormatter
|
||||
}
|
||||
}
|
||||
|
||||
// Replace any relative links with full system URL
|
||||
$linksOutput = [];
|
||||
preg_match_all("/\<a.*href\=(\'|\")(.*?)(\'|\").*?\>/i", $htmlContent, $linksOutput);
|
||||
|
||||
// Replace image src with base64 encoded image strings
|
||||
preg_match_all("/<a .*href=['\"](.*?)['\"].*?>/i", $htmlContent, $linksOutput);
|
||||
if (isset($linksOutput[0]) && count($linksOutput[0]) > 0) {
|
||||
foreach ($linksOutput[0] as $index => $linkMatch) {
|
||||
$oldLinkString = $linkMatch;
|
||||
$srcString = $linksOutput[2][$index];
|
||||
$srcString = $linksOutput[1][$index];
|
||||
if (strpos(trim($srcString), 'http') !== 0) {
|
||||
$newSrcString = url($srcString);
|
||||
$newLinkString = str_replace($srcString, $newSrcString, $oldLinkString);
|
||||
@@ -248,7 +249,6 @@ class ExportFormatter
|
||||
}
|
||||
}
|
||||
|
||||
// Replace any relative links with system domain
|
||||
return $htmlContent;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Entities\Tools;
|
||||
|
||||
use BookStack\Actions\ActivityType;
|
||||
use BookStack\Entities\Models\Book;
|
||||
use BookStack\Entities\Models\Bookshelf;
|
||||
use BookStack\Entities\Models\Chapter;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Entities\Repos\BookRepo;
|
||||
use BookStack\Entities\Repos\BookshelfRepo;
|
||||
use BookStack\Facades\Activity;
|
||||
|
||||
class HierarchyTransformer
|
||||
{
|
||||
protected BookRepo $bookRepo;
|
||||
protected BookshelfRepo $shelfRepo;
|
||||
protected Cloner $cloner;
|
||||
protected TrashCan $trashCan;
|
||||
|
||||
public function __construct(BookRepo $bookRepo, BookshelfRepo $shelfRepo, Cloner $cloner, TrashCan $trashCan)
|
||||
{
|
||||
$this->bookRepo = $bookRepo;
|
||||
$this->shelfRepo = $shelfRepo;
|
||||
$this->cloner = $cloner;
|
||||
$this->trashCan = $trashCan;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a chapter into a book.
|
||||
* Does not check permissions, check before calling.
|
||||
*/
|
||||
public function transformChapterToBook(Chapter $chapter): Book
|
||||
{
|
||||
$inputData = $this->cloner->entityToInputData($chapter);
|
||||
$book = $this->bookRepo->create($inputData);
|
||||
$this->cloner->copyEntityPermissions($chapter, $book);
|
||||
|
||||
/** @var Page $page */
|
||||
foreach ($chapter->pages as $page) {
|
||||
$page->chapter_id = 0;
|
||||
$page->changeBook($book->id);
|
||||
}
|
||||
|
||||
$this->trashCan->destroyEntity($chapter);
|
||||
|
||||
Activity::add(ActivityType::BOOK_CREATE_FROM_CHAPTER, $book);
|
||||
|
||||
return $book;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a book into a shelf.
|
||||
* Does not check permissions, check before calling.
|
||||
*/
|
||||
public function transformBookToShelf(Book $book): Bookshelf
|
||||
{
|
||||
$inputData = $this->cloner->entityToInputData($book);
|
||||
$shelf = $this->shelfRepo->create($inputData, []);
|
||||
$this->cloner->copyEntityPermissions($book, $shelf);
|
||||
|
||||
$shelfBookSyncData = [];
|
||||
|
||||
/** @var Chapter $chapter */
|
||||
foreach ($book->chapters as $index => $chapter) {
|
||||
$newBook = $this->transformChapterToBook($chapter);
|
||||
$shelfBookSyncData[$newBook->id] = ['order' => $index];
|
||||
if (!$newBook->restricted) {
|
||||
$this->cloner->copyEntityPermissions($shelf, $newBook);
|
||||
}
|
||||
}
|
||||
|
||||
if ($book->directPages->count() > 0) {
|
||||
$book->name .= ' ' . trans('entities.pages');
|
||||
$shelfBookSyncData[$book->id] = ['order' => count($shelfBookSyncData) + 1];
|
||||
$book->save();
|
||||
} else {
|
||||
$this->trashCan->destroyEntity($book);
|
||||
}
|
||||
|
||||
$shelf->books()->sync($shelfBookSyncData);
|
||||
|
||||
Activity::add(ActivityType::BOOKSHELF_CREATE_FROM_BOOK, $shelf);
|
||||
|
||||
return $shelf;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace BookStack\Entities\Tools;
|
||||
|
||||
use Barryvdh\DomPDF\Facade\Pdf as DomPDF;
|
||||
use Barryvdh\DomPDF\Facade as DomPDF;
|
||||
use Barryvdh\Snappy\Facades\SnappyPdf;
|
||||
|
||||
class PdfGenerator
|
||||
|
||||
@@ -147,8 +147,6 @@ class SearchIndex
|
||||
];
|
||||
|
||||
$html = '<body>' . $html . '</body>';
|
||||
$html = str_ireplace(['<br>', '<br />', '<br/>'], "\n", $html);
|
||||
|
||||
libxml_use_internal_errors(true);
|
||||
$doc = new DOMDocument();
|
||||
$doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace BookStack\Entities\Tools;
|
||||
|
||||
use BookStack\Auth\Permissions\PermissionApplicator;
|
||||
use BookStack\Auth\Permissions\PermissionService;
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Entities\EntityProvider;
|
||||
use BookStack\Entities\Models\BookChild;
|
||||
@@ -21,13 +21,20 @@ use SplObjectStorage;
|
||||
|
||||
class SearchRunner
|
||||
{
|
||||
protected EntityProvider $entityProvider;
|
||||
protected PermissionApplicator $permissions;
|
||||
/**
|
||||
* @var EntityProvider
|
||||
*/
|
||||
protected $entityProvider;
|
||||
|
||||
/**
|
||||
* @var PermissionService
|
||||
*/
|
||||
protected $permissionService;
|
||||
|
||||
/**
|
||||
* Acceptable operators to be used in a query.
|
||||
*
|
||||
* @var string[]
|
||||
* @var array
|
||||
*/
|
||||
protected $queryOperators = ['<=', '>=', '=', '<', '>', 'like', '!='];
|
||||
|
||||
@@ -39,10 +46,10 @@ class SearchRunner
|
||||
*/
|
||||
protected $termAdjustmentCache;
|
||||
|
||||
public function __construct(EntityProvider $entityProvider, PermissionApplicator $permissions)
|
||||
public function __construct(EntityProvider $entityProvider, PermissionService $permissionService)
|
||||
{
|
||||
$this->entityProvider = $entityProvider;
|
||||
$this->permissions = $permissions;
|
||||
$this->permissionService = $permissionService;
|
||||
$this->termAdjustmentCache = new SplObjectStorage();
|
||||
}
|
||||
|
||||
@@ -53,7 +60,7 @@ class SearchRunner
|
||||
*
|
||||
* @return array{total: int, count: int, has_more: bool, results: Entity[]}
|
||||
*/
|
||||
public function searchEntities(SearchOptions $searchOpts, string $entityType = 'all', int $page = 1, int $count = 20): array
|
||||
public function searchEntities(SearchOptions $searchOpts, string $entityType = 'all', int $page = 1, int $count = 20, string $action = 'view'): array
|
||||
{
|
||||
$entityTypes = array_keys($this->entityProvider->all());
|
||||
$entityTypesToSearch = $entityTypes;
|
||||
@@ -74,7 +81,7 @@ class SearchRunner
|
||||
}
|
||||
|
||||
$entityModelInstance = $this->entityProvider->get($entityType);
|
||||
$searchQuery = $this->buildQuery($searchOpts, $entityModelInstance);
|
||||
$searchQuery = $this->buildQuery($searchOpts, $entityModelInstance, $action);
|
||||
$entityTotal = $searchQuery->count();
|
||||
$searchResults = $this->getPageOfDataFromQuery($searchQuery, $entityModelInstance, $page, $count);
|
||||
|
||||
@@ -158,12 +165,12 @@ class SearchRunner
|
||||
/**
|
||||
* Create a search query for an entity.
|
||||
*/
|
||||
protected function buildQuery(SearchOptions $searchOpts, Entity $entityModelInstance): EloquentBuilder
|
||||
protected function buildQuery(SearchOptions $searchOpts, Entity $entityModelInstance, string $action = 'view'): EloquentBuilder
|
||||
{
|
||||
$entityQuery = $entityModelInstance->newQuery()->scopes('visible');
|
||||
$entityQuery = $entityModelInstance->newQuery();
|
||||
|
||||
if ($entityModelInstance instanceof Page) {
|
||||
$entityQuery->select(array_merge($entityModelInstance::$listAttributes, ['restricted', 'owned_by']));
|
||||
$entityQuery->select($entityModelInstance::$listAttributes);
|
||||
} else {
|
||||
$entityQuery->select(['*']);
|
||||
}
|
||||
@@ -192,7 +199,7 @@ class SearchRunner
|
||||
}
|
||||
}
|
||||
|
||||
return $entityQuery;
|
||||
return $this->permissionService->enforceEntityRestrictions($entityModelInstance, $entityQuery, $action);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,7 +20,6 @@ class ShelfContext
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @var Bookshelf $shelf */
|
||||
$shelf = Bookshelf::visible()->find($contextBookshelfId);
|
||||
$shelfContainsBook = $shelf && $shelf->contains($book);
|
||||
|
||||
|
||||
@@ -344,7 +344,7 @@ class TrashCan
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function destroyEntity(Entity $entity): int
|
||||
protected function destroyEntity(Entity $entity): int
|
||||
{
|
||||
if ($entity instanceof Page) {
|
||||
return $this->destroyPage($entity);
|
||||
|
||||
@@ -21,7 +21,6 @@ class Handler extends ExceptionHandler
|
||||
*/
|
||||
protected $dontReport = [
|
||||
NotFoundException::class,
|
||||
StoppedAuthenticationException::class,
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace BookStack\Facades;
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
|
||||
/**
|
||||
* @mixin \BookStack\Actions\ActivityLogger
|
||||
* @see \BookStack\Actions\ActivityLogger
|
||||
*/
|
||||
class Activity extends Facade
|
||||
{
|
||||
|
||||
18
app/Facades/Permissions.php
Normal file
18
app/Facades/Permissions.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Facades;
|
||||
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
|
||||
class Permissions extends Facade
|
||||
{
|
||||
/**
|
||||
* Get the registered name of the component.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected static function getFacadeAccessor()
|
||||
{
|
||||
return 'permissions';
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,19 @@ class BookApiController extends ApiController
|
||||
{
|
||||
protected $bookRepo;
|
||||
|
||||
protected $rules = [
|
||||
'create' => [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'description' => ['string', 'max:1000'],
|
||||
'tags' => ['array'],
|
||||
],
|
||||
'update' => [
|
||||
'name' => ['string', 'min:1', 'max:255'],
|
||||
'description' => ['string', 'max:1000'],
|
||||
'tags' => ['array'],
|
||||
],
|
||||
];
|
||||
|
||||
public function __construct(BookRepo $bookRepo)
|
||||
{
|
||||
$this->bookRepo = $bookRepo;
|
||||
@@ -24,21 +37,19 @@ class BookApiController extends ApiController
|
||||
$books = Book::visible();
|
||||
|
||||
return $this->apiListingResponse($books, [
|
||||
'id', 'name', 'slug', 'description', 'created_at', 'updated_at', 'created_by', 'updated_by', 'owned_by',
|
||||
'id', 'name', 'slug', 'description', 'created_at', 'updated_at', 'created_by', 'updated_by', 'owned_by', 'image_id',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new book in the system.
|
||||
* The cover image of a book can be set by sending a file via an 'image' property within a 'multipart/form-data' request.
|
||||
* If the 'image' property is null then the book cover image will be removed.
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function create(Request $request)
|
||||
{
|
||||
$this->checkPermission('book-create-all');
|
||||
$requestData = $this->validate($request, $this->rules()['create']);
|
||||
$requestData = $this->validate($request, $this->rules['create']);
|
||||
|
||||
$book = $this->bookRepo->create($requestData);
|
||||
|
||||
@@ -57,8 +68,6 @@ class BookApiController extends ApiController
|
||||
|
||||
/**
|
||||
* Update the details of a single book.
|
||||
* The cover image of a book can be set by sending a file via an 'image' property within a 'multipart/form-data' request.
|
||||
* If the 'image' property is null then the book cover image will be removed.
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
@@ -67,7 +76,7 @@ class BookApiController extends ApiController
|
||||
$book = Book::visible()->findOrFail($id);
|
||||
$this->checkOwnablePermission('book-update', $book);
|
||||
|
||||
$requestData = $this->validate($request, $this->rules()['update']);
|
||||
$requestData = $this->validate($request, $this->rules['update']);
|
||||
$book = $this->bookRepo->update($book, $requestData);
|
||||
|
||||
return response()->json($book);
|
||||
@@ -88,22 +97,4 @@ class BookApiController extends ApiController
|
||||
|
||||
return response('', 204);
|
||||
}
|
||||
|
||||
protected function rules(): array
|
||||
{
|
||||
return [
|
||||
'create' => [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'description' => ['string', 'max:1000'],
|
||||
'tags' => ['array'],
|
||||
'image' => array_merge(['nullable'], $this->getImageValidationRules()),
|
||||
],
|
||||
'update' => [
|
||||
'name' => ['string', 'min:1', 'max:255'],
|
||||
'description' => ['string', 'max:1000'],
|
||||
'tags' => ['array'],
|
||||
'image' => array_merge(['nullable'], $this->getImageValidationRules()),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ class BookExportApiController extends ApiController
|
||||
$book = Book::visible()->findOrFail($id);
|
||||
$pdfContent = $this->exportFormatter->bookToPdf($book);
|
||||
|
||||
return $this->download()->directly($pdfContent, $book->slug . '.pdf');
|
||||
return $this->downloadResponse($pdfContent, $book->slug . '.pdf');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,7 +39,7 @@ class BookExportApiController extends ApiController
|
||||
$book = Book::visible()->findOrFail($id);
|
||||
$htmlContent = $this->exportFormatter->bookToContainedHtml($book);
|
||||
|
||||
return $this->download()->directly($htmlContent, $book->slug . '.html');
|
||||
return $this->downloadResponse($htmlContent, $book->slug . '.html');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -50,7 +50,7 @@ class BookExportApiController extends ApiController
|
||||
$book = Book::visible()->findOrFail($id);
|
||||
$textContent = $this->exportFormatter->bookToPlainText($book);
|
||||
|
||||
return $this->download()->directly($textContent, $book->slug . '.txt');
|
||||
return $this->downloadResponse($textContent, $book->slug . '.txt');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -61,6 +61,6 @@ class BookExportApiController extends ApiController
|
||||
$book = Book::visible()->findOrFail($id);
|
||||
$markdown = $this->exportFormatter->bookToMarkdown($book);
|
||||
|
||||
return $this->download()->directly($markdown, $book->slug . '.md');
|
||||
return $this->downloadResponse($markdown, $book->slug . '.md');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,21 @@ class BookshelfApiController extends ApiController
|
||||
{
|
||||
protected BookshelfRepo $bookshelfRepo;
|
||||
|
||||
protected $rules = [
|
||||
'create' => [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'description' => ['string', 'max:1000'],
|
||||
'books' => ['array'],
|
||||
'tags' => ['array'],
|
||||
],
|
||||
'update' => [
|
||||
'name' => ['string', 'min:1', 'max:255'],
|
||||
'description' => ['string', 'max:1000'],
|
||||
'books' => ['array'],
|
||||
'tags' => ['array'],
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* BookshelfApiController constructor.
|
||||
*/
|
||||
@@ -29,7 +44,7 @@ class BookshelfApiController extends ApiController
|
||||
$shelves = Bookshelf::visible();
|
||||
|
||||
return $this->apiListingResponse($shelves, [
|
||||
'id', 'name', 'slug', 'description', 'created_at', 'updated_at', 'created_by', 'updated_by', 'owned_by',
|
||||
'id', 'name', 'slug', 'description', 'created_at', 'updated_at', 'created_by', 'updated_by', 'owned_by', 'image_id',
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -37,15 +52,13 @@ class BookshelfApiController extends ApiController
|
||||
* Create a new shelf in the system.
|
||||
* An array of books IDs can be provided in the request. These
|
||||
* will be added to the shelf in the same order as provided.
|
||||
* The cover image of a shelf can be set by sending a file via an 'image' property within a 'multipart/form-data' request.
|
||||
* If the 'image' property is null then the shelf cover image will be removed.
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function create(Request $request)
|
||||
{
|
||||
$this->checkPermission('bookshelf-create-all');
|
||||
$requestData = $this->validate($request, $this->rules()['create']);
|
||||
$requestData = $this->validate($request, $this->rules['create']);
|
||||
|
||||
$bookIds = $request->get('books', []);
|
||||
$shelf = $this->bookshelfRepo->create($requestData, $bookIds);
|
||||
@@ -73,8 +86,6 @@ class BookshelfApiController extends ApiController
|
||||
* An array of books IDs can be provided in the request. These
|
||||
* will be added to the shelf in the same order as provided and overwrite
|
||||
* any existing book assignments.
|
||||
* The cover image of a shelf can be set by sending a file via an 'image' property within a 'multipart/form-data' request.
|
||||
* If the 'image' property is null then the shelf cover image will be removed.
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
@@ -83,7 +94,7 @@ class BookshelfApiController extends ApiController
|
||||
$shelf = Bookshelf::visible()->findOrFail($id);
|
||||
$this->checkOwnablePermission('bookshelf-update', $shelf);
|
||||
|
||||
$requestData = $this->validate($request, $this->rules()['update']);
|
||||
$requestData = $this->validate($request, $this->rules['update']);
|
||||
$bookIds = $request->get('books', null);
|
||||
|
||||
$shelf = $this->bookshelfRepo->update($shelf, $requestData, $bookIds);
|
||||
@@ -106,24 +117,4 @@ class BookshelfApiController extends ApiController
|
||||
|
||||
return response('', 204);
|
||||
}
|
||||
|
||||
protected function rules(): array
|
||||
{
|
||||
return [
|
||||
'create' => [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'description' => ['string', 'max:1000'],
|
||||
'books' => ['array'],
|
||||
'tags' => ['array'],
|
||||
'image' => array_merge(['nullable'], $this->getImageValidationRules()),
|
||||
],
|
||||
'update' => [
|
||||
'name' => ['string', 'min:1', 'max:255'],
|
||||
'description' => ['string', 'max:1000'],
|
||||
'books' => ['array'],
|
||||
'tags' => ['array'],
|
||||
'image' => array_merge(['nullable'], $this->getImageValidationRules()),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ class ChapterExportApiController extends ApiController
|
||||
$chapter = Chapter::visible()->findOrFail($id);
|
||||
$pdfContent = $this->exportFormatter->chapterToPdf($chapter);
|
||||
|
||||
return $this->download()->directly($pdfContent, $chapter->slug . '.pdf');
|
||||
return $this->downloadResponse($pdfContent, $chapter->slug . '.pdf');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -42,7 +42,7 @@ class ChapterExportApiController extends ApiController
|
||||
$chapter = Chapter::visible()->findOrFail($id);
|
||||
$htmlContent = $this->exportFormatter->chapterToContainedHtml($chapter);
|
||||
|
||||
return $this->download()->directly($htmlContent, $chapter->slug . '.html');
|
||||
return $this->downloadResponse($htmlContent, $chapter->slug . '.html');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,7 +53,7 @@ class ChapterExportApiController extends ApiController
|
||||
$chapter = Chapter::visible()->findOrFail($id);
|
||||
$textContent = $this->exportFormatter->chapterToPlainText($chapter);
|
||||
|
||||
return $this->download()->directly($textContent, $chapter->slug . '.txt');
|
||||
return $this->downloadResponse($textContent, $chapter->slug . '.txt');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -64,6 +64,6 @@ class ChapterExportApiController extends ApiController
|
||||
$chapter = Chapter::visible()->findOrFail($id);
|
||||
$markdown = $this->exportFormatter->chapterToMarkdown($chapter);
|
||||
|
||||
return $this->download()->directly($markdown, $chapter->slug . '.md');
|
||||
return $this->downloadResponse($markdown, $chapter->slug . '.md');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,9 +86,6 @@ class PageApiController extends ApiController
|
||||
*
|
||||
* Pages will always have HTML content. They may have markdown content
|
||||
* if the markdown editor was used to last update the page.
|
||||
*
|
||||
* See the "Content Security" section of these docs for security considerations when using
|
||||
* the page content returned from this endpoint.
|
||||
*/
|
||||
public function read(string $id)
|
||||
{
|
||||
|
||||
@@ -26,7 +26,7 @@ class PageExportApiController extends ApiController
|
||||
$page = Page::visible()->findOrFail($id);
|
||||
$pdfContent = $this->exportFormatter->pageToPdf($page);
|
||||
|
||||
return $this->download()->directly($pdfContent, $page->slug . '.pdf');
|
||||
return $this->downloadResponse($pdfContent, $page->slug . '.pdf');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,7 +39,7 @@ class PageExportApiController extends ApiController
|
||||
$page = Page::visible()->findOrFail($id);
|
||||
$htmlContent = $this->exportFormatter->pageToContainedHtml($page);
|
||||
|
||||
return $this->download()->directly($htmlContent, $page->slug . '.html');
|
||||
return $this->downloadResponse($htmlContent, $page->slug . '.html');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -50,7 +50,7 @@ class PageExportApiController extends ApiController
|
||||
$page = Page::visible()->findOrFail($id);
|
||||
$textContent = $this->exportFormatter->pageToPlainText($page);
|
||||
|
||||
return $this->download()->directly($textContent, $page->slug . '.txt');
|
||||
return $this->downloadResponse($textContent, $page->slug . '.txt');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -61,6 +61,6 @@ class PageExportApiController extends ApiController
|
||||
$page = Page::visible()->findOrFail($id);
|
||||
$markdown = $this->exportFormatter->pageToMarkdown($page);
|
||||
|
||||
return $this->download()->directly($markdown, $page->slug . '.md');
|
||||
return $this->downloadResponse($markdown, $page->slug . '.md');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,26 +36,26 @@ class UserApiController extends ApiController
|
||||
{
|
||||
return [
|
||||
'create' => [
|
||||
'name' => ['required', 'min:2', 'max:100'],
|
||||
'name' => ['required', 'min:2'],
|
||||
'email' => [
|
||||
'required', 'min:2', 'email', new Unique('users', 'email'),
|
||||
],
|
||||
'external_auth_id' => ['string'],
|
||||
'language' => ['string', 'max:15', 'alpha_dash'],
|
||||
'language' => ['string'],
|
||||
'password' => [Password::default()],
|
||||
'roles' => ['array'],
|
||||
'roles.*' => ['integer'],
|
||||
'send_invite' => ['boolean'],
|
||||
],
|
||||
'update' => [
|
||||
'name' => ['min:2', 'max:100'],
|
||||
'name' => ['min:2'],
|
||||
'email' => [
|
||||
'min:2',
|
||||
'email',
|
||||
(new Unique('users', 'email'))->ignore($userId ?? null),
|
||||
],
|
||||
'external_auth_id' => ['string'],
|
||||
'language' => ['string', 'max:15', 'alpha_dash'],
|
||||
'language' => ['string'],
|
||||
'password' => [Password::default()],
|
||||
'roles' => ['array'],
|
||||
'roles.*' => ['integer'],
|
||||
|
||||
@@ -233,10 +233,10 @@ class AttachmentController extends Controller
|
||||
$attachmentStream = $this->attachmentService->streamAttachmentFromStorage($attachment);
|
||||
|
||||
if ($request->get('open') === 'true') {
|
||||
return $this->download()->streamedInline($attachmentStream, $fileName);
|
||||
return $this->streamedInlineDownloadResponse($attachmentStream, $fileName);
|
||||
}
|
||||
|
||||
return $this->download()->streamedDirectly($attachmentStream, $fileName);
|
||||
return $this->streamedDownloadResponse($attachmentStream, $fileName);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -25,16 +25,17 @@ class LoginController extends Controller
|
||||
|
|
||||
*/
|
||||
|
||||
use AuthenticatesUsers { logout as traitLogout; }
|
||||
use AuthenticatesUsers;
|
||||
|
||||
/**
|
||||
* Redirection paths.
|
||||
*/
|
||||
protected $redirectTo = '/';
|
||||
protected $redirectPath = '/';
|
||||
protected $redirectAfterLogout = '/login';
|
||||
|
||||
protected SocialAuthService $socialAuthService;
|
||||
protected LoginService $loginService;
|
||||
protected $socialAuthService;
|
||||
protected $loginService;
|
||||
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
@@ -49,6 +50,7 @@ class LoginController extends Controller
|
||||
$this->loginService = $loginService;
|
||||
|
||||
$this->redirectPath = url('/');
|
||||
$this->redirectAfterLogout = url('/login');
|
||||
}
|
||||
|
||||
public function username()
|
||||
@@ -71,7 +73,6 @@ class LoginController extends Controller
|
||||
{
|
||||
$socialDrivers = $this->socialAuthService->getActiveDrivers();
|
||||
$authMethod = config('auth.method');
|
||||
$preventInitiation = $request->get('prevent_auto_init') === 'true';
|
||||
|
||||
if ($request->has('email')) {
|
||||
session()->flashInput([
|
||||
@@ -83,12 +84,6 @@ class LoginController extends Controller
|
||||
// Store the previous location for redirect after login
|
||||
$this->updateIntendedFromPrevious();
|
||||
|
||||
if (!$preventInitiation && $this->shouldAutoInitiate()) {
|
||||
return view('auth.login-initiate', [
|
||||
'authMethod' => $authMethod,
|
||||
]);
|
||||
}
|
||||
|
||||
return view('auth.login', [
|
||||
'socialDrivers' => $socialDrivers,
|
||||
'authMethod' => $authMethod,
|
||||
@@ -256,32 +251,4 @@ class LoginController extends Controller
|
||||
|
||||
redirect()->setIntendedUrl($previous);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if login auto-initiate should be valid based upon authentication config.
|
||||
*/
|
||||
protected function shouldAutoInitiate(): bool
|
||||
{
|
||||
$socialDrivers = $this->socialAuthService->getActiveDrivers();
|
||||
$authMethod = config('auth.method');
|
||||
$autoRedirect = config('auth.auto_initiate');
|
||||
|
||||
return $autoRedirect && count($socialDrivers) === 0 && in_array($authMethod, ['oidc', 'saml2']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout user and perform subsequent redirect.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function logout(Request $request)
|
||||
{
|
||||
$this->traitLogout($request);
|
||||
|
||||
$redirectUri = $this->shouldAutoInitiate() ? '/login?prevent_auto_init=true' : '/';
|
||||
|
||||
return redirect($redirectUri);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,9 +30,9 @@ class RegisterController extends Controller
|
||||
|
||||
use RegistersUsers;
|
||||
|
||||
protected SocialAuthService $socialAuthService;
|
||||
protected RegistrationService $registrationService;
|
||||
protected LoginService $loginService;
|
||||
protected $socialAuthService;
|
||||
protected $registrationService;
|
||||
protected $loginService;
|
||||
|
||||
/**
|
||||
* Where to redirect users after login / registration.
|
||||
@@ -69,7 +69,7 @@ class RegisterController extends Controller
|
||||
protected function validator(array $data)
|
||||
{
|
||||
return Validator::make($data, [
|
||||
'name' => ['required', 'min:2', 'max:100'],
|
||||
'name' => ['required', 'min:2', 'max:255'],
|
||||
'email' => ['required', 'email', 'max:255', 'unique:users'],
|
||||
'password' => ['required', Password::default()],
|
||||
]);
|
||||
|
||||
@@ -9,7 +9,6 @@ use BookStack\Entities\Models\Bookshelf;
|
||||
use BookStack\Entities\Repos\BookRepo;
|
||||
use BookStack\Entities\Tools\BookContents;
|
||||
use BookStack\Entities\Tools\Cloner;
|
||||
use BookStack\Entities\Tools\HierarchyTransformer;
|
||||
use BookStack\Entities\Tools\PermissionsUpdater;
|
||||
use BookStack\Entities\Tools\ShelfContext;
|
||||
use BookStack\Exceptions\ImageUploadException;
|
||||
@@ -88,11 +87,10 @@ class BookController extends Controller
|
||||
public function store(Request $request, string $shelfSlug = null)
|
||||
{
|
||||
$this->checkPermission('book-create-all');
|
||||
$validated = $this->validate($request, [
|
||||
$this->validate($request, [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'description' => ['string', 'max:1000'],
|
||||
'image' => array_merge(['nullable'], $this->getImageValidationRules()),
|
||||
'tags' => ['array'],
|
||||
]);
|
||||
|
||||
$bookshelf = null;
|
||||
@@ -101,7 +99,8 @@ class BookController extends Controller
|
||||
$this->checkOwnablePermission('bookshelf-update', $bookshelf);
|
||||
}
|
||||
|
||||
$book = $this->bookRepo->create($validated);
|
||||
$book = $this->bookRepo->create($request->all());
|
||||
$this->bookRepo->updateCoverImage($book, $request->file('image', null));
|
||||
|
||||
if ($bookshelf) {
|
||||
$bookshelf->appendBook($book);
|
||||
@@ -159,21 +158,15 @@ class BookController extends Controller
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($slug);
|
||||
$this->checkOwnablePermission('book-update', $book);
|
||||
|
||||
$validated = $this->validate($request, [
|
||||
$this->validate($request, [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'description' => ['string', 'max:1000'],
|
||||
'image' => array_merge(['nullable'], $this->getImageValidationRules()),
|
||||
'tags' => ['array'],
|
||||
]);
|
||||
|
||||
if ($request->has('image_reset')) {
|
||||
$validated['image'] = null;
|
||||
} elseif (array_key_exists('image', $validated) && is_null($validated['image'])) {
|
||||
unset($validated['image']);
|
||||
}
|
||||
|
||||
$book = $this->bookRepo->update($book, $validated);
|
||||
$book = $this->bookRepo->update($book, $request->all());
|
||||
$resetCover = $request->has('image_reset');
|
||||
$this->bookRepo->updateCoverImage($book, $request->file('image', null), $resetCover);
|
||||
|
||||
return redirect($book->getUrl());
|
||||
}
|
||||
@@ -269,20 +262,4 @@ class BookController extends Controller
|
||||
|
||||
return redirect($bookCopy->getUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the chapter to a book.
|
||||
*/
|
||||
public function convertToShelf(HierarchyTransformer $transformer, string $bookSlug)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$this->checkOwnablePermission('book-update', $book);
|
||||
$this->checkOwnablePermission('book-delete', $book);
|
||||
$this->checkPermission('bookshelf-create-all');
|
||||
$this->checkPermission('book-create-all');
|
||||
|
||||
$shelf = $transformer->transformBookToShelf($book);
|
||||
|
||||
return redirect($shelf->getUrl());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ class BookExportController extends Controller
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$pdfContent = $this->exportFormatter->bookToPdf($book);
|
||||
|
||||
return $this->download()->directly($pdfContent, $bookSlug . '.pdf');
|
||||
return $this->downloadResponse($pdfContent, $bookSlug . '.pdf');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -44,7 +44,7 @@ class BookExportController extends Controller
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$htmlContent = $this->exportFormatter->bookToContainedHtml($book);
|
||||
|
||||
return $this->download()->directly($htmlContent, $bookSlug . '.html');
|
||||
return $this->downloadResponse($htmlContent, $bookSlug . '.html');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -55,7 +55,7 @@ class BookExportController extends Controller
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$textContent = $this->exportFormatter->bookToPlainText($book);
|
||||
|
||||
return $this->download()->directly($textContent, $bookSlug . '.txt');
|
||||
return $this->downloadResponse($textContent, $bookSlug . '.txt');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,6 +66,6 @@ class BookExportController extends Controller
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$textContent = $this->exportFormatter->bookToMarkdown($book);
|
||||
|
||||
return $this->download()->directly($textContent, $bookSlug . '.md');
|
||||
return $this->downloadResponse($textContent, $bookSlug . '.md');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,19 +10,22 @@ use BookStack\Entities\Tools\PermissionsUpdater;
|
||||
use BookStack\Entities\Tools\ShelfContext;
|
||||
use BookStack\Exceptions\ImageUploadException;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use BookStack\Uploads\ImageRepo;
|
||||
use Exception;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class BookshelfController extends Controller
|
||||
{
|
||||
protected BookshelfRepo $shelfRepo;
|
||||
protected ShelfContext $shelfContext;
|
||||
protected $bookshelfRepo;
|
||||
protected $entityContextManager;
|
||||
protected $imageRepo;
|
||||
|
||||
public function __construct(BookshelfRepo $shelfRepo, ShelfContext $shelfContext)
|
||||
public function __construct(BookshelfRepo $bookshelfRepo, ShelfContext $entityContextManager, ImageRepo $imageRepo)
|
||||
{
|
||||
$this->shelfRepo = $shelfRepo;
|
||||
$this->shelfContext = $shelfContext;
|
||||
$this->bookshelfRepo = $bookshelfRepo;
|
||||
$this->entityContextManager = $entityContextManager;
|
||||
$this->imageRepo = $imageRepo;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,12 +42,12 @@ class BookshelfController extends Controller
|
||||
'updated_at' => trans('common.sort_updated_at'),
|
||||
];
|
||||
|
||||
$shelves = $this->shelfRepo->getAllPaginated(18, $sort, $order);
|
||||
$recents = $this->isSignedIn() ? $this->shelfRepo->getRecentlyViewed(4) : false;
|
||||
$popular = $this->shelfRepo->getPopular(4);
|
||||
$new = $this->shelfRepo->getRecentlyCreated(4);
|
||||
$shelves = $this->bookshelfRepo->getAllPaginated(18, $sort, $order);
|
||||
$recents = $this->isSignedIn() ? $this->bookshelfRepo->getRecentlyViewed(4) : false;
|
||||
$popular = $this->bookshelfRepo->getPopular(4);
|
||||
$new = $this->bookshelfRepo->getRecentlyCreated(4);
|
||||
|
||||
$this->shelfContext->clearShelfContext();
|
||||
$this->entityContextManager->clearShelfContext();
|
||||
$this->setPageTitle(trans('entities.shelves'));
|
||||
|
||||
return view('shelves.index', [
|
||||
@@ -65,7 +68,7 @@ class BookshelfController extends Controller
|
||||
public function create()
|
||||
{
|
||||
$this->checkPermission('bookshelf-create-all');
|
||||
$books = Book::visible()->orderBy('name')->get(['name', 'id', 'slug']);
|
||||
$books = Book::hasPermission('update')->get();
|
||||
$this->setPageTitle(trans('entities.shelves_create'));
|
||||
|
||||
return view('shelves.create', ['books' => $books]);
|
||||
@@ -80,15 +83,15 @@ class BookshelfController extends Controller
|
||||
public function store(Request $request)
|
||||
{
|
||||
$this->checkPermission('bookshelf-create-all');
|
||||
$validated = $this->validate($request, [
|
||||
$this->validate($request, [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'description' => ['string', 'max:1000'],
|
||||
'image' => array_merge(['nullable'], $this->getImageValidationRules()),
|
||||
'tags' => ['array'],
|
||||
]);
|
||||
|
||||
$bookIds = explode(',', $request->get('books', ''));
|
||||
$shelf = $this->shelfRepo->create($validated, $bookIds);
|
||||
$shelf = $this->bookshelfRepo->create($request->all(), $bookIds);
|
||||
$this->bookshelfRepo->updateCoverImage($shelf, $request->file('image', null));
|
||||
|
||||
return redirect($shelf->getUrl());
|
||||
}
|
||||
@@ -100,8 +103,8 @@ class BookshelfController extends Controller
|
||||
*/
|
||||
public function show(ActivityQueries $activities, string $slug)
|
||||
{
|
||||
$shelf = $this->shelfRepo->getBySlug($slug);
|
||||
$this->checkOwnablePermission('bookshelf-view', $shelf);
|
||||
$shelf = $this->bookshelfRepo->getBySlug($slug);
|
||||
$this->checkOwnablePermission('book-view', $shelf);
|
||||
|
||||
$sort = setting()->getForCurrentUser('shelf_books_sort', 'default');
|
||||
$order = setting()->getForCurrentUser('shelf_books_sort_order', 'asc');
|
||||
@@ -112,7 +115,7 @@ class BookshelfController extends Controller
|
||||
->all();
|
||||
|
||||
View::incrementFor($shelf);
|
||||
$this->shelfContext->setShelfContext($shelf->id);
|
||||
$this->entityContextManager->setShelfContext($shelf->id);
|
||||
$view = setting()->getForCurrentUser('bookshelf_view_type');
|
||||
|
||||
$this->setPageTitle($shelf->getShortName());
|
||||
@@ -132,11 +135,11 @@ class BookshelfController extends Controller
|
||||
*/
|
||||
public function edit(string $slug)
|
||||
{
|
||||
$shelf = $this->shelfRepo->getBySlug($slug);
|
||||
$shelf = $this->bookshelfRepo->getBySlug($slug);
|
||||
$this->checkOwnablePermission('bookshelf-update', $shelf);
|
||||
|
||||
$shelfBookIds = $shelf->books()->get(['id'])->pluck('id');
|
||||
$books = Book::visible()->whereNotIn('id', $shelfBookIds)->orderBy('name')->get(['name', 'id', 'slug']);
|
||||
$books = Book::hasPermission('update')->whereNotIn('id', $shelfBookIds)->get();
|
||||
|
||||
$this->setPageTitle(trans('entities.shelves_edit_named', ['name' => $shelf->getShortName()]));
|
||||
|
||||
@@ -155,23 +158,18 @@ class BookshelfController extends Controller
|
||||
*/
|
||||
public function update(Request $request, string $slug)
|
||||
{
|
||||
$shelf = $this->shelfRepo->getBySlug($slug);
|
||||
$shelf = $this->bookshelfRepo->getBySlug($slug);
|
||||
$this->checkOwnablePermission('bookshelf-update', $shelf);
|
||||
$validated = $this->validate($request, [
|
||||
$this->validate($request, [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'description' => ['string', 'max:1000'],
|
||||
'image' => array_merge(['nullable'], $this->getImageValidationRules()),
|
||||
'tags' => ['array'],
|
||||
]);
|
||||
|
||||
if ($request->has('image_reset')) {
|
||||
$validated['image'] = null;
|
||||
} elseif (array_key_exists('image', $validated) && is_null($validated['image'])) {
|
||||
unset($validated['image']);
|
||||
}
|
||||
|
||||
$bookIds = explode(',', $request->get('books', ''));
|
||||
$shelf = $this->shelfRepo->update($shelf, $validated, $bookIds);
|
||||
$shelf = $this->bookshelfRepo->update($shelf, $request->all(), $bookIds);
|
||||
$resetCover = $request->has('image_reset');
|
||||
$this->bookshelfRepo->updateCoverImage($shelf, $request->file('image', null), $resetCover);
|
||||
|
||||
return redirect($shelf->getUrl());
|
||||
}
|
||||
@@ -181,7 +179,7 @@ class BookshelfController extends Controller
|
||||
*/
|
||||
public function showDelete(string $slug)
|
||||
{
|
||||
$shelf = $this->shelfRepo->getBySlug($slug);
|
||||
$shelf = $this->bookshelfRepo->getBySlug($slug);
|
||||
$this->checkOwnablePermission('bookshelf-delete', $shelf);
|
||||
|
||||
$this->setPageTitle(trans('entities.shelves_delete_named', ['name' => $shelf->getShortName()]));
|
||||
@@ -196,10 +194,10 @@ class BookshelfController extends Controller
|
||||
*/
|
||||
public function destroy(string $slug)
|
||||
{
|
||||
$shelf = $this->shelfRepo->getBySlug($slug);
|
||||
$shelf = $this->bookshelfRepo->getBySlug($slug);
|
||||
$this->checkOwnablePermission('bookshelf-delete', $shelf);
|
||||
|
||||
$this->shelfRepo->destroy($shelf);
|
||||
$this->bookshelfRepo->destroy($shelf);
|
||||
|
||||
return redirect('/shelves');
|
||||
}
|
||||
@@ -209,7 +207,7 @@ class BookshelfController extends Controller
|
||||
*/
|
||||
public function showPermissions(string $slug)
|
||||
{
|
||||
$shelf = $this->shelfRepo->getBySlug($slug);
|
||||
$shelf = $this->bookshelfRepo->getBySlug($slug);
|
||||
$this->checkOwnablePermission('restrictions-manage', $shelf);
|
||||
|
||||
return view('shelves.permissions', [
|
||||
@@ -222,7 +220,7 @@ class BookshelfController extends Controller
|
||||
*/
|
||||
public function permissions(Request $request, PermissionsUpdater $permissionsUpdater, string $slug)
|
||||
{
|
||||
$shelf = $this->shelfRepo->getBySlug($slug);
|
||||
$shelf = $this->bookshelfRepo->getBySlug($slug);
|
||||
$this->checkOwnablePermission('restrictions-manage', $shelf);
|
||||
|
||||
$permissionsUpdater->updateFromPermissionsForm($shelf, $request);
|
||||
@@ -237,10 +235,10 @@ class BookshelfController extends Controller
|
||||
*/
|
||||
public function copyPermissions(string $slug)
|
||||
{
|
||||
$shelf = $this->shelfRepo->getBySlug($slug);
|
||||
$shelf = $this->bookshelfRepo->getBySlug($slug);
|
||||
$this->checkOwnablePermission('restrictions-manage', $shelf);
|
||||
|
||||
$updateCount = $this->shelfRepo->copyDownPermissions($shelf);
|
||||
$updateCount = $this->bookshelfRepo->copyDownPermissions($shelf);
|
||||
$this->showSuccessNotification(trans('entities.shelves_copy_permission_success', ['count' => $updateCount]));
|
||||
|
||||
return redirect($shelf->getUrl());
|
||||
|
||||
@@ -7,7 +7,6 @@ use BookStack\Entities\Models\Book;
|
||||
use BookStack\Entities\Repos\ChapterRepo;
|
||||
use BookStack\Entities\Tools\BookContents;
|
||||
use BookStack\Entities\Tools\Cloner;
|
||||
use BookStack\Entities\Tools\HierarchyTransformer;
|
||||
use BookStack\Entities\Tools\NextPreviousContentLocator;
|
||||
use BookStack\Entities\Tools\PermissionsUpdater;
|
||||
use BookStack\Exceptions\MoveOperationException;
|
||||
@@ -273,19 +272,4 @@ class ChapterController extends Controller
|
||||
|
||||
return redirect($chapter->getUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the chapter to a book.
|
||||
*/
|
||||
public function convertToBook(HierarchyTransformer $transformer, string $bookSlug, string $chapterSlug)
|
||||
{
|
||||
$chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
|
||||
$this->checkOwnablePermission('chapter-update', $chapter);
|
||||
$this->checkOwnablePermission('chapter-delete', $chapter);
|
||||
$this->checkPermission('book-create-all');
|
||||
|
||||
$book = $transformer->transformChapterToBook($chapter);
|
||||
|
||||
return redirect($book->getUrl());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ class ChapterExportController extends Controller
|
||||
$chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
|
||||
$pdfContent = $this->exportFormatter->chapterToPdf($chapter);
|
||||
|
||||
return $this->download()->directly($pdfContent, $chapterSlug . '.pdf');
|
||||
return $this->downloadResponse($pdfContent, $chapterSlug . '.pdf');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -47,7 +47,7 @@ class ChapterExportController extends Controller
|
||||
$chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
|
||||
$containedHtml = $this->exportFormatter->chapterToContainedHtml($chapter);
|
||||
|
||||
return $this->download()->directly($containedHtml, $chapterSlug . '.html');
|
||||
return $this->downloadResponse($containedHtml, $chapterSlug . '.html');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -60,7 +60,7 @@ class ChapterExportController extends Controller
|
||||
$chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
|
||||
$chapterText = $this->exportFormatter->chapterToPlainText($chapter);
|
||||
|
||||
return $this->download()->directly($chapterText, $chapterSlug . '.txt');
|
||||
return $this->downloadResponse($chapterText, $chapterSlug . '.txt');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,9 +70,10 @@ class ChapterExportController extends Controller
|
||||
*/
|
||||
public function markdown(string $bookSlug, string $chapterSlug)
|
||||
{
|
||||
// TODO: This should probably export to a zip file.
|
||||
$chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
|
||||
$chapterText = $this->exportFormatter->chapterToMarkdown($chapter);
|
||||
|
||||
return $this->download()->directly($chapterText, $chapterSlug . '.md');
|
||||
return $this->downloadResponse($chapterText, $chapterSlug . '.md');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,13 +4,15 @@ namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Exceptions\NotifyException;
|
||||
use BookStack\Facades\Activity;
|
||||
use BookStack\Http\Responses\DownloadResponseFactory;
|
||||
use BookStack\Interfaces\Loggable;
|
||||
use BookStack\Model;
|
||||
use BookStack\Util\WebSafeMimeSniffer;
|
||||
use Illuminate\Foundation\Bus\DispatchesJobs;
|
||||
use Illuminate\Foundation\Validation\ValidatesRequests;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Routing\Controller as BaseController;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
abstract class Controller extends BaseController
|
||||
{
|
||||
@@ -108,11 +110,74 @@ abstract class Controller extends BaseController
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and return a new download response factory using the current request.
|
||||
* Create a response that forces a download in the browser.
|
||||
*/
|
||||
protected function download(): DownloadResponseFactory
|
||||
protected function downloadResponse(string $content, string $fileName): Response
|
||||
{
|
||||
return new DownloadResponseFactory(request());
|
||||
return response()->make($content, 200, [
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Content-Disposition' => 'attachment; filename="' . str_replace('"', '', $fileName) . '"',
|
||||
'X-Content-Type-Options' => 'nosniff',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a response that forces a download, from a given stream of content.
|
||||
*/
|
||||
protected function streamedDownloadResponse($stream, string $fileName): StreamedResponse
|
||||
{
|
||||
return response()->stream(function () use ($stream) {
|
||||
|
||||
// End & flush the output buffer, if we're in one, otherwise we still use memory.
|
||||
// Output buffer may or may not exist depending on PHP `output_buffering` setting.
|
||||
// Ignore in testing since output buffers are used to gather a response.
|
||||
if (!empty(ob_get_status()) && !app()->runningUnitTests()) {
|
||||
ob_end_clean();
|
||||
}
|
||||
|
||||
fpassthru($stream);
|
||||
fclose($stream);
|
||||
}, 200, [
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Content-Disposition' => 'attachment; filename="' . str_replace('"', '', $fileName) . '"',
|
||||
'X-Content-Type-Options' => 'nosniff',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a file download response that provides the file with a content-type
|
||||
* correct for the file, in a way so the browser can show the content in browser.
|
||||
*/
|
||||
protected function inlineDownloadResponse(string $content, string $fileName): Response
|
||||
{
|
||||
$mime = (new WebSafeMimeSniffer())->sniff($content);
|
||||
|
||||
return response()->make($content, 200, [
|
||||
'Content-Type' => $mime,
|
||||
'Content-Disposition' => 'inline; filename="' . str_replace('"', '', $fileName) . '"',
|
||||
'X-Content-Type-Options' => 'nosniff',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a file download response that provides the file with a content-type
|
||||
* correct for the file, in a way so the browser can show the content in browser,
|
||||
* for a given content stream.
|
||||
*/
|
||||
protected function streamedInlineDownloadResponse($stream, string $fileName): StreamedResponse
|
||||
{
|
||||
$sniffContent = fread($stream, 1000);
|
||||
$mime = (new WebSafeMimeSniffer())->sniff($sniffContent);
|
||||
|
||||
return response()->stream(function () use ($sniffContent, $stream) {
|
||||
echo $sniffContent;
|
||||
fpassthru($stream);
|
||||
fclose($stream);
|
||||
}, 200, [
|
||||
'Content-Type' => $mime,
|
||||
'Content-Disposition' => 'inline; filename="' . str_replace('"', '', $fileName) . '"',
|
||||
'X-Content-Type-Options' => 'nosniff',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -154,6 +219,6 @@ abstract class Controller extends BaseController
|
||||
*/
|
||||
protected function getImageValidationRules(): array
|
||||
{
|
||||
return ['image_extension', 'mimes:jpeg,png,gif,webp', 'max:' . (config('app.upload_limit') * 1000)];
|
||||
return ['image_extension', 'mimes:jpeg,png,gif,webp,svg', 'max:' . (config('app.upload_limit') * 1000)];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ class FavouriteController extends Controller
|
||||
|
||||
$modelInstance = $model->newQuery()
|
||||
->where('id', '=', $modelInfo['id'])
|
||||
->first(['id', 'name', 'restricted', 'owned_by']);
|
||||
->first(['id', 'name']);
|
||||
|
||||
$inaccessibleEntity = ($modelInstance instanceof Entity && !userCan('view', $modelInstance));
|
||||
if (is_null($modelInstance) || $inaccessibleEntity) {
|
||||
|
||||
@@ -76,8 +76,11 @@ class DrawioImageController extends Controller
|
||||
return $this->jsonError('Image data could not be found');
|
||||
}
|
||||
|
||||
$isSvg = strtolower(pathinfo($image->path, PATHINFO_EXTENSION)) === 'svg';
|
||||
$uriPrefix = $isSvg ? 'data:image/svg+xml;base64,' : 'data:image/png;base64,';
|
||||
|
||||
return response()->json([
|
||||
'content' => base64_encode($imageData),
|
||||
'content' => $uriPrefix . base64_encode($imageData),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ class PageExportController extends Controller
|
||||
$page->html = (new PageContent($page))->render();
|
||||
$pdfContent = $this->exportFormatter->pageToPdf($page);
|
||||
|
||||
return $this->download()->directly($pdfContent, $pageSlug . '.pdf');
|
||||
return $this->downloadResponse($pdfContent, $pageSlug . '.pdf');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,7 +51,7 @@ class PageExportController extends Controller
|
||||
$page->html = (new PageContent($page))->render();
|
||||
$containedHtml = $this->exportFormatter->pageToContainedHtml($page);
|
||||
|
||||
return $this->download()->directly($containedHtml, $pageSlug . '.html');
|
||||
return $this->downloadResponse($containedHtml, $pageSlug . '.html');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -64,7 +64,7 @@ class PageExportController extends Controller
|
||||
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
|
||||
$pageText = $this->exportFormatter->pageToPlainText($page);
|
||||
|
||||
return $this->download()->directly($pageText, $pageSlug . '.txt');
|
||||
return $this->downloadResponse($pageText, $pageSlug . '.txt');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,6 +77,6 @@ class PageExportController extends Controller
|
||||
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
|
||||
$pageText = $this->exportFormatter->pageToMarkdown($page);
|
||||
|
||||
return $this->download()->directly($pageText, $pageSlug . '.md');
|
||||
return $this->downloadResponse($pageText, $pageSlug . '.md');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,17 +2,18 @@
|
||||
|
||||
namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Actions\ActivityType;
|
||||
use BookStack\Entities\Repos\PageRepo;
|
||||
use BookStack\Entities\Tools\PageContent;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use BookStack\Facades\Activity;
|
||||
use Ssddanbrown\HtmlDiff\Diff;
|
||||
|
||||
class PageRevisionController extends Controller
|
||||
{
|
||||
protected PageRepo $pageRepo;
|
||||
protected $pageRepo;
|
||||
|
||||
/**
|
||||
* PageRevisionController constructor.
|
||||
*/
|
||||
public function __construct(PageRepo $pageRepo)
|
||||
{
|
||||
$this->pageRepo = $pageRepo;
|
||||
@@ -26,19 +27,11 @@ class PageRevisionController extends Controller
|
||||
public function index(string $bookSlug, string $pageSlug)
|
||||
{
|
||||
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
|
||||
$revisions = $page->revisions()->select([
|
||||
'id', 'page_id', 'name', 'created_at', 'created_by', 'updated_at',
|
||||
'type', 'revision_number', 'summary',
|
||||
])
|
||||
->selectRaw("IF(markdown = '', false, true) as is_markdown")
|
||||
->with(['page.book', 'createdBy'])
|
||||
->get();
|
||||
|
||||
$this->setPageTitle(trans('entities.pages_revisions_named', ['pageName' => $page->getShortName()]));
|
||||
$this->setPageTitle(trans('entities.pages_revisions_named', ['pageName'=>$page->getShortName()]));
|
||||
|
||||
return view('pages.revisions', [
|
||||
'revisions' => $revisions,
|
||||
'page' => $page,
|
||||
'current' => $page,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -139,7 +132,6 @@ class PageRevisionController extends Controller
|
||||
}
|
||||
|
||||
$revision->delete();
|
||||
Activity::add(ActivityType::REVISION_DELETE, $revision);
|
||||
$this->showSuccessNotification(trans('entities.revision_delete_success'));
|
||||
|
||||
return redirect($page->getUrl('/revisions'));
|
||||
|
||||
@@ -12,6 +12,7 @@ use Illuminate\Http\Request;
|
||||
class SearchController extends Controller
|
||||
{
|
||||
protected $searchRunner;
|
||||
protected $entityContextManager;
|
||||
|
||||
public function __construct(SearchRunner $searchRunner)
|
||||
{
|
||||
@@ -78,12 +79,12 @@ class SearchController extends Controller
|
||||
// Search for entities otherwise show most popular
|
||||
if ($searchTerm !== false) {
|
||||
$searchTerm .= ' {type:' . implode('|', $entityTypes) . '}';
|
||||
$entities = $this->searchRunner->searchEntities(SearchOptions::fromString($searchTerm), 'all', 1, 20)['results'];
|
||||
$entities = $this->searchRunner->searchEntities(SearchOptions::fromString($searchTerm), 'all', 1, 20, $permission)['results'];
|
||||
} else {
|
||||
$entities = (new Popular())->run(20, 0, $entityTypes);
|
||||
$entities = (new Popular())->run(20, 0, $entityTypes, $permission);
|
||||
}
|
||||
|
||||
return view('search.parts.entity-ajax-list', ['entities' => $entities, 'permission' => $permission]);
|
||||
return view('search.parts.entity-ajax-list', ['entities' => $entities]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,8 +18,8 @@ use Illuminate\Validation\ValidationException;
|
||||
|
||||
class UserController extends Controller
|
||||
{
|
||||
protected UserRepo $userRepo;
|
||||
protected ImageRepo $imageRepo;
|
||||
protected $userRepo;
|
||||
protected $imageRepo;
|
||||
|
||||
/**
|
||||
* UserController constructor.
|
||||
@@ -81,9 +81,9 @@ class UserController extends Controller
|
||||
$passwordRequired = ($authMethod === 'standard' && !$sendInvite);
|
||||
|
||||
$validationRules = [
|
||||
'name' => ['required', 'max:100'],
|
||||
'name' => ['required'],
|
||||
'email' => ['required', 'email', 'unique:users,email'],
|
||||
'language' => ['string', 'max:15', 'alpha_dash'],
|
||||
'language' => ['string'],
|
||||
'roles' => ['array'],
|
||||
'roles.*' => ['integer'],
|
||||
'password' => $passwordRequired ? ['required', Password::default()] : null,
|
||||
@@ -139,11 +139,11 @@ class UserController extends Controller
|
||||
$this->checkPermissionOrCurrentUser('users-manage', $id);
|
||||
|
||||
$validated = $this->validate($request, [
|
||||
'name' => ['min:2', 'max:100'],
|
||||
'name' => ['min:2'],
|
||||
'email' => ['min:2', 'email', 'unique:users,email,' . $id],
|
||||
'password' => ['required_with:password_confirm', Password::default()],
|
||||
'password-confirm' => ['same:password', 'required_with:password'],
|
||||
'language' => ['string', 'max:15', 'alpha_dash'],
|
||||
'language' => ['string'],
|
||||
'roles' => ['array'],
|
||||
'roles.*' => ['integer'],
|
||||
'external_auth_id' => ['string'],
|
||||
@@ -289,27 +289,6 @@ class UserController extends Controller
|
||||
return response('', 204);
|
||||
}
|
||||
|
||||
public function updateCodeLanguageFavourite(Request $request)
|
||||
{
|
||||
$validated = $this->validate($request, [
|
||||
'language' => ['required', 'string', 'max:20'],
|
||||
'active' => ['required', 'bool'],
|
||||
]);
|
||||
|
||||
$currentFavoritesStr = setting()->getForCurrentUser('code-language-favourites', '');
|
||||
$currentFavorites = array_filter(explode(',', $currentFavoritesStr));
|
||||
|
||||
$isFav = in_array($validated['language'], $currentFavorites);
|
||||
if (!$isFav && $validated['active']) {
|
||||
$currentFavorites[] = $validated['language'];
|
||||
} elseif ($isFav && !$validated['active']) {
|
||||
$index = array_search($validated['language'], $currentFavorites);
|
||||
array_splice($currentFavorites, $index, 1);
|
||||
}
|
||||
|
||||
setting()->putUser(user(), 'code-language-favourites', implode(',', $currentFavorites));
|
||||
}
|
||||
|
||||
/**
|
||||
* Changed the stored preference for a list sort order.
|
||||
*/
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Http\Responses;
|
||||
|
||||
use BookStack\Util\WebSafeMimeSniffer;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class DownloadResponseFactory
|
||||
{
|
||||
protected Request $request;
|
||||
|
||||
public function __construct(Request $request)
|
||||
{
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a response that directly forces a download in the browser.
|
||||
*/
|
||||
public function directly(string $content, string $fileName): Response
|
||||
{
|
||||
return response()->make($content, 200, $this->getHeaders($fileName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a response that forces a download, from a given stream of content.
|
||||
*/
|
||||
public function streamedDirectly($stream, string $fileName): StreamedResponse
|
||||
{
|
||||
return response()->stream(function () use ($stream) {
|
||||
|
||||
// End & flush the output buffer, if we're in one, otherwise we still use memory.
|
||||
// Output buffer may or may not exist depending on PHP `output_buffering` setting.
|
||||
// Ignore in testing since output buffers are used to gather a response.
|
||||
if (!empty(ob_get_status()) && !app()->runningUnitTests()) {
|
||||
ob_end_clean();
|
||||
}
|
||||
|
||||
fpassthru($stream);
|
||||
fclose($stream);
|
||||
}, 200, $this->getHeaders($fileName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a file download response that provides the file with a content-type
|
||||
* correct for the file, in a way so the browser can show the content in browser,
|
||||
* for a given content stream.
|
||||
*/
|
||||
public function streamedInline($stream, string $fileName): StreamedResponse
|
||||
{
|
||||
$sniffContent = fread($stream, 2000);
|
||||
$mime = (new WebSafeMimeSniffer())->sniff($sniffContent);
|
||||
|
||||
return response()->stream(function () use ($sniffContent, $stream) {
|
||||
echo $sniffContent;
|
||||
fpassthru($stream);
|
||||
fclose($stream);
|
||||
}, 200, $this->getHeaders($fileName, $mime));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the common headers to provide for a download response.
|
||||
*/
|
||||
protected function getHeaders(string $fileName, string $mime = 'application/octet-stream'): array
|
||||
{
|
||||
$disposition = ($mime === 'application/octet-stream') ? 'attachment' : 'inline';
|
||||
$downloadName = str_replace('"', '', $fileName);
|
||||
|
||||
return [
|
||||
'Content-Type' => $mime,
|
||||
'Content-Disposition' => "{$disposition}; filename=\"{$downloadName}\"",
|
||||
'X-Content-Type-Options' => 'nosniff',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,9 @@
|
||||
namespace BookStack\Providers;
|
||||
|
||||
use BookStack\Actions\ActivityLogger;
|
||||
use BookStack\Auth\Permissions\PermissionService;
|
||||
use BookStack\Theming\ThemeService;
|
||||
use BookStack\Uploads\ImageService;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class CustomFacadeProvider extends ServiceProvider
|
||||
@@ -29,6 +31,14 @@ class CustomFacadeProvider extends ServiceProvider
|
||||
return $this->app->make(ActivityLogger::class);
|
||||
});
|
||||
|
||||
$this->app->singleton('images', function () {
|
||||
return $this->app->make(ImageService::class);
|
||||
});
|
||||
|
||||
$this->app->singleton('permissions', function () {
|
||||
return $this->app->make(PermissionService::class);
|
||||
});
|
||||
|
||||
$this->app->singleton('theme', function () {
|
||||
return $this->app->make(ThemeService::class);
|
||||
});
|
||||
|
||||
@@ -15,19 +15,6 @@ namespace BookStack\Theming;
|
||||
*/
|
||||
class ThemeEvents
|
||||
{
|
||||
/**
|
||||
* Activity logged event.
|
||||
* Runs right after an activity is logged by bookstack.
|
||||
* These are the activities that can be seen in the audit log area of BookStack.
|
||||
* Activity types can be seen listed in the \BookStack\Actions\ActivityType class.
|
||||
* The provided $detail can be a string or a loggable type of model. You should check
|
||||
* the type before making use of this parameter.
|
||||
*
|
||||
* @param string $type
|
||||
* @param string|\BookStack\Interfaces\Loggable $detail
|
||||
*/
|
||||
const ACTIVITY_LOGGED = 'activity_logged';
|
||||
|
||||
/**
|
||||
* Application boot-up.
|
||||
* After main services are registered.
|
||||
@@ -36,6 +23,30 @@ class ThemeEvents
|
||||
*/
|
||||
const APP_BOOT = 'app_boot';
|
||||
|
||||
/**
|
||||
* Web before middleware action.
|
||||
* Runs before the request is handled but after all other middleware apart from those
|
||||
* that depend on the current session user (Localization for example).
|
||||
* Provides the original request to use.
|
||||
* Return values, if provided, will be used as a new response to use.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @returns \Illuminate\Http\Response|null
|
||||
*/
|
||||
const WEB_MIDDLEWARE_BEFORE = 'web_middleware_before';
|
||||
|
||||
/**
|
||||
* Web after middleware action.
|
||||
* Runs after the request is handled but before the response is sent.
|
||||
* Provides both the original request and the currently resolved response.
|
||||
* Return values, if provided, will be used as a new response to use.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Illuminate\Http\Response|Symfony\Component\HttpFoundation\BinaryFileResponse $response
|
||||
* @returns \Illuminate\Http\Response|null
|
||||
*/
|
||||
const WEB_MIDDLEWARE_AFTER = 'web_middleware_after';
|
||||
|
||||
/**
|
||||
* Auth login event.
|
||||
* Runs right after a user is logged-in to the application by any authentication
|
||||
@@ -61,7 +72,7 @@ class ThemeEvents
|
||||
/**
|
||||
* Commonmark environment configure.
|
||||
* Provides the commonmark library environment for customization
|
||||
* before it's used to render markdown content.
|
||||
* before its used to render markdown content.
|
||||
* If the listener returns a non-null value, that will be used as an environment instead.
|
||||
*
|
||||
* @param \League\CommonMark\ConfigurableEnvironmentInterface $environment
|
||||
@@ -69,30 +80,6 @@ class ThemeEvents
|
||||
*/
|
||||
const COMMONMARK_ENVIRONMENT_CONFIGURE = 'commonmark_environment_configure';
|
||||
|
||||
/**
|
||||
* Web before middleware action.
|
||||
* Runs before the request is handled but after all other middleware apart from those
|
||||
* that depend on the current session user (Localization for example).
|
||||
* Provides the original request to use.
|
||||
* Return values, if provided, will be used as a new response to use.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @returns \Illuminate\Http\Response|null
|
||||
*/
|
||||
const WEB_MIDDLEWARE_BEFORE = 'web_middleware_before';
|
||||
|
||||
/**
|
||||
* Web after middleware action.
|
||||
* Runs after the request is handled but before the response is sent.
|
||||
* Provides both the original request and the currently resolved response.
|
||||
* Return values, if provided, will be used as a new response to use.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Illuminate\Http\Response|\Symfony\Component\HttpFoundation\BinaryFileResponse $response
|
||||
* @returns \Illuminate\Http\Response|null
|
||||
*/
|
||||
const WEB_MIDDLEWARE_AFTER = 'web_middleware_after';
|
||||
|
||||
/**
|
||||
* Webhook call before event.
|
||||
* Runs before a webhook endpoint is called. Allows for customization
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace BookStack\Uploads;
|
||||
|
||||
use BookStack\Auth\Permissions\PermissionApplicator;
|
||||
use BookStack\Auth\Permissions\PermissionService;
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Entities\Models\Entity;
|
||||
use BookStack\Entities\Models\Page;
|
||||
@@ -89,9 +89,10 @@ class Attachment extends Model
|
||||
*/
|
||||
public function scopeVisible(): Builder
|
||||
{
|
||||
$permissions = app()->make(PermissionApplicator::class);
|
||||
$permissionService = app()->make(PermissionService::class);
|
||||
|
||||
return $permissions->restrictPageRelationQuery(
|
||||
return $permissionService->filterRelatedEntity(
|
||||
Page::class,
|
||||
self::query(),
|
||||
'attachments',
|
||||
'uploaded_to'
|
||||
|
||||
@@ -63,6 +63,16 @@ class AttachmentService
|
||||
return 'uploads/files/' . $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an attachment from storage.
|
||||
*
|
||||
* @throws FileNotFoundException
|
||||
*/
|
||||
public function getAttachmentFromStorage(Attachment $attachment): string
|
||||
{
|
||||
return $this->getStorageDisk()->get($this->adjustPathForStorageDisk($attachment->path));
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream an attachment from storage.
|
||||
*
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace BookStack\Uploads;
|
||||
|
||||
use BookStack\Auth\Permissions\PermissionApplicator;
|
||||
use BookStack\Auth\Permissions\PermissionService;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Exceptions\ImageUploadException;
|
||||
use Exception;
|
||||
@@ -11,16 +11,16 @@ use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
|
||||
class ImageRepo
|
||||
{
|
||||
protected ImageService $imageService;
|
||||
protected PermissionApplicator $permissions;
|
||||
protected $imageService;
|
||||
protected $restrictionService;
|
||||
|
||||
/**
|
||||
* ImageRepo constructor.
|
||||
*/
|
||||
public function __construct(ImageService $imageService, PermissionApplicator $permissions)
|
||||
public function __construct(ImageService $imageService, PermissionService $permissionService)
|
||||
{
|
||||
$this->imageService = $imageService;
|
||||
$this->permissions = $permissions;
|
||||
$this->restrictionService = $permissionService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -74,7 +74,7 @@ class ImageRepo
|
||||
}
|
||||
|
||||
// Filter by page access
|
||||
$imageQuery = $this->permissions->restrictPageRelationQuery($imageQuery, 'images', 'uploaded_to');
|
||||
$imageQuery = $this->restrictionService->filterRelatedEntity(Page::class, $imageQuery, 'images', 'uploaded_to');
|
||||
|
||||
if ($whereClause !== null) {
|
||||
$imageQuery = $imageQuery->where($whereClause);
|
||||
@@ -148,7 +148,8 @@ class ImageRepo
|
||||
*/
|
||||
public function saveDrawing(string $base64Uri, int $uploadedTo): Image
|
||||
{
|
||||
$name = 'Drawing-' . user()->id . '-' . time() . '.png';
|
||||
$isSvg = strpos($base64Uri, 'data:image/svg+xml;') === 0;
|
||||
$name = 'Drawing-' . user()->id . '-' . time() . ($isSvg ? '.svg' : '.png');
|
||||
|
||||
return $this->imageService->saveNewFromBase64Uri($base64Uri, $name, 'drawio', $uploadedTo);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ class ImageService
|
||||
protected $image;
|
||||
protected $fileSystem;
|
||||
|
||||
protected static $supportedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
||||
protected static $supportedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'];
|
||||
|
||||
/**
|
||||
* ImageService constructor.
|
||||
@@ -230,6 +230,14 @@ class ImageService
|
||||
return strtolower(pathinfo($image->path, PATHINFO_EXTENSION)) === 'gif';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given image is an SVG image file.
|
||||
*/
|
||||
protected function isSvg(Image $image): bool
|
||||
{
|
||||
return strtolower(pathinfo($image->path, PATHINFO_EXTENSION)) === 'svg';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given image and image data is apng.
|
||||
*/
|
||||
@@ -255,8 +263,8 @@ class ImageService
|
||||
*/
|
||||
public function getThumbnail(Image $image, ?int $width, ?int $height, bool $keepRatio = false): string
|
||||
{
|
||||
// Do not resize GIF images where we're not cropping
|
||||
if ($keepRatio && $this->isGif($image)) {
|
||||
// Do not resize GIF images where we're not cropping or SVG images.
|
||||
if (($keepRatio && $this->isGif($image)) || $this->isSvg($image)) {
|
||||
return $this->getPublicUrl($image->path);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ use BookStack\Auth\User;
|
||||
use BookStack\Exceptions\HttpFetchException;
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class UserAvatars
|
||||
{
|
||||
@@ -94,7 +93,7 @@ class UserAvatars
|
||||
*/
|
||||
protected function createAvatarImageFromData(User $user, string $imageData, string $extension): Image
|
||||
{
|
||||
$imageName = Str::random(10) . '-avatar.' . $extension;
|
||||
$imageName = str_replace(' ', '-', $user->id . '-avatar.' . $extension);
|
||||
|
||||
$image = $this->imageService->saveNew($imageName, $imageData, 'user', $user->id);
|
||||
$image->created_by = $user->id;
|
||||
@@ -135,12 +134,7 @@ class UserAvatars
|
||||
*/
|
||||
protected function getAvatarUrl(): string
|
||||
{
|
||||
$configOption = config('services.avatar_url');
|
||||
if ($configOption === false) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$url = trim($configOption);
|
||||
$url = trim(config('services.avatar_url'));
|
||||
|
||||
if (empty($url) && !config('services.disable_services')) {
|
||||
$url = 'https://www.gravatar.com/avatar/${hash}?s=${size}&d=identicon';
|
||||
|
||||
@@ -45,11 +45,6 @@ class HtmlContentFilter
|
||||
$badIframes = $xPath->query('//*[' . static::xpathContains('@src', 'data:') . '] | //*[' . static::xpathContains('@src', 'javascript:') . '] | //*[@srcdoc]');
|
||||
static::removeNodes($badIframes);
|
||||
|
||||
// Remove tags hiding JavaScript or data uris in values attribute.
|
||||
// For example, SVG animate tag can exploit javascript in values.
|
||||
$badValuesTags = $xPath->query('//*[' . static::xpathContains('@values', 'data:') . '] | //*[' . static::xpathContains('@values', 'javascript:') . ']');
|
||||
static::removeNodes($badValuesTags);
|
||||
|
||||
// Remove elements with a xlink:href attribute
|
||||
// Used in SVG but deprecated anyway, so we'll be a bit more heavy-handed here.
|
||||
$xlinkHrefAttributes = $xPath->query('//@*[contains(name(), \'xlink:href\')]');
|
||||
|
||||
@@ -17,14 +17,6 @@ class WebSafeMimeSniffer
|
||||
'application/json',
|
||||
'application/octet-stream',
|
||||
'application/pdf',
|
||||
'audio/aac',
|
||||
'audio/midi',
|
||||
'audio/mpeg',
|
||||
'audio/ogg',
|
||||
'audio/opus',
|
||||
'audio/wav',
|
||||
'audio/webm',
|
||||
'audio/x-m4a',
|
||||
'image/apng',
|
||||
'image/bmp',
|
||||
'image/jpeg',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
use BookStack\Auth\Permissions\PermissionApplicator;
|
||||
use BookStack\Auth\Permissions\PermissionService;
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Model;
|
||||
use BookStack\Settings\SettingService;
|
||||
@@ -65,20 +65,20 @@ function userCan(string $permission, Model $ownable = null): bool
|
||||
}
|
||||
|
||||
// Check permission on ownable item
|
||||
$permissions = app(PermissionApplicator::class);
|
||||
$permissionService = app(PermissionService::class);
|
||||
|
||||
return $permissions->checkOwnableUserAccess($ownable, $permission);
|
||||
return $permissionService->checkOwnableUserAccess($ownable, $permission);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current user can perform the given action on any items in the system.
|
||||
* Can be provided the class name of an entity to filter ability to that specific entity type.
|
||||
* Check if the current user has the given permission
|
||||
* on any item in the system.
|
||||
*/
|
||||
function userCanOnAny(string $action, string $entityClass = ''): bool
|
||||
function userCanOnAny(string $permission, string $entityClass = null): bool
|
||||
{
|
||||
$permissions = app(PermissionApplicator::class);
|
||||
$permissionService = app(PermissionService::class);
|
||||
|
||||
return $permissions->checkUserHasEntityPermissionOnAny($action, $entityClass);
|
||||
return $permissionService->checkUserHasPermissionOnAnything($permission, $entityClass);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"ext-mbstring": "*",
|
||||
"ext-xml": "*",
|
||||
"bacon/bacon-qr-code": "^2.0",
|
||||
"barryvdh/laravel-dompdf": "^2.0",
|
||||
"barryvdh/laravel-dompdf": "^1.0",
|
||||
"barryvdh/laravel-snappy": "^1.0",
|
||||
"doctrine/dbal": "^3.1",
|
||||
"filp/whoops": "^2.14",
|
||||
@@ -50,7 +50,7 @@
|
||||
"nunomaduro/collision": "^5.10",
|
||||
"nunomaduro/larastan": "^1.0",
|
||||
"phpunit/phpunit": "^9.5",
|
||||
"ssddanbrown/asserthtml": "^1.0"
|
||||
"symfony/dom-crawler": "^5.3"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
|
||||
850
composer.lock
generated
850
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -21,9 +21,8 @@ class RoleFactory extends Factory
|
||||
public function definition()
|
||||
{
|
||||
return [
|
||||
'display_name' => $this->faker->sentence(3),
|
||||
'description' => $this->faker->sentence(10),
|
||||
'external_auth_id' => '',
|
||||
'display_name' => $this->faker->sentence(3),
|
||||
'description' => $this->faker->sentence(10),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class DropJointPermissionType extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
DB::table('joint_permissions')
|
||||
->where('action', '!=', 'view')
|
||||
->delete();
|
||||
|
||||
Schema::table('joint_permissions', function (Blueprint $table) {
|
||||
$table->dropPrimary(['role_id', 'entity_type', 'entity_id', 'action']);
|
||||
$table->dropColumn('action');
|
||||
$table->primary(['role_id', 'entity_type', 'entity_id'], 'joint_primary');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('joint_permissions', function (Blueprint $table) {
|
||||
$table->string('action');
|
||||
$table->dropPrimary(['role_id', 'entity_type', 'entity_id']);
|
||||
$table->primary(['role_id', 'entity_type', 'entity_id', 'action']);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
namespace Database\Seeders;
|
||||
|
||||
use BookStack\Api\ApiToken;
|
||||
use BookStack\Auth\Permissions\JointPermissionBuilder;
|
||||
use BookStack\Auth\Permissions\PermissionService;
|
||||
use BookStack\Auth\Permissions\RolePermission;
|
||||
use BookStack\Auth\Role;
|
||||
use BookStack\Auth\User;
|
||||
@@ -69,7 +69,7 @@ class DummyContentSeeder extends Seeder
|
||||
]);
|
||||
$token->save();
|
||||
|
||||
app(JointPermissionBuilder::class)->rebuildForAll();
|
||||
app(PermissionService::class)->buildJointPermissions();
|
||||
app(SearchIndex::class)->indexAllEntities();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use BookStack\Auth\Permissions\JointPermissionBuilder;
|
||||
use BookStack\Auth\Permissions\PermissionService;
|
||||
use BookStack\Auth\Role;
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Entities\Models\Book;
|
||||
@@ -35,7 +35,7 @@ class LargeContentSeeder extends Seeder
|
||||
$largeBook->chapters()->saveMany($chapters);
|
||||
$all = array_merge([$largeBook], array_values($pages->all()), array_values($chapters->all()));
|
||||
|
||||
app()->make(JointPermissionBuilder::class)->rebuildForEntity($largeBook);
|
||||
app()->make(PermissionService::class)->buildJointPermissionsForEntity($largeBook);
|
||||
app()->make(SearchIndex::class)->indexEntities($all);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
"updated_at": "2019-12-11T20:57:31.000000Z",
|
||||
"created_by": 1,
|
||||
"updated_by": 1,
|
||||
"owned_by": 1
|
||||
"owned_by": 1,
|
||||
"image_id": 3
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
@@ -20,7 +21,8 @@
|
||||
"updated_at": "2019-12-11T20:57:23.000000Z",
|
||||
"created_by": 4,
|
||||
"updated_by": 3,
|
||||
"owned_by": 3
|
||||
"owned_by": 3,
|
||||
"image_id": 34
|
||||
}
|
||||
],
|
||||
"total": 14
|
||||
|
||||
@@ -7,5 +7,6 @@
|
||||
"updated_at": "2020-01-12T14:16:10.000000Z",
|
||||
"created_by": 1,
|
||||
"updated_by": 1,
|
||||
"owned_by": 1
|
||||
"owned_by": 1,
|
||||
"image_id": 452
|
||||
}
|
||||
@@ -9,7 +9,8 @@
|
||||
"updated_at": "2020-04-10T13:00:45.000000Z",
|
||||
"created_by": 4,
|
||||
"updated_by": 1,
|
||||
"owned_by": 1
|
||||
"owned_by": 1,
|
||||
"image_id": 31
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
@@ -20,7 +21,8 @@
|
||||
"updated_at": "2020-04-10T13:00:58.000000Z",
|
||||
"created_by": 4,
|
||||
"updated_by": 1,
|
||||
"owned_by": 1
|
||||
"owned_by": 1,
|
||||
"image_id": 28
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
@@ -31,7 +33,8 @@
|
||||
"updated_at": "2020-04-10T13:00:53.000000Z",
|
||||
"created_by": 4,
|
||||
"updated_by": 1,
|
||||
"owned_by": 4
|
||||
"owned_by": 4,
|
||||
"image_id": 30
|
||||
}
|
||||
],
|
||||
"total": 3
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"created_by": 1,
|
||||
"updated_by": 1,
|
||||
"owned_by": 1,
|
||||
"image_id": 501,
|
||||
"created_at": "2020-04-10T13:24:09.000000Z",
|
||||
"updated_at": "2020-04-10T13:48:22.000000Z"
|
||||
}
|
||||
@@ -6,7 +6,7 @@ WORKDIR /app
|
||||
# Install additional dependacnies and configure apache
|
||||
RUN apt-get update -y \
|
||||
&& apt-get install -y git zip unzip libpng-dev libldap2-dev libzip-dev wait-for-it \
|
||||
&& docker-php-ext-configure ldap --with-libdir="lib/$(gcc -dumpmachine)" \
|
||||
&& docker-php-ext-configure ldap --with-libdir=lib/x86_64-linux-gnu \
|
||||
&& docker-php-ext-install pdo_mysql gd ldap zip \
|
||||
&& pecl install xdebug \
|
||||
&& docker-php-ext-enable xdebug \
|
||||
|
||||
@@ -13,8 +13,7 @@ You'll need to tell BookStack to use your theme via the `APP_THEME` option in yo
|
||||
|
||||
## Customizing View Files
|
||||
|
||||
Content placed in your `themes/<theme_name>/` folder will override the original view files found in the `resources/views` folder. These files are typically [Laravel Blade](https://laravel.com/docs/8.x/blade) files.
|
||||
As an example, I could override the `resources/views/books/parts/list-item.blade.php` file with my own template at the path `themes/<theme_name>/books/parts/list-item.blade.php`.
|
||||
Content placed in your `themes/<theme_name>/` folder will override the original view files found in the `resources/views` folder. These files are typically [Laravel Blade](https://laravel.com/docs/6.x/blade) files.
|
||||
|
||||
## Customizing Icons
|
||||
|
||||
|
||||
439
package-lock.json
generated
439
package-lock.json
generated
@@ -5,21 +5,20 @@
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"clipboard": "^2.0.11",
|
||||
"codemirror": "^5.65.5",
|
||||
"clipboard": "^2.0.10",
|
||||
"codemirror": "^5.65.2",
|
||||
"dropzone": "^5.9.3",
|
||||
"markdown-it": "^13.0.1",
|
||||
"markdown-it": "^12.3.2",
|
||||
"markdown-it-task-lists": "^2.1.1",
|
||||
"snabbdom": "^3.5.0",
|
||||
"sortablejs": "^1.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chokidar-cli": "^3.0",
|
||||
"esbuild": "0.14.42",
|
||||
"esbuild": "0.14.36",
|
||||
"livereload": "^0.9.3",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"punycode": "^2.1.1",
|
||||
"sass": "^1.52.1"
|
||||
"sass": "^1.50.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
@@ -174,9 +173,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/clipboard": {
|
||||
"version": "2.0.11",
|
||||
"resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz",
|
||||
"integrity": "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==",
|
||||
"version": "2.0.10",
|
||||
"resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.10.tgz",
|
||||
"integrity": "sha512-cz3m2YVwFz95qSEbCDi2fzLN/epEN9zXBvfgAoGkvGOJZATMl9gtTDVOtBYkx2ODUJl2kvmud7n32sV2BpYR4g==",
|
||||
"dependencies": {
|
||||
"good-listener": "^1.2.2",
|
||||
"select": "^1.1.2",
|
||||
@@ -195,9 +194,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/codemirror": {
|
||||
"version": "5.65.5",
|
||||
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.5.tgz",
|
||||
"integrity": "sha512-HNyhvGLnYz5c+kIsB9QKVitiZUevha3ovbIYaQiGzKo7ECSL/elWD9RXt3JgNr0NdnyqE9/Rc/7uLfkJQL638w=="
|
||||
"version": "5.65.2",
|
||||
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.2.tgz",
|
||||
"integrity": "sha512-SZM4Zq7XEC8Fhroqe3LxbEEX1zUPWH1wMr5zxiBuiUF64iYOUH/JI88v4tBag8MiBS8B8gRv8O1pPXGYXQ4ErA=="
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "1.9.3",
|
||||
@@ -274,12 +273,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz",
|
||||
"integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz",
|
||||
"integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==",
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
@@ -345,9 +341,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.42.tgz",
|
||||
"integrity": "sha512-V0uPZotCEHokJdNqyozH6qsaQXqmZEOiZWrXnds/zaH/0SyrIayRXWRB98CENO73MIZ9T3HBIOsmds5twWtmgw==",
|
||||
"version": "0.14.36",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.36.tgz",
|
||||
"integrity": "sha512-HhFHPiRXGYOCRlrhpiVDYKcFJRdO0sBElZ668M4lh2ER0YgnkLxECuFe7uWCf23FrcLc59Pqr7dHkTqmRPDHmw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"bin": {
|
||||
@@ -357,32 +353,32 @@
|
||||
"node": ">=12"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"esbuild-android-64": "0.14.42",
|
||||
"esbuild-android-arm64": "0.14.42",
|
||||
"esbuild-darwin-64": "0.14.42",
|
||||
"esbuild-darwin-arm64": "0.14.42",
|
||||
"esbuild-freebsd-64": "0.14.42",
|
||||
"esbuild-freebsd-arm64": "0.14.42",
|
||||
"esbuild-linux-32": "0.14.42",
|
||||
"esbuild-linux-64": "0.14.42",
|
||||
"esbuild-linux-arm": "0.14.42",
|
||||
"esbuild-linux-arm64": "0.14.42",
|
||||
"esbuild-linux-mips64le": "0.14.42",
|
||||
"esbuild-linux-ppc64le": "0.14.42",
|
||||
"esbuild-linux-riscv64": "0.14.42",
|
||||
"esbuild-linux-s390x": "0.14.42",
|
||||
"esbuild-netbsd-64": "0.14.42",
|
||||
"esbuild-openbsd-64": "0.14.42",
|
||||
"esbuild-sunos-64": "0.14.42",
|
||||
"esbuild-windows-32": "0.14.42",
|
||||
"esbuild-windows-64": "0.14.42",
|
||||
"esbuild-windows-arm64": "0.14.42"
|
||||
"esbuild-android-64": "0.14.36",
|
||||
"esbuild-android-arm64": "0.14.36",
|
||||
"esbuild-darwin-64": "0.14.36",
|
||||
"esbuild-darwin-arm64": "0.14.36",
|
||||
"esbuild-freebsd-64": "0.14.36",
|
||||
"esbuild-freebsd-arm64": "0.14.36",
|
||||
"esbuild-linux-32": "0.14.36",
|
||||
"esbuild-linux-64": "0.14.36",
|
||||
"esbuild-linux-arm": "0.14.36",
|
||||
"esbuild-linux-arm64": "0.14.36",
|
||||
"esbuild-linux-mips64le": "0.14.36",
|
||||
"esbuild-linux-ppc64le": "0.14.36",
|
||||
"esbuild-linux-riscv64": "0.14.36",
|
||||
"esbuild-linux-s390x": "0.14.36",
|
||||
"esbuild-netbsd-64": "0.14.36",
|
||||
"esbuild-openbsd-64": "0.14.36",
|
||||
"esbuild-sunos-64": "0.14.36",
|
||||
"esbuild-windows-32": "0.14.36",
|
||||
"esbuild-windows-64": "0.14.36",
|
||||
"esbuild-windows-arm64": "0.14.36"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-android-64": {
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.42.tgz",
|
||||
"integrity": "sha512-P4Y36VUtRhK/zivqGVMqhptSrFILAGlYp0Z8r9UQqHJ3iWztRCNWnlBzD9HRx0DbueXikzOiwyOri+ojAFfW6A==",
|
||||
"version": "0.14.36",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.36.tgz",
|
||||
"integrity": "sha512-jwpBhF1jmo0tVCYC/ORzVN+hyVcNZUWuozGcLHfod0RJCedTDTvR4nwlTXdx1gtncDqjk33itjO+27OZHbiavw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -396,9 +392,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-android-arm64": {
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.42.tgz",
|
||||
"integrity": "sha512-0cOqCubq+RWScPqvtQdjXG3Czb3AWI2CaKw3HeXry2eoA2rrPr85HF7IpdU26UWdBXgPYtlTN1LUiuXbboROhg==",
|
||||
"version": "0.14.36",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.36.tgz",
|
||||
"integrity": "sha512-/hYkyFe7x7Yapmfv4X/tBmyKnggUmdQmlvZ8ZlBnV4+PjisrEhAvC3yWpURuD9XoB8Wa1d5dGkTsF53pIvpjsg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -412,9 +408,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-darwin-64": {
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.42.tgz",
|
||||
"integrity": "sha512-ipiBdCA3ZjYgRfRLdQwP82rTiv/YVMtW36hTvAN5ZKAIfxBOyPXY7Cejp3bMXWgzKD8B6O+zoMzh01GZsCuEIA==",
|
||||
"version": "0.14.36",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.36.tgz",
|
||||
"integrity": "sha512-kkl6qmV0dTpyIMKagluzYqlc1vO0ecgpviK/7jwPbRDEv5fejRTaBBEE2KxEQbTHcLhiiDbhG7d5UybZWo/1zQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -428,9 +424,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-darwin-arm64": {
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.42.tgz",
|
||||
"integrity": "sha512-bU2tHRqTPOaoH/4m0zYHbFWpiYDmaA0gt90/3BMEFaM0PqVK/a6MA2V/ypV5PO0v8QxN6gH5hBPY4YJ2lopXgA==",
|
||||
"version": "0.14.36",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.36.tgz",
|
||||
"integrity": "sha512-q8fY4r2Sx6P0Pr3VUm//eFYKVk07C5MHcEinU1BjyFnuYz4IxR/03uBbDwluR6ILIHnZTE7AkTUWIdidRi1Jjw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -444,9 +440,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-freebsd-64": {
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.42.tgz",
|
||||
"integrity": "sha512-75h1+22Ivy07+QvxHyhVqOdekupiTZVLN1PMwCDonAqyXd8TVNJfIRFrdL8QmSJrOJJ5h8H1I9ETyl2L8LQDaw==",
|
||||
"version": "0.14.36",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.36.tgz",
|
||||
"integrity": "sha512-Hn8AYuxXXRptybPqoMkga4HRFE7/XmhtlQjXFHoAIhKUPPMeJH35GYEUWGbjteai9FLFvBAjEAlwEtSGxnqWww==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -460,9 +456,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-freebsd-arm64": {
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.42.tgz",
|
||||
"integrity": "sha512-W6Jebeu5TTDQMJUJVarEzRU9LlKpNkPBbjqSu+GUPTHDCly5zZEQq9uHkmHHl7OKm+mQ2zFySN83nmfCeZCyNA==",
|
||||
"version": "0.14.36",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.36.tgz",
|
||||
"integrity": "sha512-S3C0attylLLRiCcHiJd036eDEMOY32+h8P+jJ3kTcfhJANNjP0TNBNL30TZmEdOSx/820HJFgRrqpNAvTbjnDA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -476,9 +472,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-linux-32": {
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.42.tgz",
|
||||
"integrity": "sha512-Ooy/Bj+mJ1z4jlWcK5Dl6SlPlCgQB9zg1UrTCeY8XagvuWZ4qGPyYEWGkT94HUsRi2hKsXvcs6ThTOjBaJSMfg==",
|
||||
"version": "0.14.36",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.36.tgz",
|
||||
"integrity": "sha512-Eh9OkyTrEZn9WGO4xkI3OPPpUX7p/3QYvdG0lL4rfr73Ap2HAr6D9lP59VMF64Ex01LhHSXwIsFG/8AQjh6eNw==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -492,9 +488,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-linux-64": {
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.42.tgz",
|
||||
"integrity": "sha512-2L0HbzQfbTuemUWfVqNIjOfaTRt9zsvjnme6lnr7/MO9toz/MJ5tZhjqrG6uDWDxhsaHI2/nsDgrv8uEEN2eoA==",
|
||||
"version": "0.14.36",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.36.tgz",
|
||||
"integrity": "sha512-vFVFS5ve7PuwlfgoWNyRccGDi2QTNkQo/2k5U5ttVD0jRFaMlc8UQee708fOZA6zTCDy5RWsT5MJw3sl2X6KDg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -508,9 +504,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-linux-arm": {
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.42.tgz",
|
||||
"integrity": "sha512-STq69yzCMhdRaWnh29UYrLSr/qaWMm/KqwaRF1pMEK7kDiagaXhSL1zQGXbYv94GuGY/zAwzK98+6idCMUOOCg==",
|
||||
"version": "0.14.36",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.36.tgz",
|
||||
"integrity": "sha512-NhgU4n+NCsYgt7Hy61PCquEz5aevI6VjQvxwBxtxrooXsxt5b2xtOUXYZe04JxqQo+XZk3d1gcr7pbV9MAQ/Lg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -524,9 +520,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-linux-arm64": {
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.42.tgz",
|
||||
"integrity": "sha512-c3Ug3e9JpVr8jAcfbhirtpBauLxzYPpycjWulD71CF6ZSY26tvzmXMJYooQ2YKqDY4e/fPu5K8bm7MiXMnyxuA==",
|
||||
"version": "0.14.36",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.36.tgz",
|
||||
"integrity": "sha512-24Vq1M7FdpSmaTYuu1w0Hdhiqkbto1I5Pjyi+4Cdw5fJKGlwQuw+hWynTcRI/cOZxBcBpP21gND7W27gHAiftw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -540,9 +536,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-linux-mips64le": {
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.42.tgz",
|
||||
"integrity": "sha512-QuvpHGbYlkyXWf2cGm51LBCHx6eUakjaSrRpUqhPwjh/uvNUYvLmz2LgPTTPwCqaKt0iwL+OGVL0tXA5aDbAbg==",
|
||||
"version": "0.14.36",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.36.tgz",
|
||||
"integrity": "sha512-hZUeTXvppJN+5rEz2EjsOFM9F1bZt7/d2FUM1lmQo//rXh1RTFYzhC0txn7WV0/jCC7SvrGRaRz0NMsRPf8SIA==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
@@ -556,9 +552,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-linux-ppc64le": {
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.42.tgz",
|
||||
"integrity": "sha512-8ohIVIWDbDT+i7lCx44YCyIRrOW1MYlks9fxTo0ME2LS/fxxdoJBwHWzaDYhjvf8kNpA+MInZvyOEAGoVDrMHg==",
|
||||
"version": "0.14.36",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.36.tgz",
|
||||
"integrity": "sha512-1Bg3QgzZjO+QtPhP9VeIBhAduHEc2kzU43MzBnMwpLSZ890azr4/A9Dganun8nsqD/1TBcqhId0z4mFDO8FAvg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -572,9 +568,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-linux-riscv64": {
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.42.tgz",
|
||||
"integrity": "sha512-DzDqK3TuoXktPyG1Lwx7vhaF49Onv3eR61KwQyxYo4y5UKTpL3NmuarHSIaSVlTFDDpcIajCDwz5/uwKLLgKiQ==",
|
||||
"version": "0.14.36",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.36.tgz",
|
||||
"integrity": "sha512-dOE5pt3cOdqEhaufDRzNCHf5BSwxgygVak9UR7PH7KPVHwSTDAZHDoEjblxLqjJYpc5XaU9+gKJ9F8mp9r5I4A==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -588,9 +584,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-linux-s390x": {
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.42.tgz",
|
||||
"integrity": "sha512-YFRhPCxl8nb//Wn6SiS5pmtplBi4z9yC2gLrYoYI/tvwuB1jldir9r7JwAGy1Ck4D7sE7wBN9GFtUUX/DLdcEQ==",
|
||||
"version": "0.14.36",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.36.tgz",
|
||||
"integrity": "sha512-g4FMdh//BBGTfVHjF6MO7Cz8gqRoDPzXWxRvWkJoGroKA18G9m0wddvPbEqcQf5Tbt2vSc1CIgag7cXwTmoTXg==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -604,9 +600,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-netbsd-64": {
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.42.tgz",
|
||||
"integrity": "sha512-QYSD2k+oT9dqB/4eEM9c+7KyNYsIPgzYOSrmfNGDIyJrbT1d+CFVKvnKahDKNJLfOYj8N4MgyFaU9/Ytc6w5Vw==",
|
||||
"version": "0.14.36",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.36.tgz",
|
||||
"integrity": "sha512-UB2bVImxkWk4vjnP62ehFNZ73lQY1xcnL5ZNYF3x0AG+j8HgdkNF05v67YJdCIuUJpBuTyCK8LORCYo9onSW+A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -620,9 +616,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-openbsd-64": {
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.42.tgz",
|
||||
"integrity": "sha512-M2meNVIKWsm2HMY7+TU9AxM7ZVwI9havdsw6m/6EzdXysyCFFSoaTQ/Jg03izjCsK17FsVRHqRe26Llj6x0MNA==",
|
||||
"version": "0.14.36",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.36.tgz",
|
||||
"integrity": "sha512-NvGB2Chf8GxuleXRGk8e9zD3aSdRO5kLt9coTQbCg7WMGXeX471sBgh4kSg8pjx0yTXRt0MlrUDnjVYnetyivg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -636,9 +632,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-sunos-64": {
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.42.tgz",
|
||||
"integrity": "sha512-uXV8TAZEw36DkgW8Ak3MpSJs1ofBb3Smkc/6pZ29sCAN1KzCAQzsje4sUwugf+FVicrHvlamCOlFZIXgct+iqQ==",
|
||||
"version": "0.14.36",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.36.tgz",
|
||||
"integrity": "sha512-VkUZS5ftTSjhRjuRLp+v78auMO3PZBXu6xl4ajomGenEm2/rGuWlhFSjB7YbBNErOchj51Jb2OK8lKAo8qdmsQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -652,9 +648,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-windows-32": {
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.42.tgz",
|
||||
"integrity": "sha512-4iw/8qWmRICWi9ZOnJJf9sYt6wmtp3hsN4TdI5NqgjfOkBVMxNdM9Vt3626G1Rda9ya2Q0hjQRD9W1o+m6Lz6g==",
|
||||
"version": "0.14.36",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.36.tgz",
|
||||
"integrity": "sha512-bIar+A6hdytJjZrDxfMBUSEHHLfx3ynoEZXx/39nxy86pX/w249WZm8Bm0dtOAByAf4Z6qV0LsnTIJHiIqbw0w==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -668,9 +664,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-windows-64": {
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.42.tgz",
|
||||
"integrity": "sha512-j3cdK+Y3+a5H0wHKmLGTJcq0+/2mMBHPWkItR3vytp/aUGD/ua/t2BLdfBIzbNN9nLCRL9sywCRpOpFMx3CxzA==",
|
||||
"version": "0.14.36",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.36.tgz",
|
||||
"integrity": "sha512-+p4MuRZekVChAeueT1Y9LGkxrT5x7YYJxYE8ZOTcEfeUUN43vktSn6hUNsvxzzATrSgq5QqRdllkVBxWZg7KqQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -684,9 +680,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-windows-arm64": {
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.42.tgz",
|
||||
"integrity": "sha512-+lRAARnF+hf8J0mN27ujO+VbhPbDqJ8rCcJKye4y7YZLV6C4n3pTRThAb388k/zqF5uM0lS5O201u0OqoWSicw==",
|
||||
"version": "0.14.36",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.36.tgz",
|
||||
"integrity": "sha512-fBB4WlDqV1m18EF/aheGYQkQZHfPHiHJSBYzXIo8yKehek+0BtBwo/4PNwKGJ5T0YK0oc8pBKjgwPbzSrPLb+Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1131,9 +1127,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/linkify-it": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz",
|
||||
"integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==",
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz",
|
||||
"integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==",
|
||||
"dependencies": {
|
||||
"uc.micro": "^1.0.1"
|
||||
}
|
||||
@@ -1203,13 +1199,13 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/markdown-it": {
|
||||
"version": "13.0.1",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.1.tgz",
|
||||
"integrity": "sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==",
|
||||
"version": "12.3.2",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz",
|
||||
"integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1",
|
||||
"entities": "~3.0.1",
|
||||
"linkify-it": "^4.0.1",
|
||||
"entities": "~2.1.0",
|
||||
"linkify-it": "^3.0.1",
|
||||
"mdurl": "^1.0.1",
|
||||
"uc.micro": "^1.0.5"
|
||||
},
|
||||
@@ -1524,9 +1520,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sass": {
|
||||
"version": "1.52.1",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.52.1.tgz",
|
||||
"integrity": "sha512-fSzYTbr7z8oQnVJ3Acp9hV80dM1fkMN7mSD/25mpcct9F7FPBMOI8krEYALgU1aZoqGhQNhTPsuSmxjnIvAm4Q==",
|
||||
"version": "1.50.0",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.50.0.tgz",
|
||||
"integrity": "sha512-cLsD6MEZ5URXHStxApajEh7gW189kkjn4Rc8DQweMyF+o5HF5nfEz8QYLMlPsTOD88DknatTmBWkOcw5/LnJLQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"chokidar": ">=3.0.0 <4.0.0",
|
||||
@@ -1601,14 +1597,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/snabbdom": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/snabbdom/-/snabbdom-3.5.0.tgz",
|
||||
"integrity": "sha512-Ff5BKG18KrrPuskHJlA9aujPHqEabItaDl96l7ZZndF4zt5AYSczz7ZjjgQAX5IBd5cd25lw9NfgX21yVUJ+9g==",
|
||||
"engines": {
|
||||
"node": ">=8.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sortablejs": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.0.tgz",
|
||||
@@ -2014,9 +2002,9 @@
|
||||
}
|
||||
},
|
||||
"clipboard": {
|
||||
"version": "2.0.11",
|
||||
"resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz",
|
||||
"integrity": "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==",
|
||||
"version": "2.0.10",
|
||||
"resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.10.tgz",
|
||||
"integrity": "sha512-cz3m2YVwFz95qSEbCDi2fzLN/epEN9zXBvfgAoGkvGOJZATMl9gtTDVOtBYkx2ODUJl2kvmud7n32sV2BpYR4g==",
|
||||
"requires": {
|
||||
"good-listener": "^1.2.2",
|
||||
"select": "^1.1.2",
|
||||
@@ -2035,9 +2023,9 @@
|
||||
}
|
||||
},
|
||||
"codemirror": {
|
||||
"version": "5.65.5",
|
||||
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.5.tgz",
|
||||
"integrity": "sha512-HNyhvGLnYz5c+kIsB9QKVitiZUevha3ovbIYaQiGzKo7ECSL/elWD9RXt3JgNr0NdnyqE9/Rc/7uLfkJQL638w=="
|
||||
"version": "5.65.2",
|
||||
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.2.tgz",
|
||||
"integrity": "sha512-SZM4Zq7XEC8Fhroqe3LxbEEX1zUPWH1wMr5zxiBuiUF64iYOUH/JI88v4tBag8MiBS8B8gRv8O1pPXGYXQ4ErA=="
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "1.9.3",
|
||||
@@ -2105,9 +2093,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"entities": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz",
|
||||
"integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q=="
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz",
|
||||
"integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w=="
|
||||
},
|
||||
"error-ex": {
|
||||
"version": "1.3.2",
|
||||
@@ -2158,170 +2146,170 @@
|
||||
}
|
||||
},
|
||||
"esbuild": {
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.42.tgz",
|
||||
"integrity": "sha512-V0uPZotCEHokJdNqyozH6qsaQXqmZEOiZWrXnds/zaH/0SyrIayRXWRB98CENO73MIZ9T3HBIOsmds5twWtmgw==",
|
||||
"version": "0.14.36",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.36.tgz",
|
||||
"integrity": "sha512-HhFHPiRXGYOCRlrhpiVDYKcFJRdO0sBElZ668M4lh2ER0YgnkLxECuFe7uWCf23FrcLc59Pqr7dHkTqmRPDHmw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"esbuild-android-64": "0.14.42",
|
||||
"esbuild-android-arm64": "0.14.42",
|
||||
"esbuild-darwin-64": "0.14.42",
|
||||
"esbuild-darwin-arm64": "0.14.42",
|
||||
"esbuild-freebsd-64": "0.14.42",
|
||||
"esbuild-freebsd-arm64": "0.14.42",
|
||||
"esbuild-linux-32": "0.14.42",
|
||||
"esbuild-linux-64": "0.14.42",
|
||||
"esbuild-linux-arm": "0.14.42",
|
||||
"esbuild-linux-arm64": "0.14.42",
|
||||
"esbuild-linux-mips64le": "0.14.42",
|
||||
"esbuild-linux-ppc64le": "0.14.42",
|
||||
"esbuild-linux-riscv64": "0.14.42",
|
||||
"esbuild-linux-s390x": "0.14.42",
|
||||
"esbuild-netbsd-64": "0.14.42",
|
||||
"esbuild-openbsd-64": "0.14.42",
|
||||
"esbuild-sunos-64": "0.14.42",
|
||||
"esbuild-windows-32": "0.14.42",
|
||||
"esbuild-windows-64": "0.14.42",
|
||||
"esbuild-windows-arm64": "0.14.42"
|
||||
"esbuild-android-64": "0.14.36",
|
||||
"esbuild-android-arm64": "0.14.36",
|
||||
"esbuild-darwin-64": "0.14.36",
|
||||
"esbuild-darwin-arm64": "0.14.36",
|
||||
"esbuild-freebsd-64": "0.14.36",
|
||||
"esbuild-freebsd-arm64": "0.14.36",
|
||||
"esbuild-linux-32": "0.14.36",
|
||||
"esbuild-linux-64": "0.14.36",
|
||||
"esbuild-linux-arm": "0.14.36",
|
||||
"esbuild-linux-arm64": "0.14.36",
|
||||
"esbuild-linux-mips64le": "0.14.36",
|
||||
"esbuild-linux-ppc64le": "0.14.36",
|
||||
"esbuild-linux-riscv64": "0.14.36",
|
||||
"esbuild-linux-s390x": "0.14.36",
|
||||
"esbuild-netbsd-64": "0.14.36",
|
||||
"esbuild-openbsd-64": "0.14.36",
|
||||
"esbuild-sunos-64": "0.14.36",
|
||||
"esbuild-windows-32": "0.14.36",
|
||||
"esbuild-windows-64": "0.14.36",
|
||||
"esbuild-windows-arm64": "0.14.36"
|
||||
}
|
||||
},
|
||||
"esbuild-android-64": {
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.42.tgz",
|
||||
"integrity": "sha512-P4Y36VUtRhK/zivqGVMqhptSrFILAGlYp0Z8r9UQqHJ3iWztRCNWnlBzD9HRx0DbueXikzOiwyOri+ojAFfW6A==",
|
||||
"version": "0.14.36",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.36.tgz",
|
||||
"integrity": "sha512-jwpBhF1jmo0tVCYC/ORzVN+hyVcNZUWuozGcLHfod0RJCedTDTvR4nwlTXdx1gtncDqjk33itjO+27OZHbiavw==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-android-arm64": {
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.42.tgz",
|
||||
"integrity": "sha512-0cOqCubq+RWScPqvtQdjXG3Czb3AWI2CaKw3HeXry2eoA2rrPr85HF7IpdU26UWdBXgPYtlTN1LUiuXbboROhg==",
|
||||
"version": "0.14.36",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.36.tgz",
|
||||
"integrity": "sha512-/hYkyFe7x7Yapmfv4X/tBmyKnggUmdQmlvZ8ZlBnV4+PjisrEhAvC3yWpURuD9XoB8Wa1d5dGkTsF53pIvpjsg==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-darwin-64": {
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.42.tgz",
|
||||
"integrity": "sha512-ipiBdCA3ZjYgRfRLdQwP82rTiv/YVMtW36hTvAN5ZKAIfxBOyPXY7Cejp3bMXWgzKD8B6O+zoMzh01GZsCuEIA==",
|
||||
"version": "0.14.36",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.36.tgz",
|
||||
"integrity": "sha512-kkl6qmV0dTpyIMKagluzYqlc1vO0ecgpviK/7jwPbRDEv5fejRTaBBEE2KxEQbTHcLhiiDbhG7d5UybZWo/1zQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-darwin-arm64": {
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.42.tgz",
|
||||
"integrity": "sha512-bU2tHRqTPOaoH/4m0zYHbFWpiYDmaA0gt90/3BMEFaM0PqVK/a6MA2V/ypV5PO0v8QxN6gH5hBPY4YJ2lopXgA==",
|
||||
"version": "0.14.36",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.36.tgz",
|
||||
"integrity": "sha512-q8fY4r2Sx6P0Pr3VUm//eFYKVk07C5MHcEinU1BjyFnuYz4IxR/03uBbDwluR6ILIHnZTE7AkTUWIdidRi1Jjw==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-freebsd-64": {
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.42.tgz",
|
||||
"integrity": "sha512-75h1+22Ivy07+QvxHyhVqOdekupiTZVLN1PMwCDonAqyXd8TVNJfIRFrdL8QmSJrOJJ5h8H1I9ETyl2L8LQDaw==",
|
||||
"version": "0.14.36",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.36.tgz",
|
||||
"integrity": "sha512-Hn8AYuxXXRptybPqoMkga4HRFE7/XmhtlQjXFHoAIhKUPPMeJH35GYEUWGbjteai9FLFvBAjEAlwEtSGxnqWww==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-freebsd-arm64": {
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.42.tgz",
|
||||
"integrity": "sha512-W6Jebeu5TTDQMJUJVarEzRU9LlKpNkPBbjqSu+GUPTHDCly5zZEQq9uHkmHHl7OKm+mQ2zFySN83nmfCeZCyNA==",
|
||||
"version": "0.14.36",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.36.tgz",
|
||||
"integrity": "sha512-S3C0attylLLRiCcHiJd036eDEMOY32+h8P+jJ3kTcfhJANNjP0TNBNL30TZmEdOSx/820HJFgRrqpNAvTbjnDA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-linux-32": {
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.42.tgz",
|
||||
"integrity": "sha512-Ooy/Bj+mJ1z4jlWcK5Dl6SlPlCgQB9zg1UrTCeY8XagvuWZ4qGPyYEWGkT94HUsRi2hKsXvcs6ThTOjBaJSMfg==",
|
||||
"version": "0.14.36",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.36.tgz",
|
||||
"integrity": "sha512-Eh9OkyTrEZn9WGO4xkI3OPPpUX7p/3QYvdG0lL4rfr73Ap2HAr6D9lP59VMF64Ex01LhHSXwIsFG/8AQjh6eNw==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-linux-64": {
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.42.tgz",
|
||||
"integrity": "sha512-2L0HbzQfbTuemUWfVqNIjOfaTRt9zsvjnme6lnr7/MO9toz/MJ5tZhjqrG6uDWDxhsaHI2/nsDgrv8uEEN2eoA==",
|
||||
"version": "0.14.36",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.36.tgz",
|
||||
"integrity": "sha512-vFVFS5ve7PuwlfgoWNyRccGDi2QTNkQo/2k5U5ttVD0jRFaMlc8UQee708fOZA6zTCDy5RWsT5MJw3sl2X6KDg==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-linux-arm": {
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.42.tgz",
|
||||
"integrity": "sha512-STq69yzCMhdRaWnh29UYrLSr/qaWMm/KqwaRF1pMEK7kDiagaXhSL1zQGXbYv94GuGY/zAwzK98+6idCMUOOCg==",
|
||||
"version": "0.14.36",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.36.tgz",
|
||||
"integrity": "sha512-NhgU4n+NCsYgt7Hy61PCquEz5aevI6VjQvxwBxtxrooXsxt5b2xtOUXYZe04JxqQo+XZk3d1gcr7pbV9MAQ/Lg==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-linux-arm64": {
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.42.tgz",
|
||||
"integrity": "sha512-c3Ug3e9JpVr8jAcfbhirtpBauLxzYPpycjWulD71CF6ZSY26tvzmXMJYooQ2YKqDY4e/fPu5K8bm7MiXMnyxuA==",
|
||||
"version": "0.14.36",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.36.tgz",
|
||||
"integrity": "sha512-24Vq1M7FdpSmaTYuu1w0Hdhiqkbto1I5Pjyi+4Cdw5fJKGlwQuw+hWynTcRI/cOZxBcBpP21gND7W27gHAiftw==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-linux-mips64le": {
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.42.tgz",
|
||||
"integrity": "sha512-QuvpHGbYlkyXWf2cGm51LBCHx6eUakjaSrRpUqhPwjh/uvNUYvLmz2LgPTTPwCqaKt0iwL+OGVL0tXA5aDbAbg==",
|
||||
"version": "0.14.36",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.36.tgz",
|
||||
"integrity": "sha512-hZUeTXvppJN+5rEz2EjsOFM9F1bZt7/d2FUM1lmQo//rXh1RTFYzhC0txn7WV0/jCC7SvrGRaRz0NMsRPf8SIA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-linux-ppc64le": {
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.42.tgz",
|
||||
"integrity": "sha512-8ohIVIWDbDT+i7lCx44YCyIRrOW1MYlks9fxTo0ME2LS/fxxdoJBwHWzaDYhjvf8kNpA+MInZvyOEAGoVDrMHg==",
|
||||
"version": "0.14.36",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.36.tgz",
|
||||
"integrity": "sha512-1Bg3QgzZjO+QtPhP9VeIBhAduHEc2kzU43MzBnMwpLSZ890azr4/A9Dganun8nsqD/1TBcqhId0z4mFDO8FAvg==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-linux-riscv64": {
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.42.tgz",
|
||||
"integrity": "sha512-DzDqK3TuoXktPyG1Lwx7vhaF49Onv3eR61KwQyxYo4y5UKTpL3NmuarHSIaSVlTFDDpcIajCDwz5/uwKLLgKiQ==",
|
||||
"version": "0.14.36",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.36.tgz",
|
||||
"integrity": "sha512-dOE5pt3cOdqEhaufDRzNCHf5BSwxgygVak9UR7PH7KPVHwSTDAZHDoEjblxLqjJYpc5XaU9+gKJ9F8mp9r5I4A==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-linux-s390x": {
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.42.tgz",
|
||||
"integrity": "sha512-YFRhPCxl8nb//Wn6SiS5pmtplBi4z9yC2gLrYoYI/tvwuB1jldir9r7JwAGy1Ck4D7sE7wBN9GFtUUX/DLdcEQ==",
|
||||
"version": "0.14.36",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.36.tgz",
|
||||
"integrity": "sha512-g4FMdh//BBGTfVHjF6MO7Cz8gqRoDPzXWxRvWkJoGroKA18G9m0wddvPbEqcQf5Tbt2vSc1CIgag7cXwTmoTXg==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-netbsd-64": {
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.42.tgz",
|
||||
"integrity": "sha512-QYSD2k+oT9dqB/4eEM9c+7KyNYsIPgzYOSrmfNGDIyJrbT1d+CFVKvnKahDKNJLfOYj8N4MgyFaU9/Ytc6w5Vw==",
|
||||
"version": "0.14.36",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.36.tgz",
|
||||
"integrity": "sha512-UB2bVImxkWk4vjnP62ehFNZ73lQY1xcnL5ZNYF3x0AG+j8HgdkNF05v67YJdCIuUJpBuTyCK8LORCYo9onSW+A==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-openbsd-64": {
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.42.tgz",
|
||||
"integrity": "sha512-M2meNVIKWsm2HMY7+TU9AxM7ZVwI9havdsw6m/6EzdXysyCFFSoaTQ/Jg03izjCsK17FsVRHqRe26Llj6x0MNA==",
|
||||
"version": "0.14.36",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.36.tgz",
|
||||
"integrity": "sha512-NvGB2Chf8GxuleXRGk8e9zD3aSdRO5kLt9coTQbCg7WMGXeX471sBgh4kSg8pjx0yTXRt0MlrUDnjVYnetyivg==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-sunos-64": {
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.42.tgz",
|
||||
"integrity": "sha512-uXV8TAZEw36DkgW8Ak3MpSJs1ofBb3Smkc/6pZ29sCAN1KzCAQzsje4sUwugf+FVicrHvlamCOlFZIXgct+iqQ==",
|
||||
"version": "0.14.36",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.36.tgz",
|
||||
"integrity": "sha512-VkUZS5ftTSjhRjuRLp+v78auMO3PZBXu6xl4ajomGenEm2/rGuWlhFSjB7YbBNErOchj51Jb2OK8lKAo8qdmsQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-windows-32": {
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.42.tgz",
|
||||
"integrity": "sha512-4iw/8qWmRICWi9ZOnJJf9sYt6wmtp3hsN4TdI5NqgjfOkBVMxNdM9Vt3626G1Rda9ya2Q0hjQRD9W1o+m6Lz6g==",
|
||||
"version": "0.14.36",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.36.tgz",
|
||||
"integrity": "sha512-bIar+A6hdytJjZrDxfMBUSEHHLfx3ynoEZXx/39nxy86pX/w249WZm8Bm0dtOAByAf4Z6qV0LsnTIJHiIqbw0w==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-windows-64": {
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.42.tgz",
|
||||
"integrity": "sha512-j3cdK+Y3+a5H0wHKmLGTJcq0+/2mMBHPWkItR3vytp/aUGD/ua/t2BLdfBIzbNN9nLCRL9sywCRpOpFMx3CxzA==",
|
||||
"version": "0.14.36",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.36.tgz",
|
||||
"integrity": "sha512-+p4MuRZekVChAeueT1Y9LGkxrT5x7YYJxYE8ZOTcEfeUUN43vktSn6hUNsvxzzATrSgq5QqRdllkVBxWZg7KqQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-windows-arm64": {
|
||||
"version": "0.14.42",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.42.tgz",
|
||||
"integrity": "sha512-+lRAARnF+hf8J0mN27ujO+VbhPbDqJ8rCcJKye4y7YZLV6C4n3pTRThAb388k/zqF5uM0lS5O201u0OqoWSicw==",
|
||||
"version": "0.14.36",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.36.tgz",
|
||||
"integrity": "sha512-fBB4WlDqV1m18EF/aheGYQkQZHfPHiHJSBYzXIo8yKehek+0BtBwo/4PNwKGJ5T0YK0oc8pBKjgwPbzSrPLb+Q==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
@@ -2627,9 +2615,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"linkify-it": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz",
|
||||
"integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==",
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz",
|
||||
"integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==",
|
||||
"requires": {
|
||||
"uc.micro": "^1.0.1"
|
||||
}
|
||||
@@ -2687,13 +2675,13 @@
|
||||
"dev": true
|
||||
},
|
||||
"markdown-it": {
|
||||
"version": "13.0.1",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.1.tgz",
|
||||
"integrity": "sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==",
|
||||
"version": "12.3.2",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz",
|
||||
"integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==",
|
||||
"requires": {
|
||||
"argparse": "^2.0.1",
|
||||
"entities": "~3.0.1",
|
||||
"linkify-it": "^4.0.1",
|
||||
"entities": "~2.1.0",
|
||||
"linkify-it": "^3.0.1",
|
||||
"mdurl": "^1.0.1",
|
||||
"uc.micro": "^1.0.5"
|
||||
}
|
||||
@@ -2922,9 +2910,9 @@
|
||||
}
|
||||
},
|
||||
"sass": {
|
||||
"version": "1.52.1",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.52.1.tgz",
|
||||
"integrity": "sha512-fSzYTbr7z8oQnVJ3Acp9hV80dM1fkMN7mSD/25mpcct9F7FPBMOI8krEYALgU1aZoqGhQNhTPsuSmxjnIvAm4Q==",
|
||||
"version": "1.50.0",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.50.0.tgz",
|
||||
"integrity": "sha512-cLsD6MEZ5URXHStxApajEh7gW189kkjn4Rc8DQweMyF+o5HF5nfEz8QYLMlPsTOD88DknatTmBWkOcw5/LnJLQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chokidar": ">=3.0.0 <4.0.0",
|
||||
@@ -2981,11 +2969,6 @@
|
||||
"object-inspect": "^1.9.0"
|
||||
}
|
||||
},
|
||||
"snabbdom": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/snabbdom/-/snabbdom-3.5.0.tgz",
|
||||
"integrity": "sha512-Ff5BKG18KrrPuskHJlA9aujPHqEabItaDl96l7ZZndF4zt5AYSczz7ZjjgQAX5IBd5cd25lw9NfgX21yVUJ+9g=="
|
||||
},
|
||||
"sortablejs": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.0.tgz",
|
||||
|
||||
15
package.json
15
package.json
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build:css:dev": "sass ./resources/sass:./public/dist --embed-sources",
|
||||
"build:css:watch": "sass ./resources/sass:./public/dist --watch --embed-sources",
|
||||
"build:css:dev": "sass ./resources/sass:./public/dist",
|
||||
"build:css:watch": "sass ./resources/sass:./public/dist --watch",
|
||||
"build:css:production": "sass ./resources/sass:./public/dist -s compressed",
|
||||
"build:js:dev": "node dev/build/esbuild.js",
|
||||
"build:js:watch": "chokidar --initial \"./resources/**/*.js\" -c \"npm run build:js:dev\"",
|
||||
@@ -16,19 +16,18 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"chokidar-cli": "^3.0",
|
||||
"esbuild": "0.14.42",
|
||||
"esbuild": "0.14.36",
|
||||
"livereload": "^0.9.3",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"punycode": "^2.1.1",
|
||||
"sass": "^1.52.1"
|
||||
"sass": "^1.50.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"clipboard": "^2.0.11",
|
||||
"codemirror": "^5.65.5",
|
||||
"clipboard": "^2.0.10",
|
||||
"codemirror": "^5.65.2",
|
||||
"dropzone": "^5.9.3",
|
||||
"markdown-it": "^13.0.1",
|
||||
"markdown-it": "^12.3.2",
|
||||
"markdown-it-task-lists": "^2.1.1",
|
||||
"snabbdom": "^3.5.0",
|
||||
"sortablejs": "^1.15.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
<server name="MAIL_DRIVER" value="array"/>
|
||||
<server name="LOG_CHANNEL" value="single"/>
|
||||
<server name="AUTH_METHOD" value="standard"/>
|
||||
<server name="AUTH_AUTO_INITIATE" value="false"/>
|
||||
<server name="DISABLE_EXTERNAL_SERVICES" value="true"/>
|
||||
<server name="ALLOW_UNTRUSTED_SERVER_FETCHING" value="false"/>
|
||||
<server name="AVATAR_URL" value=""/>
|
||||
@@ -57,6 +56,5 @@
|
||||
<server name="LOG_FAILED_LOGIN_CHANNEL" value="testing"/>
|
||||
<server name="WKHTMLTOPDF" value="false"/>
|
||||
<server name="APP_DEFAULT_DARK_MODE" value="false"/>
|
||||
<server name="IP_ADDRESS_PRECISION" value="4"/>
|
||||
</php>
|
||||
</phpunit>
|
||||
|
||||
62
public/dist/app.js
vendored
62
public/dist/app.js
vendored
File diff suppressed because one or more lines are too long
32
public/dist/code.js
vendored
32
public/dist/code.js
vendored
File diff suppressed because one or more lines are too long
1
public/dist/export-styles.css
vendored
1
public/dist/export-styles.css
vendored
File diff suppressed because one or more lines are too long
1
public/dist/print-styles.css
vendored
1
public/dist/print-styles.css
vendored
@@ -1 +0,0 @@
|
||||
:root{--color-primary: #206ea7;--color-primary-light: rgba(32,110,167,0.15);--color-page: #206ea7;--color-page-draft: #7e50b1;--color-chapter: #af4d0d;--color-book: #077b70;--color-bookshelf: #a94747;--bg-disabled: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='100%25' width='100%25'%3E%3Cdefs%3E%3Cpattern id='doodad' width='19' height='19' viewBox='0 0 40 40' patternUnits='userSpaceOnUse' patternTransform='rotate(143)'%3E%3Crect width='100%25' height='100%25' fill='rgba(42, 67, 101,0)'/%3E%3Cpath d='M-10 30h60v20h-60zM-10-10h60v20h-60' fill='rgba(26, 32, 44,0)'/%3E%3Cpath d='M-10 10h60v20h-60zM-10-30h60v20h-60z' fill='rgba(0, 0, 0,0.05)'/%3E%3C/pattern%3E%3C/defs%3E%3Crect fill='url(%23doodad)' height='200%25' width='200%25'/%3E%3C/svg%3E")}:root.dark-mode{--bg-disabled: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='100%25' width='100%25'%3E%3Cdefs%3E%3Cpattern id='doodad' width='19' height='19' viewBox='0 0 40 40' patternUnits='userSpaceOnUse' patternTransform='rotate(143)'%3E%3Crect width='100%25' height='100%25' fill='rgba(42, 67, 101,0)'/%3E%3Cpath d='M-10 30h60v20h-60zM-10-10h60v20h-60' fill='rgba(26, 32, 44,0)'/%3E%3Cpath d='M-10 10h60v20h-60zM-10-30h60v20h-60z' fill='rgba(255, 255, 255,0.05)'/%3E%3C/pattern%3E%3C/defs%3E%3Crect fill='url(%23doodad)' height='200%25' width='200%25'/%3E%3C/svg%3E")}header{display:none}html,body{font-size:12px;background-color:#fff}.page-content{margin:0 auto}.print-hidden{display:none !important}.tri-layout-container{grid-template-columns:1fr;grid-template-areas:"b";margin-inline-start:0;margin-inline-end:0;display:block}.card{box-shadow:none}.content-wrap.card{padding-inline-start:0;padding-inline-end:0}/*# sourceMappingURL=print-styles.css.map */
|
||||
1
public/dist/styles.css
vendored
1
public/dist/styles.css
vendored
File diff suppressed because one or more lines are too long
@@ -1,17 +0,0 @@
|
||||
|
||||
|
||||
### Srcdoc usage
|
||||
|
||||
By default, as of tiny 6, the editor would use srcdoc which prevents cookies being sent with images in Firefox as
|
||||
it's considered cross origin. This removes that usage to work around this case:
|
||||
|
||||
[Relevant TinyMCE issue](https://github.com/tinymce/tinymce/issues/7746).
|
||||
|
||||
Source code change applied:
|
||||
|
||||
```javascript
|
||||
// Find:
|
||||
t.srcdoc=e.iframeHTML
|
||||
// Replace:
|
||||
t.contentDocument.open();t.contentDocument.write(e.iframeHTML);t.contentDocument.close();
|
||||
```
|
||||
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user