mirror of
https://github.com/BookStackApp/BookStack.git
synced 2026-02-12 03:09:38 +03:00
Compare commits
156 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d485fcb3db | ||
|
|
0f895668a4 | ||
|
|
57bdd83d8c | ||
|
|
ce0b75294f | ||
|
|
4bb2b31bc9 | ||
|
|
9d74508ae3 | ||
|
|
c41baa1b76 | ||
|
|
cd32597d4d | ||
|
|
8594656f6e | ||
|
|
0aca1c2332 | ||
|
|
8c738aedee | ||
|
|
f64ce71afc | ||
|
|
277d5392fb | ||
|
|
23c35af9ef | ||
|
|
78fecdfcb0 | ||
|
|
a9d952560d | ||
|
|
56f234d1ee | ||
|
|
011800d425 | ||
|
|
647ce6c237 | ||
|
|
607da73109 | ||
|
|
1135d477ba | ||
|
|
a4a96a3df7 | ||
|
|
38e8a96dcd | ||
|
|
9a17656f88 | ||
|
|
e36cdaad0d | ||
|
|
722c38d576 | ||
|
|
8cd6c797e8 | ||
|
|
dff45e2c5d | ||
|
|
61d2ea6ac7 | ||
|
|
752562d23d | ||
|
|
b21a9007c5 | ||
|
|
a8fc29a31e | ||
|
|
36116a45d4 | ||
|
|
23915c3b1a | ||
|
|
55af22b487 | ||
|
|
01f3f4d315 | ||
|
|
58cadce052 | ||
|
|
1de72d09ca | ||
|
|
fa6fcc1c1c | ||
|
|
a46b438a4c | ||
|
|
7505443a0c | ||
|
|
f837083c12 | ||
|
|
e1bd13f481 | ||
|
|
c74f7cc628 | ||
|
|
9f467f4052 | ||
|
|
974390688d | ||
|
|
da3ae3ba8b | ||
|
|
0519e58fbf | ||
|
|
e711290d8b | ||
|
|
752ee664c2 | ||
|
|
69d03042c6 | ||
|
|
baf5edd73a | ||
|
|
3e738b1471 | ||
|
|
94f464cd14 | ||
|
|
900571ac9c | ||
|
|
09fd0bc5b7 | ||
|
|
74b4751a1c | ||
|
|
74b76ecdb9 | ||
|
|
9874a53206 | ||
|
|
257a703878 | ||
|
|
fdda813d5f | ||
|
|
6f45d34bf8 | ||
|
|
32c765d0c3 | ||
|
|
9813c94720 | ||
|
|
da3e4f5f75 | ||
|
|
572037ef1f | ||
|
|
50f3c10f19 | ||
|
|
6c577ac3bf | ||
|
|
31cc2423d2 | ||
|
|
3f3f221e0d | ||
|
|
d0f970fe4f | ||
|
|
95b75c067f | ||
|
|
81134e7071 | ||
|
|
e722ee4268 | ||
|
|
fd674d10e3 | ||
|
|
4835a0dcb1 | ||
|
|
d353e87ca1 | ||
|
|
8e64324d62 | ||
|
|
c9ed32e518 | ||
|
|
6b4c3a0969 | ||
|
|
0a0fdd7f3e | ||
|
|
3410cf21cb | ||
|
|
6e284d7a6c | ||
|
|
ea7914422c | ||
|
|
509cab3e28 | ||
|
|
dde38e91b5 | ||
|
|
970088a8a1 | ||
|
|
0e43618dda | ||
|
|
f2293a70f8 | ||
|
|
dce5123452 | ||
|
|
c81cb6f2af | ||
|
|
9b66e93b15 | ||
|
|
402eb845ab | ||
|
|
3a808fd768 | ||
|
|
d9eec6d82c | ||
|
|
6357056d7b | ||
|
|
a369971e04 | ||
|
|
1903924829 | ||
|
|
0de7530059 | ||
|
|
c42956bcaf | ||
|
|
7b5111571c | ||
|
|
2dad92d1bd | ||
|
|
c1fb7ab7dc | ||
|
|
3464f5e961 | ||
|
|
7c27d26161 | ||
|
|
98315f3899 | ||
|
|
8c82aaabd6 | ||
|
|
c7e33d1981 | ||
|
|
ba21b54195 | ||
|
|
f35c42b0b8 | ||
|
|
b88b1bef2c | ||
|
|
8abb41abbd | ||
|
|
a031edec16 | ||
|
|
2724b2867b | ||
|
|
8bebea4cca | ||
|
|
6545afacd6 | ||
|
|
31495758a9 | ||
|
|
c80396136f | ||
|
|
8da3e64039 | ||
|
|
c1167f8821 | ||
|
|
4176b598ce | ||
|
|
950c02e996 | ||
|
|
9502f349a2 | ||
|
|
3c3c2ae9b5 | ||
|
|
723f108bd9 | ||
|
|
55456a57d6 | ||
|
|
c148e2f3d9 | ||
|
|
f51036b203 | ||
|
|
9135a85de4 | ||
|
|
fd45d280b4 | ||
|
|
524adce654 | ||
|
|
f799c9b260 | ||
|
|
9c26ccf43d | ||
|
|
71a09bcf6e | ||
|
|
af31a6fc1b | ||
|
|
08b39500b3 | ||
|
|
f9fcc9f3c7 | ||
|
|
0812184995 | ||
|
|
646f8f60c0 | ||
|
|
f333db8e4f | ||
|
|
da42fc7457 | ||
|
|
48f1934387 | ||
|
|
2845e0003e | ||
|
|
1a189640f1 | ||
|
|
420f89af99 | ||
|
|
da1a66abd3 | ||
|
|
5d18e7df79 | ||
|
|
ba25a3e1b7 | ||
|
|
bc18dc7da6 | ||
|
|
5e8ec56196 | ||
|
|
9ca088a4e2 | ||
|
|
008e7a4d25 | ||
|
|
9fd7a6abed | ||
|
|
4757ed9453 | ||
|
|
97146a6359 | ||
|
|
d4f2fcdf79 |
@@ -3,6 +3,10 @@
|
||||
# Each option is shown with it's default value.
|
||||
# Do not copy this whole file to use as your '.env' file.
|
||||
|
||||
# The details here only serve as a quick reference.
|
||||
# Please refer to the BookStack documentation for full details:
|
||||
# https://www.bookstackapp.com/docs/
|
||||
|
||||
# Application environment
|
||||
# Can be 'production', 'development', 'testing' or 'demo'
|
||||
APP_ENV=production
|
||||
@@ -79,6 +83,10 @@ MAIL_PORT=1025
|
||||
MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_ENCRYPTION=null
|
||||
MAIL_VERIFY_SSL=true
|
||||
|
||||
# Command to use when email is sent via sendmail
|
||||
MAIL_SENDMAIL_COMMAND="/usr/sbin/sendmail -bs"
|
||||
|
||||
# Cache & Session driver to use
|
||||
# Can be 'file', 'database', 'memcached' or 'redis'
|
||||
@@ -319,6 +327,13 @@ FILE_UPLOAD_SIZE_LIMIT=50
|
||||
# Can be 'a4' or 'letter'.
|
||||
EXPORT_PAGE_SIZE=a4
|
||||
|
||||
# Set path to wkhtmltopdf binary for PDF generation.
|
||||
# Can be 'false' or a path path like: '/home/bins/wkhtmltopdf'
|
||||
# When false, BookStack will attempt to find a wkhtmltopdf in the application
|
||||
# root folder then fall back to the default dompdf renderer if no binary exists.
|
||||
# Only used if 'ALLOW_UNTRUSTED_SERVER_FETCHING=true' which disables security protections.
|
||||
WKHTMLTOPDF=false
|
||||
|
||||
# Allow <script> tags in page content
|
||||
# Note, if set to 'true' the page editor may still escape scripts.
|
||||
ALLOW_CONTENT_SCRIPTS=false
|
||||
@@ -369,4 +384,4 @@ LOG_FAILED_LOGIN_CHANNEL=errorlog_plain_webserver
|
||||
# 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
|
||||
IP_ADDRESS_PRECISION=4
|
||||
|
||||
19
.github/translators.txt
vendored
19
.github/translators.txt
vendored
@@ -308,3 +308,22 @@ Adrian Ocneanu (aocneanu) :: Romanian
|
||||
Eduardo Castanho (EduardoCastanho) :: Portuguese
|
||||
VIET NAM VPS (vietnamvps) :: Vietnamese
|
||||
m4tthi4s :: French
|
||||
toras9000 :: Japanese
|
||||
pathab :: German
|
||||
MichelSchoon85 :: Dutch
|
||||
Jøran Haugli (haugli92) :: Norwegian Bokmal
|
||||
Vasileios Kouvelis (VasilisKouvelis) :: Greek
|
||||
Dremski :: Bulgarian
|
||||
Frédéric SENE (nothingfr) :: French
|
||||
bendem :: French
|
||||
kostasdizas :: Greek
|
||||
Ricardo Schroeder (brownstone666) :: Portuguese, Brazilian
|
||||
Eitan MG (EitanMG) :: Hebrew
|
||||
Robin Flikkema (RobinFlikkema) :: Dutch
|
||||
Michal Gurcik (mgurcik) :: Slovak
|
||||
Pooyan Arab (pooyanarab) :: Persian
|
||||
Ochi Darma Putra (troke12) :: Indonesian
|
||||
H.-H. Peng (Hsins) :: Chinese Traditional
|
||||
Mosi Wang (mosiwang) :: Chinese Traditional
|
||||
骆言 (LawssssCat) :: Chinese Simplified
|
||||
Stickers Gaming Shøw (StickerSGSHOW) :: French
|
||||
|
||||
16
.github/workflows/lint-js.yml
vendored
Normal file
16
.github/workflows/lint-js.yml
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
name: lint-js
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: ${{ github.ref != 'refs/heads/l10n_development' }}
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
- name: Install NPM deps
|
||||
run: npm ci
|
||||
|
||||
- name: Run formatting check
|
||||
run: npm run lint
|
||||
2
.github/workflows/test-migrations.yml
vendored
2
.github/workflows/test-migrations.yml
vendored
@@ -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
|
||||
|
||||
|
||||
2
.github/workflows/test-php.yml
vendored
2
.github/workflows/test-php.yml
vendored
@@ -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
|
||||
|
||||
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1,5 +1,7 @@
|
||||
/vendor
|
||||
/node_modules
|
||||
/.vscode
|
||||
/composer
|
||||
Homestead.yaml
|
||||
.env
|
||||
.idea
|
||||
@@ -11,6 +13,7 @@ yarn-error.log
|
||||
/public/js/*.map
|
||||
/public/bower
|
||||
/public/build/
|
||||
/public/favicon.ico
|
||||
/storage/images
|
||||
_ide_helper.php
|
||||
/storage/debugbar
|
||||
@@ -20,8 +23,10 @@ yarn.lock
|
||||
nbproject
|
||||
.buildpath
|
||||
.project
|
||||
.nvmrc
|
||||
.settings/
|
||||
webpack-stats.json
|
||||
.phpunit.result.cache
|
||||
.DS_Store
|
||||
phpstan.neon
|
||||
phpstan.neon
|
||||
esbuild-meta.json
|
||||
2
LICENSE
2
LICENSE
@@ -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
|
||||
|
||||
@@ -11,11 +11,9 @@ use Illuminate\Support\Facades\DB;
|
||||
|
||||
class TagRepo
|
||||
{
|
||||
protected PermissionApplicator $permissions;
|
||||
|
||||
public function __construct(PermissionApplicator $permissions)
|
||||
{
|
||||
$this->permissions = $permissions;
|
||||
public function __construct(
|
||||
protected PermissionApplicator $permissions
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -90,6 +88,7 @@ class TagRepo
|
||||
{
|
||||
$query = Tag::query()
|
||||
->select('*', DB::raw('count(*) as count'))
|
||||
->where('value', '!=', '')
|
||||
->groupBy('value');
|
||||
|
||||
if ($searchTerm) {
|
||||
|
||||
@@ -8,8 +8,8 @@ use BookStack\Notifications\ConfirmEmail;
|
||||
|
||||
class EmailConfirmationService extends UserTokenService
|
||||
{
|
||||
protected $tokenTable = 'email_confirmations';
|
||||
protected $expiryTime = 24;
|
||||
protected string $tokenTable = 'email_confirmations';
|
||||
protected int $expiryTime = 24;
|
||||
|
||||
/**
|
||||
* Create new confirmation for a user,
|
||||
|
||||
@@ -4,35 +4,16 @@ namespace BookStack\Auth\Access\Oidc;
|
||||
|
||||
class OidcIdToken
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $header;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $payload;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $signature;
|
||||
protected array $header;
|
||||
protected array $payload;
|
||||
protected string $signature;
|
||||
protected string $issuer;
|
||||
protected array $tokenParts = [];
|
||||
|
||||
/**
|
||||
* @var array[]|string[]
|
||||
*/
|
||||
protected $keys;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $issuer;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $tokenParts = [];
|
||||
protected array $keys;
|
||||
|
||||
public function __construct(string $token, string $issuer, array $keys)
|
||||
{
|
||||
@@ -106,6 +87,14 @@ class OidcIdToken
|
||||
return $this->payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the existing claim data of this token with that provided.
|
||||
*/
|
||||
public function replaceClaims(array $claims): void
|
||||
{
|
||||
$this->payload = $claims;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the structure of the given token and ensure we have the required pieces.
|
||||
* As per https://datatracker.ietf.org/doc/html/rfc7519#section-7.2.
|
||||
|
||||
@@ -9,6 +9,8 @@ use BookStack\Auth\User;
|
||||
use BookStack\Exceptions\JsonDebugException;
|
||||
use BookStack\Exceptions\StoppedAuthenticationException;
|
||||
use BookStack\Exceptions\UserRegistrationException;
|
||||
use BookStack\Facades\Theme;
|
||||
use BookStack\Theming\ThemeEvents;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use League\OAuth2\Client\OptionProvider\HttpBasicAuthOptionProvider;
|
||||
@@ -21,24 +23,12 @@ use Psr\Http\Client\ClientInterface as HttpClient;
|
||||
*/
|
||||
class OidcService
|
||||
{
|
||||
protected RegistrationService $registrationService;
|
||||
protected LoginService $loginService;
|
||||
protected HttpClient $httpClient;
|
||||
protected GroupSyncService $groupService;
|
||||
|
||||
/**
|
||||
* OpenIdService constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
RegistrationService $registrationService,
|
||||
LoginService $loginService,
|
||||
HttpClient $httpClient,
|
||||
GroupSyncService $groupService
|
||||
protected RegistrationService $registrationService,
|
||||
protected LoginService $loginService,
|
||||
protected HttpClient $httpClient,
|
||||
protected GroupSyncService $groupService
|
||||
) {
|
||||
$this->registrationService = $registrationService;
|
||||
$this->loginService = $loginService;
|
||||
$this->httpClient = $httpClient;
|
||||
$this->groupService = $groupService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -226,6 +216,16 @@ class OidcService
|
||||
$settings->keys,
|
||||
);
|
||||
|
||||
$returnClaims = Theme::dispatch(ThemeEvents::OIDC_ID_TOKEN_PRE_VALIDATE, $idToken->getAllClaims(), [
|
||||
'access_token' => $accessToken->getToken(),
|
||||
'expires_in' => $accessToken->getExpires(),
|
||||
'refresh_token' => $accessToken->getRefreshToken(),
|
||||
]);
|
||||
|
||||
if (!is_null($returnClaims)) {
|
||||
$idToken->replaceClaims($returnClaims);
|
||||
}
|
||||
|
||||
if ($this->config()['dump_user_details']) {
|
||||
throw new JsonDebugException($idToken->getAllClaims());
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ class Saml2Service
|
||||
$returnRoute,
|
||||
[],
|
||||
$user->email,
|
||||
null,
|
||||
session()->get('saml2_session_index'),
|
||||
true,
|
||||
Constants::NAMEID_EMAIL_ADDRESS
|
||||
);
|
||||
@@ -118,6 +118,7 @@ class Saml2Service
|
||||
|
||||
$attrs = $toolkit->getAttributes();
|
||||
$id = $toolkit->getNameId();
|
||||
session()->put('saml2_session_index', $toolkit->getSessionIndex());
|
||||
|
||||
return $this->processLoginCallback($id, $attrs);
|
||||
}
|
||||
|
||||
@@ -7,14 +7,12 @@ use BookStack\Notifications\UserInvite;
|
||||
|
||||
class UserInviteService extends UserTokenService
|
||||
{
|
||||
protected $tokenTable = 'user_invites';
|
||||
protected $expiryTime = 336; // Two weeks
|
||||
protected string $tokenTable = 'user_invites';
|
||||
protected int $expiryTime = 336; // Two weeks
|
||||
|
||||
/**
|
||||
* Send an invitation to a user to sign into BookStack
|
||||
* Removes existing invitation tokens.
|
||||
*
|
||||
* @param User $user
|
||||
*/
|
||||
public function sendInvitation(User $user)
|
||||
{
|
||||
|
||||
@@ -14,41 +14,29 @@ class UserTokenService
|
||||
{
|
||||
/**
|
||||
* Name of table where user tokens are stored.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $tokenTable = 'user_tokens';
|
||||
protected string $tokenTable = 'user_tokens';
|
||||
|
||||
/**
|
||||
* Token expiry time in hours.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $expiryTime = 24;
|
||||
protected int $expiryTime = 24;
|
||||
|
||||
/**
|
||||
* Delete all email confirmations that belong to a user.
|
||||
*
|
||||
* @param User $user
|
||||
*
|
||||
* @return mixed
|
||||
* Delete all tokens that belong to a user.
|
||||
*/
|
||||
public function deleteByUser(User $user)
|
||||
public function deleteByUser(User $user): void
|
||||
{
|
||||
return DB::table($this->tokenTable)
|
||||
DB::table($this->tokenTable)
|
||||
->where('user_id', '=', $user->id)
|
||||
->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user id from a token, while check the token exists and has not expired.
|
||||
*
|
||||
* @param string $token
|
||||
* Get the user id from a token, while checking the token exists and has not expired.
|
||||
*
|
||||
* @throws UserTokenNotFoundException
|
||||
* @throws UserTokenExpiredException
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function checkTokenAndGetUserId(string $token): int
|
||||
{
|
||||
@@ -67,8 +55,6 @@ class UserTokenService
|
||||
|
||||
/**
|
||||
* Creates a unique token within the email confirmation database.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function generateToken(): string
|
||||
{
|
||||
@@ -82,10 +68,6 @@ class UserTokenService
|
||||
|
||||
/**
|
||||
* Generate and store a token for the given user.
|
||||
*
|
||||
* @param User $user
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function createTokenForUser(User $user): string
|
||||
{
|
||||
@@ -102,10 +84,6 @@ class UserTokenService
|
||||
|
||||
/**
|
||||
* Check if the given token exists.
|
||||
*
|
||||
* @param string $token
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function tokenExists(string $token): bool
|
||||
{
|
||||
@@ -115,12 +93,8 @@ class UserTokenService
|
||||
|
||||
/**
|
||||
* Get a token entry for the given token.
|
||||
*
|
||||
* @param string $token
|
||||
*
|
||||
* @return object|null
|
||||
*/
|
||||
protected function getEntryByToken(string $token)
|
||||
protected function getEntryByToken(string $token): ?stdClass
|
||||
{
|
||||
return DB::table($this->tokenTable)
|
||||
->where('token', '=', $token)
|
||||
@@ -129,10 +103,6 @@ class UserTokenService
|
||||
|
||||
/**
|
||||
* Check if the given token entry has expired.
|
||||
*
|
||||
* @param stdClass $tokenEntry
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function entryExpired(stdClass $tokenEntry): bool
|
||||
{
|
||||
|
||||
@@ -5,7 +5,6 @@ namespace BookStack\Auth\Permissions;
|
||||
use BookStack\Auth\Role;
|
||||
use BookStack\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
@@ -23,14 +22,14 @@ class EntityPermission extends Model
|
||||
|
||||
protected $fillable = ['role_id', 'view', 'create', 'update', 'delete'];
|
||||
public $timestamps = false;
|
||||
|
||||
/**
|
||||
* Get this restriction's attached entity.
|
||||
*/
|
||||
public function restrictable(): MorphTo
|
||||
{
|
||||
return $this->morphTo('restrictable');
|
||||
}
|
||||
protected $hidden = ['entity_id', 'entity_type', 'id'];
|
||||
protected $casts = [
|
||||
'view' => 'boolean',
|
||||
'create' => 'boolean',
|
||||
'read' => 'boolean',
|
||||
'update' => 'boolean',
|
||||
'delete' => 'boolean',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the role assigned to this entity permission.
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -8,6 +8,8 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property string $name
|
||||
* @property string $display_name
|
||||
*/
|
||||
class RolePermission extends Model
|
||||
{
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -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', ''),
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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_'),
|
||||
|
||||
];
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,43 @@ 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'),
|
||||
'verify_peer' => env('MAIL_VERIFY_SSL', true),
|
||||
'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 +67,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'),
|
||||
|
||||
];
|
||||
|
||||
@@ -18,30 +18,11 @@ use BookStack\Entities\Models\PageRevision;
|
||||
*/
|
||||
class EntityProvider
|
||||
{
|
||||
/**
|
||||
* @var Bookshelf
|
||||
*/
|
||||
public $bookshelf;
|
||||
|
||||
/**
|
||||
* @var Book
|
||||
*/
|
||||
public $book;
|
||||
|
||||
/**
|
||||
* @var Chapter
|
||||
*/
|
||||
public $chapter;
|
||||
|
||||
/**
|
||||
* @var Page
|
||||
*/
|
||||
public $page;
|
||||
|
||||
/**
|
||||
* @var PageRevision
|
||||
*/
|
||||
public $pageRevision;
|
||||
public Bookshelf $bookshelf;
|
||||
public Book $book;
|
||||
public Chapter $chapter;
|
||||
public Page $page;
|
||||
public PageRevision $pageRevision;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@@ -69,13 +50,18 @@ class EntityProvider
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an entity instance by it's basic name.
|
||||
* Get an entity instance by its basic name.
|
||||
*/
|
||||
public function get(string $type): Entity
|
||||
{
|
||||
$type = strtolower($type);
|
||||
$instance = $this->all()[$type] ?? null;
|
||||
|
||||
return $this->all()[$type];
|
||||
if (is_null($instance)) {
|
||||
throw new \InvalidArgumentException("Provided type \"{$type}\" is not a valid entity type");
|
||||
}
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -295,7 +304,9 @@ class PageContent
|
||||
if ($blankIncludes) {
|
||||
$content = $this->blankPageIncludes($content);
|
||||
} else {
|
||||
$content = $this->parsePageIncludes($content);
|
||||
for ($includeDepth = 0; $includeDepth < 3; $includeDepth++) {
|
||||
$content = $this->parsePageIncludes($content);
|
||||
}
|
||||
}
|
||||
|
||||
return $content;
|
||||
@@ -440,8 +451,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;
|
||||
}
|
||||
|
||||
@@ -4,20 +4,20 @@ namespace BookStack\Entities\Tools;
|
||||
|
||||
use BookStack\Actions\ActivityType;
|
||||
use BookStack\Auth\Permissions\EntityPermission;
|
||||
use BookStack\Auth\Role;
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Entities\Models\Book;
|
||||
use BookStack\Entities\Models\Bookshelf;
|
||||
use BookStack\Entities\Models\Entity;
|
||||
use BookStack\Facades\Activity;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class PermissionsUpdater
|
||||
{
|
||||
/**
|
||||
* Update an entities permissions from a permission form submit request.
|
||||
*/
|
||||
public function updateFromPermissionsForm(Entity $entity, Request $request)
|
||||
public function updateFromPermissionsForm(Entity $entity, Request $request): void
|
||||
{
|
||||
$permissions = $request->get('permissions', null);
|
||||
$ownerId = $request->get('owned_by', null);
|
||||
@@ -39,12 +39,44 @@ class PermissionsUpdater
|
||||
Activity::add(ActivityType::PERMISSIONS_UPDATE, $entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update permissions from API request data.
|
||||
*/
|
||||
public function updateFromApiRequestData(Entity $entity, array $data): void
|
||||
{
|
||||
if (isset($data['role_permissions'])) {
|
||||
$entity->permissions()->where('role_id', '!=', 0)->delete();
|
||||
$rolePermissionData = $this->formatPermissionsFromApiRequestToEntityPermissions($data['role_permissions'] ?? [], false);
|
||||
$entity->permissions()->createMany($rolePermissionData);
|
||||
}
|
||||
|
||||
if (array_key_exists('fallback_permissions', $data)) {
|
||||
$entity->permissions()->where('role_id', '=', 0)->delete();
|
||||
}
|
||||
|
||||
if (isset($data['fallback_permissions']['inheriting']) && $data['fallback_permissions']['inheriting'] !== true) {
|
||||
$data = $data['fallback_permissions'];
|
||||
$data['role_id'] = 0;
|
||||
$rolePermissionData = $this->formatPermissionsFromApiRequestToEntityPermissions([$data], true);
|
||||
$entity->permissions()->createMany($rolePermissionData);
|
||||
}
|
||||
|
||||
if (isset($data['owner_id'])) {
|
||||
$this->updateOwnerFromId($entity, intval($data['owner_id']));
|
||||
}
|
||||
|
||||
$entity->save();
|
||||
$entity->rebuildPermissions();
|
||||
|
||||
Activity::add(ActivityType::PERMISSIONS_UPDATE, $entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the owner of the given entity.
|
||||
* Checks the user exists in the system first.
|
||||
* Does not save the model, just updates it.
|
||||
*/
|
||||
protected function updateOwnerFromId(Entity $entity, int $newOwnerId)
|
||||
protected function updateOwnerFromId(Entity $entity, int $newOwnerId): void
|
||||
{
|
||||
$newOwner = User::query()->find($newOwnerId);
|
||||
if (!is_null($newOwner)) {
|
||||
@@ -67,7 +99,41 @@ class PermissionsUpdater
|
||||
$formatted[] = $entityPermissionData;
|
||||
}
|
||||
|
||||
return $formatted;
|
||||
return $this->filterEntityPermissionDataUponRole($formatted, true);
|
||||
}
|
||||
|
||||
protected function formatPermissionsFromApiRequestToEntityPermissions(array $permissions, bool $allowFallback): array
|
||||
{
|
||||
$formatted = [];
|
||||
|
||||
foreach ($permissions as $requestPermissionData) {
|
||||
$entityPermissionData = ['role_id' => $requestPermissionData['role_id']];
|
||||
foreach (EntityPermission::PERMISSIONS as $permission) {
|
||||
$entityPermissionData[$permission] = boolval($requestPermissionData[$permission] ?? false);
|
||||
}
|
||||
$formatted[] = $entityPermissionData;
|
||||
}
|
||||
|
||||
return $this->filterEntityPermissionDataUponRole($formatted, $allowFallback);
|
||||
}
|
||||
|
||||
protected function filterEntityPermissionDataUponRole(array $entityPermissionData, bool $allowFallback): array
|
||||
{
|
||||
$roleIds = [];
|
||||
foreach ($entityPermissionData as $permissionEntry) {
|
||||
$roleIds[] = intval($permissionEntry['role_id']);
|
||||
}
|
||||
|
||||
$actualRoleIds = array_unique(array_values(array_filter($roleIds)));
|
||||
$rolesById = Role::query()->whereIn('id', $actualRoleIds)->get('id')->keyBy('id');
|
||||
|
||||
return array_values(array_filter($entityPermissionData, function ($data) use ($rolesById, $allowFallback) {
|
||||
if (intval($data['role_id']) === 0) {
|
||||
return $allowFallback;
|
||||
}
|
||||
|
||||
return $rolesById->has($data['role_id']);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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)
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
100
app/Http/Controllers/Api/ContentPermissionApiController.php
Normal file
100
app/Http/Controllers/Api/ContentPermissionApiController.php
Normal file
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Http\Controllers\Api;
|
||||
|
||||
use BookStack\Entities\EntityProvider;
|
||||
use BookStack\Entities\Models\Entity;
|
||||
use BookStack\Entities\Tools\PermissionsUpdater;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ContentPermissionApiController extends ApiController
|
||||
{
|
||||
public function __construct(
|
||||
protected PermissionsUpdater $permissionsUpdater,
|
||||
protected EntityProvider $entities
|
||||
) {
|
||||
}
|
||||
|
||||
protected $rules = [
|
||||
'update' => [
|
||||
'owner_id' => ['int'],
|
||||
|
||||
'role_permissions' => ['array'],
|
||||
'role_permissions.*.role_id' => ['required', 'int', 'exists:roles,id'],
|
||||
'role_permissions.*.view' => ['required', 'boolean'],
|
||||
'role_permissions.*.create' => ['required', 'boolean'],
|
||||
'role_permissions.*.update' => ['required', 'boolean'],
|
||||
'role_permissions.*.delete' => ['required', 'boolean'],
|
||||
|
||||
'fallback_permissions' => ['nullable'],
|
||||
'fallback_permissions.inheriting' => ['required_with:fallback_permissions', 'boolean'],
|
||||
'fallback_permissions.view' => ['required_if:fallback_permissions.inheriting,false', 'boolean'],
|
||||
'fallback_permissions.create' => ['required_if:fallback_permissions.inheriting,false', 'boolean'],
|
||||
'fallback_permissions.update' => ['required_if:fallback_permissions.inheriting,false', 'boolean'],
|
||||
'fallback_permissions.delete' => ['required_if:fallback_permissions.inheriting,false', 'boolean'],
|
||||
]
|
||||
];
|
||||
|
||||
/**
|
||||
* Read the configured content-level permissions for the item of the given type and ID.
|
||||
* 'contentType' should be one of: page, book, chapter, bookshelf.
|
||||
* 'contentId' should be the relevant ID of that item type you'd like to handle permissions for.
|
||||
* The permissions shown are those that override the default for just the specified item, they do not show the
|
||||
* full evaluated permission for a role, nor do they reflect permissions inherited from other items in the hierarchy.
|
||||
* Fallback permission values may be `null` when inheriting is active.
|
||||
*/
|
||||
public function read(string $contentType, string $contentId)
|
||||
{
|
||||
$entity = $this->entities->get($contentType)
|
||||
->newQuery()->scopes(['visible'])->findOrFail($contentId);
|
||||
|
||||
$this->checkOwnablePermission('restrictions-manage', $entity);
|
||||
|
||||
return response()->json($this->formattedPermissionDataForEntity($entity));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the configured content-level permission overrides for the item of the given type and ID.
|
||||
* 'contentType' should be one of: page, book, chapter, bookshelf.
|
||||
* 'contentId' should be the relevant ID of that item type you'd like to handle permissions for.
|
||||
* Providing an empty `role_permissions` array will remove any existing configured role permissions,
|
||||
* so you may want to fetch existing permissions beforehand if just adding/removing a single item.
|
||||
* You should completely omit the `owner_id`, `role_permissions` and/or the `fallback_permissions` properties
|
||||
* from your request data if you don't wish to update details within those categories.
|
||||
*/
|
||||
public function update(Request $request, string $contentType, string $contentId)
|
||||
{
|
||||
$entity = $this->entities->get($contentType)
|
||||
->newQuery()->scopes(['visible'])->findOrFail($contentId);
|
||||
|
||||
$this->checkOwnablePermission('restrictions-manage', $entity);
|
||||
|
||||
$data = $this->validate($request, $this->rules()['update']);
|
||||
$this->permissionsUpdater->updateFromApiRequestData($entity, $data);
|
||||
|
||||
return response()->json($this->formattedPermissionDataForEntity($entity));
|
||||
}
|
||||
|
||||
protected function formattedPermissionDataForEntity(Entity $entity): array
|
||||
{
|
||||
$rolePermissions = $entity->permissions()
|
||||
->where('role_id', '!=', 0)
|
||||
->with(['role:id,display_name'])
|
||||
->get();
|
||||
|
||||
$fallback = $entity->permissions()->where('role_id', '=', 0)->first();
|
||||
$fallbackData = [
|
||||
'inheriting' => is_null($fallback),
|
||||
'view' => $fallback->view ?? null,
|
||||
'create' => $fallback->create ?? null,
|
||||
'update' => $fallback->update ?? null,
|
||||
'delete' => $fallback->delete ?? null,
|
||||
];
|
||||
|
||||
return [
|
||||
'owner' => $entity->ownedBy()->first(),
|
||||
'role_permissions' => $rolePermissions,
|
||||
'fallback_permissions' => $fallbackData,
|
||||
];
|
||||
}
|
||||
}
|
||||
146
app/Http/Controllers/Api/ImageGalleryApiController.php
Normal file
146
app/Http/Controllers/Api/ImageGalleryApiController.php
Normal file
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Http\Controllers\Api;
|
||||
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Uploads\Image;
|
||||
use BookStack\Uploads\ImageRepo;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ImageGalleryApiController extends ApiController
|
||||
{
|
||||
protected array $fieldsToExpose = [
|
||||
'id', 'name', 'url', 'path', 'type', 'uploaded_to', 'created_by', 'updated_by', 'created_at', 'updated_at',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
protected ImageRepo $imageRepo
|
||||
) {
|
||||
}
|
||||
|
||||
protected function rules(): array
|
||||
{
|
||||
return [
|
||||
'create' => [
|
||||
'type' => ['required', 'string', 'in:gallery,drawio'],
|
||||
'uploaded_to' => ['required', 'integer'],
|
||||
'image' => ['required', 'file', ...$this->getImageValidationRules()],
|
||||
'name' => ['string', 'max:180'],
|
||||
],
|
||||
'update' => [
|
||||
'name' => ['string', 'max:180'],
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a listing of images in the system. Includes gallery (page content) images and drawings.
|
||||
* Requires visibility of the page they're originally uploaded to.
|
||||
*/
|
||||
public function list()
|
||||
{
|
||||
$images = Image::query()->scopes(['visible'])
|
||||
->select($this->fieldsToExpose)
|
||||
->whereIn('type', ['gallery', 'drawio']);
|
||||
|
||||
return $this->apiListingResponse($images, [
|
||||
...$this->fieldsToExpose
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new image in the system.
|
||||
* Since "image" is expected to be a file, this needs to be a 'multipart/form-data' type request.
|
||||
* The provided "uploaded_to" should be an existing page ID in the system.
|
||||
* If the "name" parameter is omitted, the filename of the provided image file will be used instead.
|
||||
* The "type" parameter should be 'gallery' for page content images, and 'drawio' should only be used
|
||||
* when the file is a PNG file with diagrams.net image data embedded within.
|
||||
*/
|
||||
public function create(Request $request)
|
||||
{
|
||||
$this->checkPermission('image-create-all');
|
||||
$data = $this->validate($request, $this->rules()['create']);
|
||||
Page::visible()->findOrFail($data['uploaded_to']);
|
||||
|
||||
$image = $this->imageRepo->saveNew($data['image'], $data['type'], $data['uploaded_to']);
|
||||
|
||||
if (isset($data['name'])) {
|
||||
$image->refresh();
|
||||
$image->update(['name' => $data['name']]);
|
||||
}
|
||||
|
||||
return response()->json($this->formatForSingleResponse($image));
|
||||
}
|
||||
|
||||
/**
|
||||
* View the details of a single image.
|
||||
* The "thumbs" response property contains links to scaled variants that BookStack may use in its UI.
|
||||
* The "content" response property provides HTML and Markdown content, in the format that BookStack
|
||||
* would typically use by default to add the image in page content, as a convenience.
|
||||
* Actual image file data is not provided but can be fetched via the "url" response property.
|
||||
*/
|
||||
public function read(string $id)
|
||||
{
|
||||
$image = Image::query()->scopes(['visible'])->findOrFail($id);
|
||||
|
||||
return response()->json($this->formatForSingleResponse($image));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the details of an existing image in the system.
|
||||
* Only allows updating of the image name at this time.
|
||||
*/
|
||||
public function update(Request $request, string $id)
|
||||
{
|
||||
$data = $this->validate($request, $this->rules()['update']);
|
||||
$image = $this->imageRepo->getById($id);
|
||||
$this->checkOwnablePermission('page-view', $image->getPage());
|
||||
$this->checkOwnablePermission('image-update', $image);
|
||||
|
||||
$this->imageRepo->updateImageDetails($image, $data);
|
||||
|
||||
return response()->json($this->formatForSingleResponse($image));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an image from the system.
|
||||
* Will also delete thumbnails for the image.
|
||||
* Does not check or handle image usage so this could leave pages with broken image references.
|
||||
*/
|
||||
public function delete(string $id)
|
||||
{
|
||||
$image = $this->imageRepo->getById($id);
|
||||
$this->checkOwnablePermission('page-view', $image->getPage());
|
||||
$this->checkOwnablePermission('image-delete', $image);
|
||||
$this->imageRepo->destroyImage($image);
|
||||
|
||||
return response('', 204);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the given image model for single-result display.
|
||||
*/
|
||||
protected function formatForSingleResponse(Image $image): array
|
||||
{
|
||||
$this->imageRepo->loadThumbs($image);
|
||||
$data = $image->getAttributes();
|
||||
$data['created_by'] = $image->createdBy;
|
||||
$data['updated_by'] = $image->updatedBy;
|
||||
$data['content'] = [];
|
||||
|
||||
$escapedUrl = htmlentities($image->url);
|
||||
$escapedName = htmlentities($image->name);
|
||||
if ($image->type === 'drawio') {
|
||||
$data['content']['html'] = "<div drawio-diagram=\"{$image->id}\"><img src=\"{$escapedUrl}\"></div>";
|
||||
$data['content']['markdown'] = $data['content']['html'];
|
||||
} else {
|
||||
$escapedDisplayThumb = htmlentities($image->thumbs['display']);
|
||||
$data['content']['html'] = "<a href=\"{$escapedUrl}\" target=\"_blank\"><img src=\"{$escapedDisplayThumb}\" alt=\"{$escapedName}\"></a>";
|
||||
$mdEscapedName = str_replace(']', '', str_replace('[', '', $image->name));
|
||||
$mdEscapedThumb = str_replace(']', '', str_replace('[', '', $image->thumbs['display']));
|
||||
$data['content']['markdown'] = "";
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
136
app/Http/Controllers/Api/RoleApiController.php
Normal file
136
app/Http/Controllers/Api/RoleApiController.php
Normal 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)
|
||||
{
|
||||
$role = $this->permissionsRepo->getRoleById($id);
|
||||
$this->singleFormatter($role);
|
||||
|
||||
return response()->json($role);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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']);
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
];
|
||||
|
||||
|
||||
@@ -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']), [
|
||||
|
||||
@@ -14,21 +14,11 @@ use Illuminate\Http\Request;
|
||||
|
||||
class ConfirmEmailController extends Controller
|
||||
{
|
||||
protected EmailConfirmationService $emailConfirmationService;
|
||||
protected LoginService $loginService;
|
||||
protected UserRepo $userRepo;
|
||||
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*/
|
||||
public function __construct(
|
||||
EmailConfirmationService $emailConfirmationService,
|
||||
LoginService $loginService,
|
||||
UserRepo $userRepo
|
||||
protected EmailConfirmationService $emailConfirmationService,
|
||||
protected LoginService $loginService,
|
||||
protected UserRepo $userRepo
|
||||
) {
|
||||
$this->emailConfirmationService = $emailConfirmationService;
|
||||
$this->loginService = $loginService;
|
||||
$this->userRepo = $userRepo;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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()]));
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,14 +10,9 @@ use Illuminate\Validation\ValidationException;
|
||||
|
||||
class GalleryImageController extends Controller
|
||||
{
|
||||
protected $imageRepo;
|
||||
|
||||
/**
|
||||
* GalleryImageController constructor.
|
||||
*/
|
||||
public function __construct(ImageRepo $imageRepo)
|
||||
{
|
||||
$this->imageRepo = $imageRepo;
|
||||
public function __construct(
|
||||
protected ImageRepo $imageRepo
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -47,9 +42,14 @@ class GalleryImageController extends Controller
|
||||
public function create(Request $request)
|
||||
{
|
||||
$this->checkPermission('image-create-all');
|
||||
$this->validate($request, [
|
||||
'file' => $this->getImageValidationRules(),
|
||||
]);
|
||||
|
||||
try {
|
||||
$this->validate($request, [
|
||||
'file' => $this->getImageValidationRules(),
|
||||
]);
|
||||
} catch (ValidationException $exception) {
|
||||
return $this->jsonError(implode("\n", $exception->errors()['file']));
|
||||
}
|
||||
|
||||
try {
|
||||
$imageUpload = $request->file('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,14 @@ class RoleController extends Controller
|
||||
$this->checkPermission('user-roles-manage');
|
||||
|
||||
try {
|
||||
$this->permissionsRepo->deleteRole($id, $request->get('migrate_role_id'));
|
||||
$migrateRoleId = intval($request->get('migrate_role_id') ?: "0");
|
||||
$this->permissionsRepo->deleteRole($id, $migrateRoleId);
|
||||
} catch (PermissionsException $e) {
|
||||
$this->showErrorNotification($e->getMessage());
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
$this->showSuccessNotification(trans('settings.role_delete_success'));
|
||||
|
||||
return redirect('/settings/roles');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,9 @@ use Illuminate\Http\Request;
|
||||
|
||||
class TagController extends Controller
|
||||
{
|
||||
protected TagRepo $tagRepo;
|
||||
|
||||
public function __construct(TagRepo $tagRepo)
|
||||
{
|
||||
$this->tagRepo = $tagRepo;
|
||||
public function __construct(
|
||||
protected TagRepo $tagRepo
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -197,7 +197,7 @@ class UserController extends Controller
|
||||
$this->checkPermissionOrCurrentUser('users-manage', $id);
|
||||
|
||||
$user = $this->userRepo->getById($id);
|
||||
$newOwnerId = $request->get('new_owner_id', null);
|
||||
$newOwnerId = intval($request->get('new_owner_id')) ?: null;
|
||||
|
||||
$this->userRepo->destroy($user, $newOwnerId);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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]');
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -173,6 +173,7 @@ class SearchRunner
|
||||
// Handle exact term matching
|
||||
foreach ($searchOpts->exacts as $inputTerm) {
|
||||
$entityQuery->where(function (EloquentBuilder $query) use ($inputTerm, $entityModelInstance) {
|
||||
$inputTerm = str_replace('\\', '\\\\', $inputTerm);
|
||||
$query->where('name', 'like', '%' . $inputTerm . '%')
|
||||
->orWhere($entityModelInstance->textField, 'like', '%' . $inputTerm . '%');
|
||||
});
|
||||
@@ -218,6 +219,7 @@ class SearchRunner
|
||||
$subQuery->where('entity_type', '=', $entity->getMorphClass());
|
||||
$subQuery->where(function (Builder $query) use ($terms) {
|
||||
foreach ($terms as $inputTerm) {
|
||||
$inputTerm = str_replace('\\', '\\\\', $inputTerm);
|
||||
$query->orWhere('term', 'like', $inputTerm . '%');
|
||||
}
|
||||
});
|
||||
@@ -354,6 +356,9 @@ class SearchRunner
|
||||
$tagValue = (float) trim($connection->getPdo()->quote($tagValue), "'");
|
||||
$query->whereRaw("value {$tagOperator} {$tagValue}");
|
||||
} else {
|
||||
if ($tagOperator === 'like') {
|
||||
$tagValue = str_replace('\\', '\\\\', $tagValue);
|
||||
}
|
||||
$query->where('value', $tagOperator, $tagValue);
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,11 +65,24 @@ 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';
|
||||
|
||||
/**
|
||||
* OIDC ID token pre-validate event.
|
||||
* Runs just before BookStack validates the user ID token data upon login.
|
||||
* Provides the existing found set of claims for the user as a key-value array,
|
||||
* along with an array of the proceeding access token data provided by the identity platform.
|
||||
* If the listener returns a non-null value, that will replace the existing ID token claim data.
|
||||
*
|
||||
* @param array $idTokenData
|
||||
* @param array $accessTokenData
|
||||
* @returns array|null
|
||||
*/
|
||||
const OIDC_ID_TOKEN_PRE_VALIDATE = 'oidc_id_token_pre_validate';
|
||||
|
||||
/**
|
||||
* Page include parse event.
|
||||
* Runs when a page include tag is being parsed, typically when page content is being processed for viewing.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
110
app/Uploads/FaviconHandler.php
Normal file
110
app/Uploads/FaviconHandler.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,11 @@
|
||||
namespace BookStack\Uploads;
|
||||
|
||||
use BookStack\Auth\Permissions\JointPermission;
|
||||
use BookStack\Auth\Permissions\PermissionApplicator;
|
||||
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\HasMany;
|
||||
|
||||
@@ -33,12 +35,21 @@ class Image extends Model
|
||||
->where('joint_permissions.entity_type', '=', 'page');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope the query to just the images visible to the user based upon the
|
||||
* user visibility of the uploaded_to page.
|
||||
*/
|
||||
public function scopeVisible(Builder $query): Builder
|
||||
{
|
||||
return app()->make(PermissionApplicator::class)->restrictPageRelationQuery($query, 'images', 'uploaded_to');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a thumbnail for this image.
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getThumb(int $width, int $height, bool $keepRatio = false): string
|
||||
public function getThumb(?int $width, ?int $height, bool $keepRatio = false): string
|
||||
{
|
||||
return app()->make(ImageService::class)->getThumbnail($this, $width, $height, $keepRatio);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -24,6 +24,7 @@ class LanguageManager
|
||||
'bg' => ['iso' => 'bg_BG', 'windows' => 'Bulgarian'],
|
||||
'bs' => ['iso' => 'bs_BA', 'windows' => 'Bosnian (Latin)'],
|
||||
'ca' => ['iso' => 'ca', 'windows' => 'Catalan'],
|
||||
'cs' => ['iso' => 'cs_CZ', 'windows' => 'Czech'],
|
||||
'da' => ['iso' => 'da_DK', 'windows' => 'Danish'],
|
||||
'de' => ['iso' => 'de_DE', 'windows' => 'German'],
|
||||
'de_informal' => ['iso' => 'de_DE', 'windows' => 'German'],
|
||||
@@ -120,17 +121,17 @@ class LanguageManager
|
||||
$isoLang = $this->localeMap[$language]['iso'] ?? '';
|
||||
$isoLangPrefix = explode('_', $isoLang)[0];
|
||||
|
||||
$locales = array_filter([
|
||||
$locales = array_values(array_filter([
|
||||
$isoLang ? $isoLang . '.utf8' : false,
|
||||
$isoLang ?: false,
|
||||
$isoLang ? str_replace('_', '-', $isoLang) : false,
|
||||
$isoLang ? $isoLangPrefix . '.UTF-8' : false,
|
||||
$this->localeMap[$language]['windows'] ?? false,
|
||||
$language,
|
||||
]);
|
||||
]));
|
||||
|
||||
if (!empty($locales)) {
|
||||
setlocale(LC_TIME, ...$locales);
|
||||
setlocale(LC_TIME, $locales[0], ...array_slice($locales, 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,7 +147,7 @@ function icon(string $name, array $attrs = []): string
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a url with multiple parameters for sorting purposes.
|
||||
* Generate a URL with multiple parameters for sorting purposes.
|
||||
* Works out the logic to set the correct sorting direction
|
||||
* Discards empty parameters and allows overriding.
|
||||
*/
|
||||
@@ -172,7 +172,7 @@ function sortUrl(string $path, array $data, array $overrideData = []): string
|
||||
}
|
||||
|
||||
if (count($queryStringSections) === 0) {
|
||||
return $path;
|
||||
return url($path);
|
||||
}
|
||||
|
||||
return url($path . '?' . implode('&', $queryStringSections));
|
||||
|
||||
BIN
bookstack-system-cli
Executable file
BIN
bookstack-system-cli
Executable file
Binary file not shown.
@@ -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,39 +19,37 @@
|
||||
"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"
|
||||
"ssddanbrown/asserthtml": "^2.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
@@ -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
|
||||
}
|
||||
|
||||
4032
composer.lock
generated
4032
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
39
database/factories/Uploads/AttachmentFactory.php
Normal file
39
database/factories/Uploads/AttachmentFactory.php
Normal 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,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user