Merge branch 'development' into release

This commit is contained in:
Dan Brown 2023-02-26 11:02:56 +00:00
commit 8c82aaabd6
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
741 changed files with 5191 additions and 4673 deletions

View File

@ -80,6 +80,9 @@ MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
# Command to use when email is sent via sendmail
MAIL_SENDMAIL_COMMAND="/usr/sbin/sendmail -bs"
# Cache & Session driver to use
# Can be 'file', 'database', 'memcached' or 'redis'
CACHE_DRIVER=file

View File

@ -308,3 +308,6 @@ Adrian Ocneanu (aocneanu) :: Romanian
Eduardo Castanho (EduardoCastanho) :: Portuguese
VIET NAM VPS (vietnamvps) :: Vietnamese
m4tthi4s :: French
toras9000 :: Japanese
pathab :: German
MichelSchoon85 :: Dutch

View File

@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-22.04
strategy:
matrix:
php: ['7.4', '8.0', '8.1', '8.2']
php: ['8.0', '8.1', '8.2']
steps:
- uses: actions/checkout@v1

View File

@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-22.04
strategy:
matrix:
php: ['7.4', '8.0', '8.1', '8.2']
php: ['8.0', '8.1', '8.2']
steps:
- uses: actions/checkout@v1

1
.gitignore vendored
View File

@ -11,6 +11,7 @@ yarn-error.log
/public/js/*.map
/public/bower
/public/build/
/public/favicon.ico
/storage/images
_ide_helper.php
/storage/debugbar

View File

@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2015-2022, Dan Brown and the BookStack Project contributors.
Copyright (c) 2015-2023, Dan Brown and the BookStack Project contributors.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@ -158,6 +158,11 @@ class PermissionApplicator
$query->select('id')->from('pages')
->whereColumn('pages.id', '=', $fullPageIdColumn)
->where('pages.draft', '=', false);
})->orWhereExists(function (QueryBuilder $query) use ($fullPageIdColumn) {
$query->select('id')->from('pages')
->whereColumn('pages.id', '=', $fullPageIdColumn)
->where('pages.draft', '=', true)
->where('pages.created_by', '=', $this->currentUser()->id);
});
});
}

View File

@ -12,11 +12,8 @@ use Illuminate\Database\Eloquent\Collection;
class PermissionsRepo
{
protected JointPermissionBuilder $permissionBuilder;
protected $systemRoles = ['admin', 'public'];
protected array $systemRoles = ['admin', 'public'];
/**
* PermissionsRepo constructor.
*/
public function __construct(JointPermissionBuilder $permissionBuilder)
{
$this->permissionBuilder = $permissionBuilder;
@ -41,7 +38,7 @@ class PermissionsRepo
/**
* Get a role via its ID.
*/
public function getRoleById($id): Role
public function getRoleById(int $id): Role
{
return Role::query()->findOrFail($id);
}
@ -52,10 +49,10 @@ class PermissionsRepo
public function saveNewRole(array $roleData): Role
{
$role = new Role($roleData);
$role->mfa_enforced = ($roleData['mfa_enforced'] ?? 'false') === 'true';
$role->mfa_enforced = boolval($roleData['mfa_enforced'] ?? false);
$role->save();
$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
$permissions = $roleData['permissions'] ?? [];
$this->assignRolePermissions($role, $permissions);
$this->permissionBuilder->rebuildForRole($role);
@ -66,42 +63,45 @@ class PermissionsRepo
/**
* Updates an existing role.
* Ensure Admin role always have core permissions.
* Ensures Admin system role always have core permissions.
*/
public function updateRole($roleId, array $roleData)
public function updateRole($roleId, array $roleData): Role
{
$role = $this->getRoleById($roleId);
$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
if (isset($roleData['permissions'])) {
$this->assignRolePermissions($role, $roleData['permissions']);
}
$role->fill($roleData);
$role->save();
$this->permissionBuilder->rebuildForRole($role);
Activity::add(ActivityType::ROLE_UPDATE, $role);
return $role;
}
/**
* Assign a list of permission names to the given role.
*/
protected function assignRolePermissions(Role $role, array $permissionNameArray = []): void
{
$permissions = [];
$permissionNameArray = array_values($permissionNameArray);
// Ensure the admin system role retains vital system permissions
if ($role->system_name === 'admin') {
$permissions = array_merge($permissions, [
$permissionNameArray = array_unique(array_merge($permissionNameArray, [
'users-manage',
'user-roles-manage',
'restrictions-manage-all',
'restrictions-manage-own',
'settings-manage',
]);
]));
}
$this->assignRolePermissions($role, $permissions);
$role->fill($roleData);
$role->mfa_enforced = ($roleData['mfa_enforced'] ?? 'false') === 'true';
$role->save();
$this->permissionBuilder->rebuildForRole($role);
Activity::add(ActivityType::ROLE_UPDATE, $role);
}
/**
* Assign a list of permission names to a role.
*/
protected function assignRolePermissions(Role $role, array $permissionNameArray = [])
{
$permissions = [];
$permissionNameArray = array_values($permissionNameArray);
if ($permissionNameArray) {
if (!empty($permissionNameArray)) {
$permissions = RolePermission::query()
->whereIn('name', $permissionNameArray)
->pluck('id')
@ -114,13 +114,13 @@ class PermissionsRepo
/**
* Delete a role from the system.
* Check it's not an admin role or set as default before deleting.
* If an migration Role ID is specified the users assign to the current role
* If a migration Role ID is specified the users assign to the current role
* will be added to the role of the specified id.
*
* @throws PermissionsException
* @throws Exception
*/
public function deleteRole($roleId, $migrateRoleId)
public function deleteRole(int $roleId, int $migrateRoleId = 0): void
{
$role = $this->getRoleById($roleId);
@ -131,7 +131,7 @@ class PermissionsRepo
throw new PermissionsException(trans('errors.role_registration_default_cannot_delete'));
}
if ($migrateRoleId) {
if ($migrateRoleId !== 0) {
$newRole = Role::query()->find($migrateRoleId);
if ($newRole) {
$users = $role->users()->pluck('id')->toArray();

View File

@ -8,6 +8,8 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany;
/**
* @property int $id
* @property string $name
* @property string $display_name
*/
class RolePermission extends Model
{

View File

@ -27,10 +27,14 @@ class Role extends Model implements Loggable
{
use HasFactory;
protected $fillable = ['display_name', 'description', 'external_auth_id'];
protected $fillable = ['display_name', 'description', 'external_auth_id', 'mfa_enforced'];
protected $hidden = ['pivot'];
protected $casts = [
'mfa_enforced' => 'boolean',
];
/**
* The roles that belong to the role.
*/
@ -107,7 +111,13 @@ class Role extends Model implements Loggable
*/
public static function getSystemRole(string $systemName): ?self
{
return static::query()->where('system_name', '=', $systemName)->first();
static $cache = [];
if (!isset($cache[$systemName])) {
$cache[$systemName] = static::query()->where('system_name', '=', $systemName)->first();
}
return $cache[$systemName];
}
/**

View File

@ -72,7 +72,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
*/
protected $hidden = [
'password', 'remember_token', 'system_name', 'email_confirmed', 'external_auth_id', 'email',
'created_at', 'updated_at', 'image_id', 'roles', 'avatar', 'user_id',
'created_at', 'updated_at', 'image_id', 'roles', 'avatar', 'user_id', 'pivot',
];
/**

View File

@ -8,6 +8,8 @@
* Do not edit this file unless you're happy to maintain any changes yourself.
*/
use Illuminate\Support\Facades\Facade;
return [
// The environment to run BookStack in.
@ -98,7 +100,13 @@ return [
// Encryption cipher
'cipher' => 'AES-256-CBC',
// Application Services Provides
// Maintenance Mode Driver
'maintenance' => [
'driver' => 'file',
// 'store' => 'redis',
],
// Application Service Providers
'providers' => [
// Laravel Framework Service Providers...
@ -141,58 +149,9 @@ return [
BookStack\Providers\ViewTweaksServiceProvider::class,
],
/*
|--------------------------------------------------------------------------
| Class Aliases
|--------------------------------------------------------------------------
|
| This array of class aliases will be registered when this application
| is started. However, feel free to register as many as you wish as
| the aliases are "lazy" loaded so they don't hinder performance.
|
*/
// Class aliases, Registered on application start
'aliases' => [
// Laravel
'App' => Illuminate\Support\Facades\App::class,
'Arr' => Illuminate\Support\Arr::class,
'Artisan' => Illuminate\Support\Facades\Artisan::class,
'Auth' => Illuminate\Support\Facades\Auth::class,
'Blade' => Illuminate\Support\Facades\Blade::class,
'Bus' => Illuminate\Support\Facades\Bus::class,
'Cache' => Illuminate\Support\Facades\Cache::class,
'Config' => Illuminate\Support\Facades\Config::class,
'Cookie' => Illuminate\Support\Facades\Cookie::class,
'Crypt' => Illuminate\Support\Facades\Crypt::class,
'Date' => Illuminate\Support\Facades\Date::class,
'DB' => Illuminate\Support\Facades\DB::class,
'Eloquent' => Illuminate\Database\Eloquent\Model::class,
'Event' => Illuminate\Support\Facades\Event::class,
'File' => Illuminate\Support\Facades\File::class,
'Gate' => Illuminate\Support\Facades\Gate::class,
'Hash' => Illuminate\Support\Facades\Hash::class,
'Http' => Illuminate\Support\Facades\Http::class,
'Lang' => Illuminate\Support\Facades\Lang::class,
'Log' => Illuminate\Support\Facades\Log::class,
'Mail' => Illuminate\Support\Facades\Mail::class,
'Notification' => Illuminate\Support\Facades\Notification::class,
'Password' => Illuminate\Support\Facades\Password::class,
'Queue' => Illuminate\Support\Facades\Queue::class,
'RateLimiter' => Illuminate\Support\Facades\RateLimiter::class,
'Redirect' => Illuminate\Support\Facades\Redirect::class,
// 'Redis' => Illuminate\Support\Facades\Redis::class,
'Request' => Illuminate\Support\Facades\Request::class,
'Response' => Illuminate\Support\Facades\Response::class,
'Route' => Illuminate\Support\Facades\Route::class,
'Schema' => Illuminate\Support\Facades\Schema::class,
'Session' => Illuminate\Support\Facades\Session::class,
'Storage' => Illuminate\Support\Facades\Storage::class,
'Str' => Illuminate\Support\Str::class,
'URL' => Illuminate\Support\Facades\URL::class,
'Validator' => Illuminate\Support\Facades\Validator::class,
'View' => Illuminate\Support\Facades\View::class,
// Class Aliases
// This array of class aliases to be registered on application start.
'aliases' => Facade::defaultAliases()->merge([
// Laravel Packages
'Socialite' => Laravel\Socialite\Facades\Socialite::class,
@ -202,7 +161,7 @@ return [
// Custom BookStack
'Activity' => BookStack\Facades\Activity::class,
'Theme' => BookStack\Facades\Theme::class,
],
])->toArray(),
// Proxy configuration
'proxies' => env('APP_PROXIES', ''),

View File

@ -14,7 +14,7 @@ return [
// This option controls the default broadcaster that will be used by the
// framework when an event needs to be broadcast. This can be set to
// any of the connections defined in the "connections" array below.
'default' => env('BROADCAST_DRIVER', 'pusher'),
'default' => 'null',
// Broadcast Connections
// Here you may define all of the broadcast connections that will be used
@ -22,21 +22,7 @@ return [
// each available type of connection are provided inside this array.
'connections' => [
'pusher' => [
'driver' => 'pusher',
'key' => env('PUSHER_APP_KEY'),
'secret' => env('PUSHER_APP_SECRET'),
'app_id' => env('PUSHER_APP_ID'),
'options' => [
'cluster' => env('PUSHER_APP_CLUSTER'),
'useTLS' => true,
],
],
'redis' => [
'driver' => 'redis',
'connection' => 'default',
],
// Default options removed since we don't use broadcasting.
'log' => [
'driver' => 'log',

View File

@ -87,6 +87,6 @@ return [
|
*/
'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_') . '_cache'),
'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_') . '_cache_'),
];

View File

@ -33,17 +33,20 @@ return [
'driver' => 'local',
'root' => public_path(),
'visibility' => 'public',
'throw' => true,
],
'local_secure_attachments' => [
'driver' => 'local',
'root' => storage_path('uploads/files/'),
'throw' => true,
],
'local_secure_images' => [
'driver' => 'local',
'root' => storage_path('uploads/images/'),
'visibility' => 'public',
'throw' => true,
],
's3' => [
@ -54,6 +57,7 @@ return [
'bucket' => env('STORAGE_S3_BUCKET', 'your-bucket'),
'endpoint' => env('STORAGE_S3_ENDPOINT', null),
'use_path_style_endpoint' => env('STORAGE_S3_ENDPOINT', null) !== null,
'throw' => true,
],
],

View File

@ -21,6 +21,15 @@ return [
// one of the channels defined in the "channels" configuration array.
'default' => env('LOG_CHANNEL', 'single'),
// Deprecations Log Channel
// This option controls the log channel that should be used to log warnings
// regarding deprecated PHP and library features. This allows you to get
// your application ready for upcoming major versions of dependencies.
'deprecations' => [
'channel' => 'null',
'trace' => false,
],
// Log Channels
// Here you may configure the log channels for your application. Out of
// the box, Laravel uses the Monolog PHP logging library. This gives

View File

@ -14,13 +14,7 @@ return [
// From Laravel 7+ this is MAIL_MAILER in laravel.
// Kept as MAIL_DRIVER in BookStack to prevent breaking change.
// Options: smtp, sendmail, log, array
'driver' => env('MAIL_DRIVER', 'smtp'),
// SMTP host address
'host' => env('MAIL_HOST', 'smtp.mailgun.org'),
// SMTP host port
'port' => env('MAIL_PORT', 587),
'default' => env('MAIL_DRIVER', 'smtp'),
// Global "From" address & name
'from' => [
@ -28,17 +22,42 @@ return [
'name' => env('MAIL_FROM_NAME', 'BookStack'),
],
// Email encryption protocol
'encryption' => env('MAIL_ENCRYPTION', 'tls'),
// Mailer Configurations
// Available mailing methods and their settings.
'mailers' => [
'smtp' => [
'transport' => 'smtp',
'host' => env('MAIL_HOST', 'smtp.mailgun.org'),
'port' => env('MAIL_PORT', 587),
'encryption' => env('MAIL_ENCRYPTION', 'tls'),
'username' => env('MAIL_USERNAME'),
'password' => env('MAIL_PASSWORD'),
'timeout' => null,
'local_domain' => env('MAIL_EHLO_DOMAIN'),
],
// SMTP server username
'username' => env('MAIL_USERNAME'),
'sendmail' => [
'transport' => 'sendmail',
'path' => env('MAIL_SENDMAIL_COMMAND', '/usr/sbin/sendmail -bs'),
],
// SMTP server password
'password' => env('MAIL_PASSWORD'),
'log' => [
'transport' => 'log',
'channel' => env('MAIL_LOG_CHANNEL'),
],
// Sendmail application path
'sendmail' => '/usr/sbin/sendmail -bs',
'array' => [
'transport' => 'array',
],
'failover' => [
'transport' => 'failover',
'mailers' => [
'smtp',
'log',
],
],
],
// Email markdown configuration
'markdown' => [
@ -47,11 +66,4 @@ return [
resource_path('views/vendor/mail'),
],
],
// Log Channel
// If you are using the "log" driver, you may specify the logging channel
// if you prefer to keep mail messages separate from other log entries
// for simpler reading. Otherwise, the default channel will be used.
'log_channel' => env('MAIL_LOG_CHANNEL'),
];

View File

@ -2,18 +2,18 @@
namespace BookStack\Entities\Tools\Markdown;
use League\CommonMark\Block\Element\AbstractBlock;
use League\CommonMark\Block\Element\ListItem;
use League\CommonMark\Block\Element\Paragraph;
use League\CommonMark\Block\Renderer\BlockRendererInterface;
use League\CommonMark\Block\Renderer\ListItemRenderer;
use League\CommonMark\ElementRendererInterface;
use League\CommonMark\Extension\CommonMark\Node\Block\ListItem;
use League\CommonMark\Extension\CommonMark\Renderer\Block\ListItemRenderer;
use League\CommonMark\Extension\TaskList\TaskListItemMarker;
use League\CommonMark\HtmlElement;
use League\CommonMark\Node\Block\Paragraph;
use League\CommonMark\Node\Node;
use League\CommonMark\Renderer\ChildNodeRendererInterface;
use League\CommonMark\Renderer\NodeRendererInterface;
use League\CommonMark\Util\HtmlElement;
class CustomListItemRenderer implements BlockRendererInterface
class CustomListItemRenderer implements NodeRendererInterface
{
protected $baseRenderer;
protected ListItemRenderer $baseRenderer;
public function __construct()
{
@ -23,11 +23,11 @@ class CustomListItemRenderer implements BlockRendererInterface
/**
* @return HtmlElement|string|null
*/
public function render(AbstractBlock $block, ElementRendererInterface $htmlRenderer, bool $inTightList = false)
public function render(Node $node, ChildNodeRendererInterface $childRenderer)
{
$listItem = $this->baseRenderer->render($block, $htmlRenderer, $inTightList);
$listItem = $this->baseRenderer->render($node, $childRenderer);
if ($this->startsTaskListItem($block)) {
if ($node instanceof ListItem && $this->startsTaskListItem($node) && $listItem instanceof HtmlElement) {
$listItem->setAttribute('class', 'task-list-item');
}

View File

@ -2,16 +2,16 @@
namespace BookStack\Entities\Tools\Markdown;
use League\CommonMark\ConfigurableEnvironmentInterface;
use League\CommonMark\Environment\EnvironmentBuilderInterface;
use League\CommonMark\Extension\ExtensionInterface;
use League\CommonMark\Extension\Strikethrough\Strikethrough;
use League\CommonMark\Extension\Strikethrough\StrikethroughDelimiterProcessor;
class CustomStrikeThroughExtension implements ExtensionInterface
{
public function register(ConfigurableEnvironmentInterface $environment)
public function register(EnvironmentBuilderInterface $environment): void
{
$environment->addDelimiterProcessor(new StrikethroughDelimiterProcessor());
$environment->addInlineRenderer(Strikethrough::class, new CustomStrikethroughRenderer());
$environment->addRenderer(Strikethrough::class, new CustomStrikethroughRenderer());
}
}

View File

@ -2,25 +2,23 @@
namespace BookStack\Entities\Tools\Markdown;
use League\CommonMark\ElementRendererInterface;
use League\CommonMark\Extension\Strikethrough\Strikethrough;
use League\CommonMark\HtmlElement;
use League\CommonMark\Inline\Element\AbstractInline;
use League\CommonMark\Inline\Renderer\InlineRendererInterface;
use League\CommonMark\Node\Node;
use League\CommonMark\Renderer\ChildNodeRendererInterface;
use League\CommonMark\Renderer\NodeRendererInterface;
use League\CommonMark\Util\HtmlElement;
/**
* This is a somewhat clone of the League\CommonMark\Extension\Strikethrough\StrikethroughRender
* class but modified slightly to use <s> HTML tags instead of <del> in order to
* match front-end markdown-it rendering.
*/
class CustomStrikethroughRenderer implements InlineRendererInterface
class CustomStrikethroughRenderer implements NodeRendererInterface
{
public function render(AbstractInline $inline, ElementRendererInterface $htmlRenderer)
public function render(Node $node, ChildNodeRendererInterface $childRenderer)
{
if (!($inline instanceof Strikethrough)) {
throw new \InvalidArgumentException('Incompatible inline type: ' . get_class($inline));
}
Strikethrough::assertInstanceOf($node);
return new HtmlElement('s', $inline->getData('attributes', []), $htmlRenderer->renderInlines($inline->children()));
return new HtmlElement('s', $node->data->get('attributes'), $childRenderer->renderNodes($node->children()));
}
}

View File

@ -4,8 +4,9 @@ namespace BookStack\Entities\Tools\Markdown;
use BookStack\Facades\Theme;
use BookStack\Theming\ThemeEvents;
use League\CommonMark\Block\Element\ListItem;
use League\CommonMark\Environment;
use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\Extension\CommonMark\Node\Block\ListItem;
use League\CommonMark\Extension\Table\TableExtension;
use League\CommonMark\Extension\TaskList\TaskListExtension;
use League\CommonMark\MarkdownConverter;
@ -21,15 +22,16 @@ class MarkdownToHtml
public function convert(): string
{
$environment = Environment::createCommonMarkEnvironment();
$environment = new Environment();
$environment->addExtension(new CommonMarkCoreExtension());
$environment->addExtension(new TableExtension());
$environment->addExtension(new TaskListExtension());
$environment->addExtension(new CustomStrikeThroughExtension());
$environment = Theme::dispatch(ThemeEvents::COMMONMARK_ENVIRONMENT_CONFIGURE, $environment) ?? $environment;
$converter = new MarkdownConverter($environment);
$environment->addBlockRenderer(ListItem::class, new CustomListItemRenderer(), 10);
$environment->addRenderer(ListItem::class, new CustomListItemRenderer(), 10);
return $converter->convertToHtml($this->markdown);
return $converter->convert($this->markdown)->getContent();
}
}

View File

@ -19,20 +19,15 @@ use Illuminate\Support\Str;
class PageContent
{
protected Page $page;
/**
* PageContent constructor.
*/
public function __construct(Page $page)
{
$this->page = $page;
public function __construct(
protected Page $page
) {
}
/**
* Update the content of the page with new provided HTML.
*/
public function setNewHTML(string $html)
public function setNewHTML(string $html): void
{
$html = $this->extractBase64ImagesFromHtml($html);
$this->page->html = $this->formatHtml($html);
@ -43,7 +38,7 @@ class PageContent
/**
* Update the content of the page with new provided Markdown content.
*/
public function setNewMarkdown(string $markdown)
public function setNewMarkdown(string $markdown): void
{
$markdown = $this->extractBase64ImagesFromMarkdown($markdown);
$this->page->markdown = $markdown;
@ -57,7 +52,7 @@ class PageContent
*/
protected function extractBase64ImagesFromHtml(string $htmlText): string
{
if (empty($htmlText) || strpos($htmlText, 'data:image') === false) {
if (empty($htmlText) || !str_contains($htmlText, 'data:image')) {
return $htmlText;
}
@ -91,7 +86,7 @@ class PageContent
* Attempting to capture the whole data uri using regex can cause PHP
* PCRE limits to be hit with larger, multi-MB, files.
*/
protected function extractBase64ImagesFromMarkdown(string $markdown)
protected function extractBase64ImagesFromMarkdown(string $markdown): string
{
$matches = [];
$contentLength = strlen($markdown);
@ -183,32 +178,13 @@ class PageContent
$childNodes = $body->childNodes;
$xPath = new DOMXPath($doc);
// Set ids on top-level nodes
// Map to hold used ID references
$idMap = [];
foreach ($childNodes as $index => $childNode) {
[$oldId, $newId] = $this->setUniqueId($childNode, $idMap);
if ($newId && $newId !== $oldId) {
$this->updateLinks($xPath, '#' . $oldId, '#' . $newId);
}
}
// Map to hold changing ID references
$changeMap = [];
// Set ids on nested header nodes
$nestedHeaders = $xPath->query('//body//*//h1|//body//*//h2|//body//*//h3|//body//*//h4|//body//*//h5|//body//*//h6');
foreach ($nestedHeaders as $nestedHeader) {
[$oldId, $newId] = $this->setUniqueId($nestedHeader, $idMap);
if ($newId && $newId !== $oldId) {
$this->updateLinks($xPath, '#' . $oldId, '#' . $newId);
}
}
// Ensure no duplicate ids within child items
$idElems = $xPath->query('//body//*//*[@id]');
foreach ($idElems as $domElem) {
[$oldId, $newId] = $this->setUniqueId($domElem, $idMap);
if ($newId && $newId !== $oldId) {
$this->updateLinks($xPath, '#' . $oldId, '#' . $newId);
}
}
$this->updateIdsRecursively($body, 0, $idMap, $changeMap);
$this->updateLinks($xPath, $changeMap);
// Generate inner html as a string
$html = '';
@ -223,20 +199,53 @@ class PageContent
}
/**
* Update the all links to the $old location to instead point to $new.
* For the given DOMNode, traverse its children recursively and update IDs
* where required (Top-level, headers & elements with IDs).
* Will update the provided $changeMap array with changes made, where keys are the old
* ids and the corresponding values are the new ids.
*/
protected function updateLinks(DOMXPath $xpath, string $old, string $new)
protected function updateIdsRecursively(DOMNode $element, int $depth, array &$idMap, array &$changeMap): void
{
$old = str_replace('"', '', $old);
$matchingLinks = $xpath->query('//body//*//*[@href="' . $old . '"]');
foreach ($matchingLinks as $domElem) {
$domElem->setAttribute('href', $new);
/* @var DOMNode $child */
foreach ($element->childNodes as $child) {
if ($child instanceof DOMElement && ($depth === 0 || in_array($child->nodeName, ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']) || $child->getAttribute('id'))) {
[$oldId, $newId] = $this->setUniqueId($child, $idMap);
if ($newId && $newId !== $oldId && !isset($idMap[$oldId])) {
$changeMap[$oldId] = $newId;
}
}
if ($child->hasChildNodes()) {
$this->updateIdsRecursively($child, $depth + 1, $idMap, $changeMap);
}
}
}
/**
* Update the all links in the given xpath to apply requires changes within the
* given $changeMap array.
*/
protected function updateLinks(DOMXPath $xpath, array $changeMap): void
{
if (empty($changeMap)) {
return;
}
$links = $xpath->query('//body//*//*[@href]');
/** @var DOMElement $domElem */
foreach ($links as $domElem) {
$href = ltrim($domElem->getAttribute('href'), '#');
$newHref = $changeMap[$href] ?? null;
if ($newHref) {
$domElem->setAttribute('href', '#' . $newHref);
}
}
}
/**
* Set a unique id on the given DOMElement.
* A map for existing ID's should be passed in to check for current existence.
* A map for existing ID's should be passed in to check for current existence,
* and this will be updated with any new IDs set upon elements.
* Returns a pair of strings in the format [old_id, new_id].
*/
protected function setUniqueId(DOMNode $element, array &$idMap): array
@ -247,7 +256,7 @@ class PageContent
// Stop if there's an existing valid id that has not already been used.
$existingId = $element->getAttribute('id');
if (strpos($existingId, 'bkmrk') === 0 && !isset($idMap[$existingId])) {
if (str_starts_with($existingId, 'bkmrk') && !isset($idMap[$existingId])) {
$idMap[$existingId] = true;
return [$existingId, $existingId];
@ -258,7 +267,7 @@ class PageContent
// the same content is passed through.
$contentId = 'bkmrk-' . mb_substr(strtolower(preg_replace('/\s+/', '-', trim($element->nodeValue))), 0, 20);
$newId = urlencode($contentId);
$loopIndex = 0;
$loopIndex = 1;
while (isset($idMap[$newId])) {
$newId = urlencode($contentId . '-' . $loopIndex);
@ -440,8 +449,8 @@ class PageContent
{
libxml_use_internal_errors(true);
$doc = new DOMDocument();
$html = '<body>' . $html . '</body>';
$doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
$html = '<?xml encoding="utf-8" ?><body>' . $html . '</body>';
$doc->loadHTML($html);
return $doc;
}

View File

@ -2,25 +2,18 @@
namespace BookStack\Exceptions;
use Whoops\Handler\Handler;
use Illuminate\Contracts\Foundation\ExceptionRenderer;
class WhoopsBookStackPrettyHandler extends Handler
class BookStackExceptionHandlerPage implements ExceptionRenderer
{
/**
* @return int|null A handler may return nothing, or a Handler::HANDLE_* constant
*/
public function handle()
public function render($throwable)
{
$exception = $this->getException();
echo view('errors.debug', [
'error' => $exception->getMessage(),
'errorClass' => get_class($exception),
'trace' => $exception->getTraceAsString(),
return view('errors.debug', [
'error' => $throwable->getMessage(),
'errorClass' => get_class($throwable),
'trace' => $throwable->getTraceAsString(),
'environment' => $this->getEnvironment(),
])->render();
return Handler::QUIT;
}
protected function safeReturn(callable $callback, $default = null)

View File

@ -17,7 +17,7 @@ class Handler extends ExceptionHandler
/**
* A list of the exception types that are not reported.
*
* @var array
* @var array<int, class-string<\Throwable>>
*/
protected $dontReport = [
NotFoundException::class,
@ -25,9 +25,9 @@ class Handler extends ExceptionHandler
];
/**
* A list of the inputs that are never flashed for validation exceptions.
* A list of the inputs that are never flashed to the session on validation exceptions.
*
* @var array
* @var array<int, string>
*/
protected $dontFlash = [
'current_password',
@ -98,6 +98,7 @@ class Handler extends ExceptionHandler
];
if ($e instanceof ValidationException) {
$responseData['error']['message'] = 'The given data was invalid.';
$responseData['error']['validation'] = $e->errors();
$code = $e->status;
}

View File

@ -32,10 +32,15 @@ abstract class ApiController extends Controller
*/
public function getValidationRules(): array
{
if (method_exists($this, 'rules')) {
return $this->rules();
}
return $this->rules();
}
/**
* Get the validation rules for the actions in this controller.
* Defaults to a $rules property but can be a rules() method.
*/
protected function rules(): array
{
return $this->rules;
}
}

View File

@ -13,11 +13,9 @@ use Illuminate\Validation\ValidationException;
class AttachmentApiController extends ApiController
{
protected $attachmentService;
public function __construct(AttachmentService $attachmentService)
{
$this->attachmentService = $attachmentService;
public function __construct(
protected AttachmentService $attachmentService
) {
}
/**
@ -174,13 +172,13 @@ class AttachmentApiController extends ApiController
'name' => ['required', 'min:1', 'max:255', 'string'],
'uploaded_to' => ['required', 'integer', 'exists:pages,id'],
'file' => array_merge(['required_without:link'], $this->attachmentService->getFileValidationRules()),
'link' => ['required_without:file', 'min:1', 'max:255', 'safe_url'],
'link' => ['required_without:file', 'min:1', 'max:2000', 'safe_url'],
],
'update' => [
'name' => ['min:1', 'max:255', 'string'],
'uploaded_to' => ['integer', 'exists:pages,id'],
'file' => $this->attachmentService->getFileValidationRules(),
'link' => ['min:1', 'max:255', 'safe_url'],
'link' => ['min:1', 'max:2000', 'safe_url'],
],
];
}

View File

@ -0,0 +1,136 @@
<?php
namespace BookStack\Http\Controllers\Api;
use BookStack\Auth\Permissions\PermissionsRepo;
use BookStack\Auth\Role;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class RoleApiController extends ApiController
{
protected PermissionsRepo $permissionsRepo;
protected array $fieldsToExpose = [
'display_name', 'description', 'mfa_enforced', 'external_auth_id', 'created_at', 'updated_at',
];
protected $rules = [
'create' => [
'display_name' => ['required', 'string', 'min:3', 'max:180'],
'description' => ['string', 'max:180'],
'mfa_enforced' => ['boolean'],
'external_auth_id' => ['string'],
'permissions' => ['array'],
'permissions.*' => ['string'],
],
'update' => [
'display_name' => ['string', 'min:3', 'max:180'],
'description' => ['string', 'max:180'],
'mfa_enforced' => ['boolean'],
'external_auth_id' => ['string'],
'permissions' => ['array'],
'permissions.*' => ['string'],
]
];
public function __construct(PermissionsRepo $permissionsRepo)
{
$this->permissionsRepo = $permissionsRepo;
// Checks for all endpoints in this controller
$this->middleware(function ($request, $next) {
$this->checkPermission('user-roles-manage');
return $next($request);
});
}
/**
* Get a listing of roles in the system.
* Requires permission to manage roles.
*/
public function list()
{
$roles = Role::query()->select(['*'])
->withCount(['users', 'permissions']);
return $this->apiListingResponse($roles, [
...$this->fieldsToExpose,
'permissions_count',
'users_count',
]);
}
/**
* Create a new role in the system.
* Permissions should be provided as an array of permission name strings.
* Requires permission to manage roles.
*/
public function create(Request $request)
{
$data = $this->validate($request, $this->rules()['create']);
$role = null;
DB::transaction(function () use ($data, &$role) {
$role = $this->permissionsRepo->saveNewRole($data);
});
$this->singleFormatter($role);
return response()->json($role);
}
/**
* View the details of a single role.
* Provides the permissions and a high-level list of the users assigned.
* Requires permission to manage roles.
*/
public function read(string $id)
{
$user = $this->permissionsRepo->getRoleById($id);
$this->singleFormatter($user);
return response()->json($user);
}
/**
* Update an existing role in the system.
* Permissions should be provided as an array of permission name strings.
* An empty "permissions" array would clear granted permissions.
* In many cases, where permissions are changed, you'll want to fetch the existing
* permissions and then modify before providing in your update request.
* Requires permission to manage roles.
*/
public function update(Request $request, string $id)
{
$data = $this->validate($request, $this->rules()['update']);
$role = $this->permissionsRepo->updateRole($id, $data);
$this->singleFormatter($role);
return response()->json($role);
}
/**
* Delete a role from the system.
* Requires permission to manage roles.
*/
public function delete(string $id)
{
$this->permissionsRepo->deleteRole(intval($id));
return response('', 204);
}
/**
* Format the given role model for single-result display.
*/
protected function singleFormatter(Role $role)
{
$role->load('users:id,name,slug');
$role->unsetRelation('permissions');
$role->setAttribute('permissions', $role->permissions()->orderBy('name', 'asc')->pluck('name'));
$role->makeVisible(['users', 'permissions']);
}
}

View File

@ -13,9 +13,9 @@ use Illuminate\Validation\Rules\Unique;
class UserApiController extends ApiController
{
protected $userRepo;
protected UserRepo $userRepo;
protected $fieldsToExpose = [
protected array $fieldsToExpose = [
'email', 'created_at', 'updated_at', 'last_activity_at', 'external_auth_id',
];

View File

@ -15,16 +15,10 @@ use Illuminate\Validation\ValidationException;
class AttachmentController extends Controller
{
protected AttachmentService $attachmentService;
protected PageRepo $pageRepo;
/**
* AttachmentController constructor.
*/
public function __construct(AttachmentService $attachmentService, PageRepo $pageRepo)
{
$this->attachmentService = $attachmentService;
$this->pageRepo = $pageRepo;
public function __construct(
protected AttachmentService $attachmentService,
protected PageRepo $pageRepo
) {
}
/**
@ -112,7 +106,7 @@ class AttachmentController extends Controller
try {
$this->validate($request, [
'attachment_edit_name' => ['required', 'string', 'min:1', 'max:255'],
'attachment_edit_url' => ['string', 'min:1', 'max:255', 'safe_url'],
'attachment_edit_url' => ['string', 'min:1', 'max:2000', 'safe_url'],
]);
} catch (ValidationException $exception) {
return response()->view('attachments.manager-edit-form', array_merge($request->only(['attachment_edit_name', 'attachment_edit_url']), [
@ -148,7 +142,7 @@ class AttachmentController extends Controller
$this->validate($request, [
'attachment_link_uploaded_to' => ['required', 'integer', 'exists:pages,id'],
'attachment_link_name' => ['required', 'string', 'min:1', 'max:255'],
'attachment_link_url' => ['required', 'string', 'min:1', 'max:255', 'safe_url'],
'attachment_link_url' => ['required', 'string', 'min:1', 'max:2000', 'safe_url'],
]);
} catch (ValidationException $exception) {
return response()->view('attachments.manager-link-form', array_merge($request->only(['attachment_link_name', 'attachment_link_url']), [

View File

@ -64,7 +64,7 @@ class BookshelfController extends Controller
public function create()
{
$this->checkPermission('bookshelf-create-all');
$books = Book::visible()->orderBy('name')->get(['name', 'id', 'slug']);
$books = Book::visible()->orderBy('name')->get(['name', 'id', 'slug', 'created_at', 'updated_at']);
$this->setPageTitle(trans('entities.shelves_create'));
return view('shelves.create', ['books' => $books]);
@ -140,7 +140,7 @@ class BookshelfController extends Controller
$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::visible()->whereNotIn('id', $shelfBookIds)->orderBy('name')->get(['name', 'id', 'slug', 'created_at', 'updated_at']);
$this->setPageTitle(trans('entities.shelves_edit_named', ['name' => $shelf->getShortName()]));

View File

@ -10,6 +10,7 @@ use BookStack\Entities\Queries\TopFavourites;
use BookStack\Entities\Repos\BookRepo;
use BookStack\Entities\Repos\BookshelfRepo;
use BookStack\Entities\Tools\PageContent;
use BookStack\Uploads\FaviconHandler;
use BookStack\Util\SimpleListOptions;
use Illuminate\Http\Request;
@ -127,4 +128,15 @@ class HomeController extends Controller
{
return response()->view('errors.404', [], 404);
}
/**
* Serve the application favicon.
* Ensures a 'favicon.ico' file exists at the web root location (if writable) to be served
* directly by the webserver in the future.
*/
public function favicon(FaviconHandler $favicons)
{
$exists = $favicons->restoreOriginalIfNotExists();
return response()->file($exists ? $favicons->getPath() : $favicons->getOriginalPath());
}
}

View File

@ -74,13 +74,17 @@ class RoleController extends Controller
public function store(Request $request)
{
$this->checkPermission('user-roles-manage');
$this->validate($request, [
$data = $this->validate($request, [
'display_name' => ['required', 'min:3', 'max:180'],
'description' => ['max:180'],
'external_auth_id' => ['string'],
'permissions' => ['array'],
'mfa_enforced' => ['string'],
]);
$this->permissionsRepo->saveNewRole($request->all());
$this->showSuccessNotification(trans('settings.role_create_success'));
$data['permissions'] = array_keys($data['permissions'] ?? []);
$data['mfa_enforced'] = ($data['mfa_enforced'] ?? 'false') === 'true';
$this->permissionsRepo->saveNewRole($data);
return redirect('/settings/roles');
}
@ -100,19 +104,21 @@ class RoleController extends Controller
/**
* Updates a user role.
*
* @throws ValidationException
*/
public function update(Request $request, string $id)
{
$this->checkPermission('user-roles-manage');
$this->validate($request, [
$data = $this->validate($request, [
'display_name' => ['required', 'min:3', 'max:180'],
'description' => ['max:180'],
'external_auth_id' => ['string'],
'permissions' => ['array'],
'mfa_enforced' => ['string'],
]);
$this->permissionsRepo->updateRole($id, $request->all());
$this->showSuccessNotification(trans('settings.role_update_success'));
$data['permissions'] = array_keys($data['permissions'] ?? []);
$data['mfa_enforced'] = ($data['mfa_enforced'] ?? 'false') === 'true';
$this->permissionsRepo->updateRole($id, $data);
return redirect('/settings/roles');
}
@ -145,15 +151,13 @@ class RoleController extends Controller
$this->checkPermission('user-roles-manage');
try {
$this->permissionsRepo->deleteRole($id, $request->get('migrate_role_id'));
$this->permissionsRepo->deleteRole($id, $request->get('migrate_role_id', 0));
} catch (PermissionsException $e) {
$this->showErrorNotification($e->getMessage());
return redirect()->back();
}
$this->showSuccessNotification(trans('settings.role_delete_success'));
return redirect('/settings/roles');
}
}

View File

@ -9,10 +9,8 @@ class Request extends LaravelRequest
/**
* Override the default request methods to get the scheme and host
* to directly use the custom APP_URL, if set.
*
* @return string
*/
public function getSchemeAndHttpHost()
public function getSchemeAndHttpHost(): string
{
$appUrl = config('app.url', null);
@ -27,10 +25,8 @@ class Request extends LaravelRequest
* Override the default request methods to get the base URL
* to directly use the custom APP_URL, if set.
* The base URL never ends with a / but should start with one if not empty.
*
* @return string
*/
public function getBaseUrl()
public function getBaseUrl(): string
{
$appUrl = config('app.url', null);

View File

@ -8,16 +8,16 @@ use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Page;
use BookStack\Exceptions\WhoopsBookStackPrettyHandler;
use BookStack\Exceptions\BookStackExceptionHandlerPage;
use BookStack\Settings\SettingService;
use BookStack\Util\CspService;
use GuzzleHttp\Client;
use Illuminate\Contracts\Foundation\ExceptionRenderer;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\ServiceProvider;
use Psr\Http\Client\ClientInterface as HttpClientInterface;
use Whoops\Handler\HandlerInterface;
class AppServiceProvider extends ServiceProvider
{
@ -26,7 +26,7 @@ class AppServiceProvider extends ServiceProvider
* @var string[]
*/
public $bindings = [
HandlerInterface::class => WhoopsBookStackPrettyHandler::class,
ExceptionRenderer::class => BookStackExceptionHandlerPage::class,
];
/**

View File

@ -24,11 +24,22 @@ class EventServiceProvider extends ServiceProvider
];
/**
* Register any other events for your application.
* Register any events for your application.
*
* @return void
*/
public function boot()
{
//
}
/**
* Determine if events and listeners should be automatically discovered.
*
* @return bool
*/
public function shouldDiscoverEvents()
{
return false;
}
}

View File

@ -77,7 +77,7 @@ class RouteServiceProvider extends ServiceProvider
protected function configureRateLimiting()
{
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by(optional($request->user())->id ?: $request->ip());
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
}
}

View File

@ -21,8 +21,8 @@ class ValidationRuleServiceProvider extends ServiceProvider
Validator::extend('safe_url', function ($attribute, $value, $parameters, $validator) {
$cleanLinkName = strtolower(trim($value));
$isJs = strpos($cleanLinkName, 'javascript:') === 0;
$isData = strpos($cleanLinkName, 'data:') === 0;
$isJs = str_starts_with($cleanLinkName, 'javascript:');
$isData = str_starts_with($cleanLinkName, 'data:');
return !$isJs && !$isData;
});

View File

@ -54,10 +54,10 @@ class CrossLinkParser
{
$links = [];
$html = '<body>' . $html . '</body>';
$html = '<?xml encoding="utf-8" ?><body>' . $html . '</body>';
libxml_use_internal_errors(true);
$doc = new DOMDocument();
$doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
$doc->loadHTML($html);
$xPath = new DOMXPath($doc);
$anchors = $xPath->query('//a[@href]');

View File

@ -15,25 +15,18 @@ class SearchIndex
{
/**
* A list of delimiter characters used to break-up parsed content into terms for indexing.
*
* @var string
*/
public static $delimiters = " \n\t.,!?:;()[]{}<>`'\"";
public static string $delimiters = " \n\t.,!?:;()[]{}<>`'\"";
/**
* @var EntityProvider
*/
protected $entityProvider;
public function __construct(EntityProvider $entityProvider)
{
$this->entityProvider = $entityProvider;
public function __construct(
protected EntityProvider $entityProvider
) {
}
/**
* Index the given entity.
*/
public function indexEntity(Entity $entity)
public function indexEntity(Entity $entity): void
{
$this->deleteEntityTerms($entity);
$terms = $this->entityToTermDataArray($entity);
@ -45,7 +38,7 @@ class SearchIndex
*
* @param Entity[] $entities
*/
public function indexEntities(array $entities)
public function indexEntities(array $entities): void
{
$terms = [];
foreach ($entities as $entity) {
@ -69,7 +62,7 @@ class SearchIndex
*
* @param callable(Entity, int, int):void|null $progressCallback
*/
public function indexAllEntities(?callable $progressCallback = null)
public function indexAllEntities(?callable $progressCallback = null): void
{
SearchTerm::query()->truncate();
@ -101,7 +94,7 @@ class SearchIndex
/**
* Delete related Entity search terms.
*/
public function deleteEntityTerms(Entity $entity)
public function deleteEntityTerms(Entity $entity): void
{
$entity->searchTerms()->delete();
}
@ -145,12 +138,12 @@ class SearchIndex
'h6' => 1.5,
];
$html = '<body>' . $html . '</body>';
$html = '<?xml encoding="utf-8" ?><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'));
$doc->loadHTML($html);
$topElems = $doc->documentElement->childNodes->item(0)->childNodes;
/** @var DOMNode $child */

View File

@ -2,16 +2,16 @@
namespace BookStack\Settings;
use BookStack\Uploads\FaviconHandler;
use BookStack\Uploads\ImageRepo;
use Illuminate\Http\Request;
class AppSettingsStore
{
protected ImageRepo $imageRepo;
public function __construct(ImageRepo $imageRepo)
{
$this->imageRepo = $imageRepo;
public function __construct(
protected ImageRepo $imageRepo,
protected FaviconHandler $faviconHandler,
) {
}
public function storeFromUpdateRequest(Request $request, string $category)
@ -39,6 +39,8 @@ class AppSettingsStore
$icon = $this->imageRepo->saveNew($iconFile, 'system', 0, $size, $size);
setting()->put('app-icon-' . $size, $icon->url);
}
$this->faviconHandler->saveForUploadedImage($iconFile);
}
// Clear icon image if requested
@ -49,6 +51,8 @@ class AppSettingsStore
$this->destroyExistingSettingImage('app-icon-' . $size);
setting()->remove('app-icon-' . $size);
}
$this->faviconHandler->restoreOriginal();
}
}

View File

@ -3,45 +3,29 @@
namespace BookStack\Settings;
use BookStack\Auth\User;
use Illuminate\Contracts\Cache\Repository as Cache;
/**
* Class SettingService
* The settings are a simple key-value database store.
* For non-authenticated users, user settings are stored via the session instead.
* A local array-based cache is used to for setting accesses across a request.
*/
class SettingService
{
protected Setting $setting;
protected Cache $cache;
protected array $localCache = [];
protected string $cachePrefix = 'setting-';
public function __construct(Setting $setting, Cache $cache)
{
$this->setting = $setting;
$this->cache = $cache;
}
/**
* Gets a setting from the database,
* If not found, Returns default, Which is false by default.
*/
public function get(string $key, $default = null)
public function get(string $key, $default = null): mixed
{
if (is_null($default)) {
$default = config('setting-defaults.' . $key, false);
}
if (isset($this->localCache[$key])) {
return $this->localCache[$key];
}
$value = $this->getValueFromStore($key) ?? $default;
$formatted = $this->formatValue($value, $default);
$this->localCache[$key] = $formatted;
return $formatted;
return $this->formatValue($value, $default);
}
/**
@ -79,52 +63,78 @@ class SettingService
}
/**
* Gets a setting value from the cache or database.
* Looks at the system defaults if not cached or in database.
* Returns null if nothing is found.
* Gets a setting value from the local cache.
* Will load the local cache if not previously loaded.
*/
protected function getValueFromStore(string $key)
protected function getValueFromStore(string $key): mixed
{
// Check the cache
$cacheKey = $this->cachePrefix . $key;
$cacheVal = $this->cache->get($cacheKey, null);
if ($cacheVal !== null) {
return $cacheVal;
$cacheCategory = $this->localCacheCategory($key);
if (!isset($this->localCache[$cacheCategory])) {
$this->loadToLocalCache($cacheCategory);
}
// Check the database
$settingObject = $this->getSettingObjectByKey($key);
if ($settingObject !== null) {
$value = $settingObject->value;
if ($settingObject->type === 'array') {
$value = json_decode($value, true) ?? [];
}
$this->cache->forever($cacheKey, $value);
return $value;
}
return null;
return $this->localCache[$cacheCategory][$key] ?? null;
}
/**
* Clear an item from the cache completely.
* Put the given value into the local cached under the given key.
*/
protected function clearFromCache(string $key)
protected function putValueIntoLocalCache(string $key, mixed $value): void
{
$cacheKey = $this->cachePrefix . $key;
$this->cache->forget($cacheKey);
if (isset($this->localCache[$key])) {
unset($this->localCache[$key]);
$cacheCategory = $this->localCacheCategory($key);
if (!isset($this->localCache[$cacheCategory])) {
$this->loadToLocalCache($cacheCategory);
}
$this->localCache[$cacheCategory][$key] = $value;
}
/**
* Get the category for the given setting key.
* Will return 'app' for a general app setting otherwise 'user:<user_id>' for a user setting.
*/
protected function localCacheCategory(string $key): string
{
if (str_starts_with($key, 'user:')) {
return implode(':', array_slice(explode(':', $key), 0, 2));
}
return 'app';
}
/**
* For the given category, load the relevant settings from the database into the local cache.
*/
protected function loadToLocalCache(string $cacheCategory): void
{
$query = Setting::query();
if ($cacheCategory === 'app') {
$query->where('setting_key', 'not like', 'user:%');
} else {
$query->where('setting_key', 'like', $cacheCategory . ':%');
}
$settings = $query->toBase()->get();
if (!isset($this->localCache[$cacheCategory])) {
$this->localCache[$cacheCategory] = [];
}
foreach ($settings as $setting) {
$value = $setting->value;
if ($setting->type === 'array') {
$value = json_decode($value, true) ?? [];
}
$this->localCache[$cacheCategory][$setting->setting_key] = $value;
}
}
/**
* Format a settings value.
*/
protected function formatValue($value, $default)
protected function formatValue(mixed $value, mixed $default): mixed
{
// Change string booleans to actual booleans
if ($value === 'true') {
@ -155,21 +165,22 @@ class SettingService
* Add a setting to the database.
* Values can be an array or a string.
*/
public function put(string $key, $value): bool
public function put(string $key, mixed $value): bool
{
$setting = $this->setting->newQuery()->firstOrNew([
$setting = Setting::query()->firstOrNew([
'setting_key' => $key,
]);
$setting->type = 'string';
$setting->value = $value;
if (is_array($value)) {
$setting->type = 'array';
$value = $this->formatArrayValue($value);
$setting->value = $this->formatArrayValue($value);
}
$setting->value = $value;
$setting->save();
$this->clearFromCache($key);
$this->putValueIntoLocalCache($key, $value);
return true;
}
@ -209,7 +220,7 @@ class SettingService
* Can only take string value types since this may use
* the session which is less flexible to data types.
*/
public function putForCurrentUser(string $key, string $value)
public function putForCurrentUser(string $key, string $value): bool
{
return $this->putUser(user(), $key, $value);
}
@ -231,15 +242,19 @@ class SettingService
if ($setting) {
$setting->delete();
}
$this->clearFromCache($key);
$cacheCategory = $this->localCacheCategory($key);
if (isset($this->localCache[$cacheCategory])) {
unset($this->localCache[$cacheCategory][$key]);
}
}
/**
* Delete settings for a given user id.
*/
public function deleteUserSettings(string $userId)
public function deleteUserSettings(string $userId): void
{
return $this->setting->newQuery()
Setting::query()
->where('setting_key', 'like', $this->userKey($userId) . '%')
->delete();
}
@ -249,7 +264,16 @@ class SettingService
*/
protected function getSettingObjectByKey(string $key): ?Setting
{
return $this->setting->newQuery()
->where('setting_key', '=', $key)->first();
return Setting::query()
->where('setting_key', '=', $key)
->first();
}
/**
* Empty the local setting value cache used by this service.
*/
public function flushCache(): void
{
$this->localCache = [];
}
}

View File

@ -65,8 +65,8 @@ class ThemeEvents
* Provides the commonmark library environment for customization before it's 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
* @returns \League\CommonMark\ConfigurableEnvironmentInterface|null
* @param \League\CommonMark\Environment\Environment $environment
* @returns \League\CommonMark\Environment\Environment|null
*/
const COMMONMARK_ENVIRONMENT_CONFIGURE = 'commonmark_environment_configure';

View File

@ -10,6 +10,7 @@ use BookStack\Entities\Models\Page;
use BookStack\Model;
use BookStack\Traits\HasCreatorAndUpdater;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
@ -29,6 +30,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
class Attachment extends Model
{
use HasCreatorAndUpdater;
use HasFactory;
protected $fillable = ['name', 'order'];
protected $hidden = ['path', 'page'];
@ -38,12 +40,10 @@ class Attachment extends Model
/**
* Get the downloadable file name for this upload.
*
* @return mixed|string
*/
public function getFileName()
public function getFileName(): string
{
if (strpos($this->name, '.') !== false) {
if (str_contains($this->name, '.')) {
return $this->name;
}
@ -69,7 +69,7 @@ class Attachment extends Model
*/
public function getUrl($openInline = false): string
{
if ($this->external && strpos($this->path, 'http') !== 0) {
if ($this->external && !str_starts_with($this->path, 'http')) {
return $this->path;
}

View File

@ -9,7 +9,7 @@ use Illuminate\Contracts\Filesystem\Filesystem as Storage;
use Illuminate\Filesystem\FilesystemManager;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use League\Flysystem\Util;
use League\Flysystem\WhitespacePathNormalizer;
use Symfony\Component\HttpFoundation\File\UploadedFile;
class AttachmentService
@ -54,7 +54,7 @@ class AttachmentService
*/
protected function adjustPathForStorageDisk(string $path): string
{
$path = Util::normalizePath(str_replace('uploads/files/', '', $path));
$path = (new WhitespacePathNormalizer())->normalizePath(str_replace('uploads/files/', '', $path));
if ($this->getStorageDiskName() === 'local_secure_attachments') {
return $path;

View File

@ -0,0 +1,110 @@
<?php
namespace BookStack\Uploads;
use Illuminate\Http\UploadedFile;
use Intervention\Image\ImageManager;
class FaviconHandler
{
protected string $path;
public function __construct(
protected ImageManager $imageTool
) {
$this->path = public_path('favicon.ico');
}
/**
* Save the given UploadedFile instance as the application favicon.
*/
public function saveForUploadedImage(UploadedFile $file): void
{
if (!is_writeable($this->path)) {
return;
}
$imageData = file_get_contents($file->getRealPath());
$image = $this->imageTool->make($imageData);
$image->resize(32, 32);
$bmpData = $image->encode('png');
$icoData = $this->pngToIco($bmpData, 32, 32);
file_put_contents($this->path, $icoData);
}
/**
* Restore the original favicon image.
* Returned boolean indicates if the copy occurred.
*/
public function restoreOriginal(): bool
{
$permissionItem = file_exists($this->path) ? $this->path : dirname($this->path);
if (!is_writeable($permissionItem)) {
return false;
}
return copy($this->getOriginalPath(), $this->path);
}
/**
* Restore the original favicon image if no favicon image is already in use.
* Returns a boolean to indicate if the file exists.
*/
public function restoreOriginalIfNotExists(): bool
{
if (file_exists($this->path)) {
return true;
}
return $this->restoreOriginal();
}
/**
* Get the path to the favicon file.
*/
public function getPath(): string
{
return $this->path;
}
/**
* Get the path of the original favicon copy.
*/
public function getOriginalPath(): string
{
return public_path('icon.ico');
}
/**
* Convert PNG image data to ICO file format.
* Built following the file format info from Wikipedia:
* https://en.wikipedia.org/wiki/ICO_(file_format)
*/
protected function pngToIco(string $bmpData, int $width, int $height): string
{
// ICO header
$header = pack('v', 0x00); // Reserved. Must always be 0
$header .= pack('v', 0x01); // Specifies ico image
$header .= pack('v', 0x01); // Specifies number of images
// ICO Image Directory
$entry = hex2bin(dechex($width)); // Image width
$entry .= hex2bin(dechex($height)); // Image height
$entry .= "\0"; // Color palette, typically 0
$entry .= "\0"; // Reserved
// Color planes, Appears to remain 1 for bmp image data
$entry .= pack('v', 0x01);
// Bits per pixel, can range from 1 to 32. From testing conversion
// via intervention from png typically provides this as 24.
$entry .= pack('v', 0x00);
// Size of the image data in bytes
$entry .= pack('V', strlen($bmpData));
// Offset of the bmp data from file start
$entry .= pack('V', strlen($header) + strlen($entry) + 4);
// Join & return the combined parts of the ICO image data
return $header . $entry . $bmpData;
}
}

View File

@ -20,7 +20,7 @@ use Illuminate\Support\Str;
use Intervention\Image\Exception\NotSupportedException;
use Intervention\Image\Image as InterventionImage;
use Intervention\Image\ImageManager;
use League\Flysystem\Util;
use League\Flysystem\WhitespacePathNormalizer;
use Psr\SimpleCache\InvalidArgumentException;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\StreamedResponse;
@ -29,10 +29,9 @@ class ImageService
{
protected ImageManager $imageTool;
protected Cache $cache;
protected $storageUrl;
protected FilesystemManager $fileSystem;
protected static $supportedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
protected static array $supportedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
public function __construct(ImageManager $imageTool, FilesystemManager $fileSystem, Cache $cache)
{
@ -73,7 +72,7 @@ class ImageService
*/
protected function adjustPathForStorageDisk(string $path, string $imageType = ''): string
{
$path = Util::normalizePath(str_replace('uploads/images/', '', $path));
$path = (new WhitespacePathNormalizer())->normalizePath(str_replace('uploads/images/', '', $path));
if ($this->usingSecureImages($imageType)) {
return $path;
@ -548,7 +547,7 @@ class ImageService
// Check the image file exists
&& $disk->exists($imagePath)
// Check the file is likely an image file
&& strpos($disk->getMimetype($imagePath), 'image/') === 0;
&& strpos($disk->mimeType($imagePath), 'image/') === 0;
}
/**
@ -661,25 +660,21 @@ class ImageService
*/
private function getPublicUrl(string $filePath): string
{
if (is_null($this->storageUrl)) {
$storageUrl = config('filesystems.url');
$storageUrl = config('filesystems.url');
// Get the standard public s3 url if s3 is set as storage type
// Uses the nice, short URL if bucket name has no periods in otherwise the longer
// region-based url will be used to prevent http issues.
if ($storageUrl == false && config('filesystems.images') === 's3') {
$storageDetails = config('filesystems.disks.s3');
if (strpos($storageDetails['bucket'], '.') === false) {
$storageUrl = 'https://' . $storageDetails['bucket'] . '.s3.amazonaws.com';
} else {
$storageUrl = 'https://s3-' . $storageDetails['region'] . '.amazonaws.com/' . $storageDetails['bucket'];
}
// Get the standard public s3 url if s3 is set as storage type
// Uses the nice, short URL if bucket name has no periods in otherwise the longer
// region-based url will be used to prevent http issues.
if (!$storageUrl && config('filesystems.images') === 's3') {
$storageDetails = config('filesystems.disks.s3');
if (strpos($storageDetails['bucket'], '.') === false) {
$storageUrl = 'https://' . $storageDetails['bucket'] . '.s3.amazonaws.com';
} else {
$storageUrl = 'https://s3-' . $storageDetails['region'] . '.amazonaws.com/' . $storageDetails['bucket'];
}
$this->storageUrl = $storageUrl;
}
$basePath = ($this->storageUrl == false) ? url('/') : $this->storageUrl;
$basePath = $storageUrl ?: url('/');
return rtrim($basePath, '/') . $filePath;
}

View File

@ -19,10 +19,10 @@ class HtmlContentFilter
return $html;
}
$html = '<body>' . $html . '</body>';
$html = '<?xml encoding="utf-8" ?><body>' . $html . '</body>';
libxml_use_internal_errors(true);
$doc = new DOMDocument();
$doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
$doc->loadHTML($html);
$xPath = new DOMXPath($doc);
// Remove standard script tags

View File

@ -130,7 +130,7 @@ class LanguageManager
]);
if (!empty($locales)) {
setlocale(LC_TIME, ...$locales);
setlocale(LC_TIME, $locales[0], ...array_slice($locales, 1));
}
}
}

View File

@ -8,7 +8,7 @@
"license": "MIT",
"type": "project",
"require": {
"php": "^7.4|^8.0",
"php": "^8.0.2",
"ext-curl": "*",
"ext-dom": "*",
"ext-fileinfo": "*",
@ -19,36 +19,34 @@
"bacon/bacon-qr-code": "^2.0",
"barryvdh/laravel-dompdf": "^2.0",
"barryvdh/laravel-snappy": "^1.0",
"doctrine/dbal": "^3.1",
"filp/whoops": "^2.14",
"doctrine/dbal": "^3.5",
"guzzlehttp/guzzle": "^7.4",
"intervention/image": "^2.7",
"laravel/framework": "^8.68",
"laravel/framework": "^9.0",
"laravel/socialite": "^5.2",
"laravel/tinker": "^2.6",
"league/commonmark": "^1.6",
"league/flysystem-aws-s3-v3": "^1.0.29",
"league/commonmark": "^2.3",
"league/flysystem-aws-s3-v3": "^3.0",
"league/html-to-markdown": "^5.0.0",
"league/oauth2-client": "^2.6",
"onelogin/php-saml": "^4.0",
"phpseclib/phpseclib": "~3.0",
"phpseclib/phpseclib": "^3.0",
"pragmarx/google2fa": "^8.0",
"predis/predis": "^1.1",
"predis/predis": "^2.1",
"socialiteproviders/discord": "^4.1",
"socialiteproviders/gitlab": "^4.1",
"socialiteproviders/microsoft-azure": "^5.0.1",
"socialiteproviders/okta": "^4.1",
"socialiteproviders/microsoft-azure": "^5.1",
"socialiteproviders/okta": "^4.2",
"socialiteproviders/slack": "^4.1",
"socialiteproviders/twitch": "^5.3",
"ssddanbrown/htmldiff": "^1.0.2"
},
"require-dev": {
"brianium/paratest": "^6.6",
"fakerphp/faker": "^1.16",
"fakerphp/faker": "^1.21",
"itsgoingd/clockwork": "^5.1",
"mockery/mockery": "^1.4",
"nunomaduro/collision": "^5.10",
"nunomaduro/larastan": "^1.0",
"mockery/mockery": "^1.5",
"nunomaduro/collision": "^6.4",
"nunomaduro/larastan": "^2.4",
"phpunit/phpunit": "^9.5",
"squizlabs/php_codesniffer": "^3.7",
"ssddanbrown/asserthtml": "^1.0"
@ -73,7 +71,6 @@
"format": "phpcbf",
"lint": "phpcs",
"test": "phpunit",
"t": "@php artisan test --parallel",
"t-reset": "@php artisan test --recreate-databases",
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
@ -102,7 +99,7 @@
"preferred-install": "dist",
"sort-packages": true,
"platform": {
"php": "7.4.0"
"php": "8.0.2"
}
},
"extra": {
@ -110,6 +107,6 @@
"dont-discover": []
}
},
"minimum-stability": "dev",
"minimum-stability": "stable",
"prefer-stable": true
}

3846
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -5,6 +5,6 @@ pull_request_title: Updated translations with latest Crowdin changes
pull_request_labels:
- ":earth_africa: Translations"
files:
- source: /resources/lang/en/*.php
translation: /resources/lang/%two_letters_code%/%original_file_name%
- source: /lang/en/*.php
translation: /lang/%two_letters_code%/%original_file_name%
type: php

View File

@ -0,0 +1,39 @@
<?php
namespace Database\Factories\Uploads;
use BookStack\Auth\User;
use BookStack\Entities\Models\Page;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\BookStack\Uploads\Attachment>
*/
class AttachmentFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* @var string
*/
protected $model = \BookStack\Uploads\Attachment::class;
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition()
{
return [
'name' => $this->faker->words(2, true),
'path' => $this->faker->url(),
'extension' => '',
'external' => true,
'uploaded_to' => Page::factory(),
'created_by' => User::factory(),
'updated_by' => User::factory(),
'order' => 0,
];
}
}

View File

@ -3,7 +3,7 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class CreateUsersTable extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@ -40,4 +40,4 @@ class CreateUsersTable extends Migration
{
Schema::drop('users');
}
}
};

View File

@ -3,7 +3,7 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class CreatePasswordResetsTable extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@ -28,4 +28,4 @@ class CreatePasswordResetsTable extends Migration
{
Schema::drop('password_resets');
}
}
};

View File

@ -3,7 +3,7 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class CreateBooksTable extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@ -30,4 +30,4 @@ class CreateBooksTable extends Migration
{
Schema::drop('books');
}
}
};

View File

@ -3,7 +3,7 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class CreatePagesTable extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@ -34,4 +34,4 @@ class CreatePagesTable extends Migration
{
Schema::drop('pages');
}
}
};

View File

@ -3,7 +3,7 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class CreateImagesTable extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@ -29,4 +29,4 @@ class CreateImagesTable extends Migration
{
Schema::drop('images');
}
}
};

View File

@ -3,7 +3,7 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class CreateChaptersTable extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@ -32,4 +32,4 @@ class CreateChaptersTable extends Migration
{
Schema::drop('chapters');
}
}
};

View File

@ -3,7 +3,7 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class AddUsersToEntities extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@ -54,4 +54,4 @@ class AddUsersToEntities extends Migration
$table->dropColumn('updated_by');
});
}
}
};

View File

@ -3,7 +3,7 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class CreatePageRevisionsTable extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@ -32,4 +32,4 @@ class CreatePageRevisionsTable extends Migration
{
Schema::drop('page_revisions');
}
}
};

View File

@ -3,7 +3,7 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class CreateActivitiesTable extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@ -33,4 +33,4 @@ class CreateActivitiesTable extends Migration
{
Schema::drop('activities');
}
}
};

View File

@ -1,8 +1,5 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
/**
* Much of this code has been taken from entrust,
* a role & permission management solution for Laravel.
@ -12,7 +9,11 @@ use Illuminate\Database\Schema\Blueprint;
* @license MIT
* @url https://github.com/Zizaco/entrust
*/
class AddRolesAndPermissions extends Migration
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
return new class extends Migration
{
/**
* Run the migrations.
@ -147,4 +148,4 @@ class AddRolesAndPermissions extends Migration
Schema::drop('role_user');
Schema::drop('roles');
}
}
};

View File

@ -3,7 +3,7 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class CreateSettingsTable extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@ -28,4 +28,4 @@ class CreateSettingsTable extends Migration
{
Schema::drop('settings');
}
}
};

View File

@ -3,7 +3,7 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class AddSearchIndexes extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@ -51,4 +51,4 @@ class AddSearchIndexes extends Migration
});
}
}
}
};

View File

@ -3,7 +3,7 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class CreateSocialAccountsTable extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@ -31,4 +31,4 @@ class CreateSocialAccountsTable extends Migration
{
Schema::drop('social_accounts');
}
}
};

View File

@ -3,7 +3,7 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class AddEmailConfirmationTable extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@ -36,4 +36,4 @@ class AddEmailConfirmationTable extends Migration
});
Schema::drop('email_confirmations');
}
}
};

View File

@ -3,7 +3,7 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class CreateViewsTable extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@ -31,4 +31,4 @@ class CreateViewsTable extends Migration
{
Schema::drop('views');
}
}
};

View File

@ -3,7 +3,7 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class AddEntityIndexes extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@ -86,4 +86,4 @@ class AddEntityIndexes extends Migration
$table->dropIndex('views_viewable_id_index');
});
}
}
};

View File

@ -3,7 +3,7 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class FulltextWeighting extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@ -51,4 +51,4 @@ class FulltextWeighting extends Migration
});
}
}
}
};

View File

@ -4,7 +4,7 @@ use BookStack\Uploads\Image;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class AddImageUploadTypes extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@ -37,4 +37,4 @@ class AddImageUploadTypes extends Migration
$table->dropColumn('path');
});
}
}
};

View File

@ -3,7 +3,7 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class AddUserAvatars extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@ -28,4 +28,4 @@ class AddUserAvatars extends Migration
$table->dropColumn('image_id');
});
}
}
};

View File

@ -3,7 +3,7 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class AddExternalAuthToUsers extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@ -28,4 +28,4 @@ class AddExternalAuthToUsers extends Migration
$table->dropColumn('external_auth_id');
});
}
}
};

View File

@ -3,7 +3,7 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class AddSlugToRevisions extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@ -32,4 +32,4 @@ class AddSlugToRevisions extends Migration
$table->dropColumn('book_slug');
});
}
}
};

View File

@ -2,7 +2,7 @@
use Illuminate\Database\Migrations\Migration;
class UpdatePermissionsAndRoles extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@ -113,4 +113,4 @@ class UpdatePermissionsAndRoles extends Migration
}
}
}
}
};

View File

@ -3,7 +3,7 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class AddEntityAccessControls extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@ -69,4 +69,4 @@ class AddEntityAccessControls extends Migration
Schema::drop('restrictions');
}
}
};

View File

@ -3,7 +3,7 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class AddPageRevisionTypes extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@ -29,4 +29,4 @@ class AddPageRevisionTypes extends Migration
$table->dropColumn('type');
});
}
}
};

View File

@ -3,7 +3,7 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class AddPageDrafts extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@ -29,4 +29,4 @@ class AddPageDrafts extends Migration
$table->dropColumn('draft');
});
}
}
};

View File

@ -3,7 +3,7 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class AddMarkdownSupport extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@ -36,4 +36,4 @@ class AddMarkdownSupport extends Migration
$table->dropColumn('markdown');
});
}
}
};

View File

@ -2,7 +2,7 @@
use Illuminate\Database\Migrations\Migration;
class AddViewPermissionsToRoles extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@ -54,4 +54,4 @@ class AddViewPermissionsToRoles extends Migration
}
}
}
}
};

View File

@ -4,7 +4,7 @@ use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Str;
class CreateJointPermissionsTable extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@ -97,4 +97,4 @@ class CreateJointPermissionsTable extends Migration
$table->dropColumn('hidden');
});
}
}
};

View File

@ -3,7 +3,7 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class CreateTagsTable extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@ -37,4 +37,4 @@ class CreateTagsTable extends Migration
{
Schema::drop('tags');
}
}
};

View File

@ -2,7 +2,7 @@
use Illuminate\Database\Migrations\Migration;
class AddSummaryToPageRevisions extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@ -27,4 +27,4 @@ class AddSummaryToPageRevisions extends Migration
$table->dropColumn('summary');
});
}
}
};

View File

@ -4,7 +4,7 @@ use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class RemoveHiddenRoles extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@ -63,4 +63,4 @@ class RemoveHiddenRoles extends Migration
DB::table('roles')->where('system_name', '=', 'public')->update(['hidden' => true]);
}
}
};

View File

@ -4,7 +4,7 @@ use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateAttachmentsTable extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@ -67,4 +67,4 @@ class CreateAttachmentsTable extends Migration
DB::table('role_permissions')->where('name', '=', $permName)->delete();
}
}
}
};

View File

@ -4,7 +4,7 @@ use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateCacheTable extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@ -29,4 +29,4 @@ class CreateCacheTable extends Migration
{
Schema::dropIfExists('cache');
}
}
};

View File

@ -4,7 +4,7 @@ use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateSessionsTable extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@ -32,4 +32,4 @@ class CreateSessionsTable extends Migration
{
Schema::dropIfExists('sessions');
}
}
};

View File

@ -4,7 +4,7 @@ use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateSearchIndexTable extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@ -73,4 +73,4 @@ class CreateSearchIndexTable extends Migration
Schema::dropIfExists('search_terms');
}
}
};

View File

@ -4,7 +4,7 @@ use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddRevisionCounts extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@ -24,7 +24,7 @@ class AddRevisionCounts extends Migration
// Update revision count
$pTable = DB::getTablePrefix() . 'pages';
$rTable = DB::getTablePrefix() . 'page_revisions';
DB::statement("UPDATE ${pTable} SET ${pTable}.revision_count=(SELECT count(*) FROM ${rTable} WHERE ${rTable}.page_id=${pTable}.id)");
DB::statement("UPDATE {$pTable} SET {$pTable}.revision_count=(SELECT count(*) FROM {$rTable} WHERE {$rTable}.page_id={$pTable}.id)");
}
/**
@ -41,4 +41,4 @@ class AddRevisionCounts extends Migration
$table->dropColumn('revision_number');
});
}
}
};

View File

@ -2,7 +2,7 @@
use Illuminate\Database\Migrations\Migration;
class UpdateDbEncodingToUt8mb4 extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@ -25,4 +25,4 @@ class UpdateDbEncodingToUt8mb4 extends Migration
{
//
}
}
};

View File

@ -4,7 +4,7 @@ use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateCommentsTable extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@ -64,4 +64,4 @@ class CreateCommentsTable extends Migration
DB::table('role_permissions')->where('name', '=', $permName)->delete();
}
}
}
};

View File

@ -4,7 +4,7 @@ use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddCoverImageDisplay extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@ -29,4 +29,4 @@ class AddCoverImageDisplay extends Migration
$table->dropColumn('image_id');
});
}
}
};

View File

@ -4,7 +4,7 @@ use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddRoleExternalAuthId extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@ -30,4 +30,4 @@ class AddRoleExternalAuthId extends Migration
$table->dropColumn('external_auth_id');
});
}
}
};

View File

@ -5,7 +5,7 @@ use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class CreateBookshelvesTable extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@ -128,4 +128,4 @@ class CreateBookshelvesTable extends Migration
DB::table('search_terms')->where('entity_type', '=', 'BookStack\Entities\Models\Bookshelf')->delete();
DB::table('comments')->where('entity_type', '=', 'BookStack\Entities\Models\Bookshelf')->delete();
}
}
};

View File

@ -5,7 +5,7 @@ use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddTemplateSupport extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@ -51,4 +51,4 @@ class AddTemplateSupport extends Migration
DB::table('permission_role')->where('permission_id', '=', $templatesManagePermission->id)->delete();
DB::table('role_permissions')->where('name', '=', 'templates-manage')->delete();
}
}
};

View File

@ -4,7 +4,7 @@ use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddUserInvitesTable extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@ -30,4 +30,4 @@ class AddUserInvitesTable extends Migration
{
Schema::dropIfExists('user_invites');
}
}
};

View File

@ -5,7 +5,7 @@ use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Schema;
class AddApiAuth extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@ -57,4 +57,4 @@ class AddApiAuth extends Migration
DB::table('permission_role')->where('permission_id', '=', $apiAccessPermission->id)->delete();
DB::table('role_permissions')->where('name', '=', 'access-api')->delete();
}
}
};

View File

@ -4,7 +4,7 @@ use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class DropJointPermissionsId extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@ -34,4 +34,4 @@ class DropJointPermissionsId extends Migration
$table->increments('id')->unsigned();
});
}
}
};

View File

@ -5,7 +5,7 @@ use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class RemoveRoleNameField extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@ -34,4 +34,4 @@ class RemoveRoleNameField extends Migration
'name' => DB::raw("lower(replace(`display_name`, ' ', '-'))"),
]);
}
}
};

View File

@ -4,7 +4,7 @@ use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddActivityIndexes extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@ -31,4 +31,4 @@ class AddActivityIndexes extends Migration
$table->dropIndex('activities_created_at_index');
});
}
}
};

View File

@ -4,7 +4,7 @@ use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddEntitySoftDeletes extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@ -47,4 +47,4 @@ class AddEntitySoftDeletes extends Migration
$table->dropSoftDeletes();
});
}
}
};

Some files were not shown because too many files have changed in this diff Show More