Compare commits

..

24 Commits

Author SHA1 Message Date
Dan Brown
3083979855 Added method for using enity ownership in relation queries
It has a large linear-entity-scaling performance impact though.
2023-01-15 17:38:08 +00:00
Dan Brown
55642a33ee Attempted fix of issues, realised new query system is a failure
As part of the permission checking we need to check owner user status.
Upon this, we'd also want to check page draft status (and its
creator/owner).
These, for cross-entity/relation queries would need up to another 4 joins.
The performance/index usage is already questionable here.
2023-01-14 13:50:41 +00:00
Dan Brown
93ba572369 Aligned admin permission check restriction ignore 2023-01-13 22:19:29 +00:00
Dan Brown
a825f27930 Updated additional relation queries to apply permissions correctly 2023-01-13 22:13:31 +00:00
Dan Brown
932e1d7c61 Got entity relation query permission application working
May be issues at points of use though, Added todo for this in code.
Also added extra indexes to collapsed table for better query
performance.
2023-01-13 17:10:20 +00:00
Dan Brown
2f1491c5a4 Split out 'restrictEntityQuery' function components
Also fixed search query issue with abiguous column
2023-01-13 16:07:36 +00:00
Dan Brown
026e9030b9 Reworked userCan permission check to follow defined logic.
Got all current scenario tests passing.
Also fixes own permission which was using the wrong field.
2022-12-23 21:07:49 +00:00
Dan Brown
451e4ac452 Fixed collapsed perm. gen for book sub-items.
Also converted the existing "JointPermission" usage to the new
collapsed permission system.
2022-12-23 14:05:43 +00:00
Dan Brown
7330139555 Created big scary query to apply permissions via new format 2022-12-22 20:32:06 +00:00
Dan Brown
39acbeac68 Started new permission-caching/querying model 2022-12-22 15:09:17 +00:00
Dan Brown
2d9d2bba80 Added additional case thats known to currently fail
Also removed so no-longer-relevant todo/comments.
2022-12-21 17:14:54 +00:00
Dan Brown
adabf06dbe Added more inter-method permissions test cases 2022-12-20 19:10:09 +00:00
Dan Brown
5ffc10e688 Added entity user permission scenarios
Also added definitions for general expected behaviour to readme doc, and
added some entity role inherit scenarios to check they meet expectations.
Currently failing role test but not an issue with test, needs fixing to
app logic.
2022-12-20 15:50:41 +00:00
Dan Brown
6a6f5e4d19 Added a bunch of role content permissions 2022-12-17 19:46:48 +00:00
Dan Brown
491beee93e Added additional entity_role_permission scenario tests 2022-12-17 15:27:09 +00:00
Dan Brown
f844ae0902 Create additional test helper classes
Following recent similar actions done for entities.
Required at this stage to provider better & cleaner helpers
for common user and permission actions to built out permission testing.
2022-12-15 12:29:10 +00:00
Dan Brown
d54ea1b3ed Started more formal permission test case definitions 2022-12-15 11:22:53 +00:00
Dan Brown
e8a8fedfd6 Started aligning permission behaviour across application methods 2022-12-14 18:14:01 +00:00
Dan Brown
60bf838a4a Added joint_user_permissions handling to query system
Some issues exist to resolve though, not in final state.
2022-12-11 22:53:46 +00:00
Dan Brown
0411185fbb Added, and built perm. gen for, joint_user_permissions table 2022-12-11 14:51:53 +00:00
Dan Brown
93cbd3b8aa Improved user-permissions adding ux
- Reset input after user selection.
- Corrected permission row title text for user rows.
2022-12-10 14:48:19 +00:00
Dan Brown
7a269e7689 Added users to permission form interface
Also updated non-joint permission handling to support user permissions.
2022-12-10 14:37:18 +00:00
Dan Brown
f8c4725166 Aligned logic to entity_permission role_id usage change
Now idenitifies fallback using role_id and user_id = null.
Lays some foundations for handling user_id.
2022-12-07 22:07:03 +00:00
Dan Brown
1c53ffc4d1 Updated entity_permissions table for user perms.
As start of user permissions work
2022-12-07 14:57:23 +00:00
1295 changed files with 18270 additions and 26302 deletions

View File

@@ -37,10 +37,8 @@ MAIL_FROM=bookstack@example.com
# SMTP mail options
# These settings can be checked using the "Send a Test Email"
# feature found in the "Settings > Maintenance" area of the system.
# For more detailed documentation on mail options, refer to:
# https://www.bookstackapp.com/docs/admin/email-webhooks/#email-configuration
MAIL_HOST=localhost
MAIL_PORT=587
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null

View File

@@ -3,10 +3,6 @@
# 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
@@ -69,20 +65,20 @@ DB_PASSWORD=database_user_password
# certificate itself (Common Name or Subject Alternative Name).
MYSQL_ATTR_SSL_CA="/path/to/ca.pem"
# Mail configuration
# Refer to https://www.bookstackapp.com/docs/admin/email-webhooks/#email-configuration
# Mail system to use
# Can be 'smtp' or 'sendmail'
MAIL_DRIVER=smtp
# Mail sending options
MAIL_FROM=mail@bookstackapp.com
MAIL_FROM_NAME=BookStack
# SMTP mail options
MAIL_HOST=localhost
MAIL_PORT=587
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_VERIFY_SSL=true
MAIL_SENDMAIL_COMMAND="/usr/sbin/sendmail -bs"
# Cache & Session driver to use
# Can be 'file', 'database', 'memcached' or 'redis'
@@ -272,7 +268,6 @@ OIDC_DUMP_USER_DETAILS=false
OIDC_USER_TO_GROUPS=false
OIDC_GROUPS_CLAIM=groups
OIDC_REMOVE_FROM_GROUPS=false
OIDC_EXTERNAL_ID_CLAIM=sub
# Disable default third-party services such as Gravatar and Draw.IO
# Service-specific options will override this option
@@ -323,13 +318,6 @@ 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
@@ -380,4 +368,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

View File

@@ -176,7 +176,7 @@ Alexander Predl (Harveyhase68) :: German
Rem (Rem9000) :: Dutch
Michał Stelmach (stelmach-web) :: Polish
arniom :: French
REMOVED_USER :: ; French; Dutch; Turkish
REMOVED_USER :: ; Dutch; Turkish
林祖年 (contagion) :: Chinese Traditional
Siamak Guodarzi (siamakgoudarzi88) :: Persian
Lis Maestrelo (lismtrl) :: Portuguese, Brazilian
@@ -283,13 +283,13 @@ Kuchinashi Hoshikawa (kuchinashi) :: Chinese Simplified
digilady :: Greek
Linus (LinusOP) :: Swedish
Felipe Cardoso (felipecardosoruff) :: Portuguese, Brazilian
RandomUser0815 :: German Informal; German
RandomUser0815 :: German
Ismael Mesquita (mesquitoliveira) :: Portuguese, Brazilian
구인회 (laskdjlaskdj12) :: Korean
LiZerui (CNLiZerui) :: Chinese Traditional
Fabrice Boyer (FabriceBoyer) :: French
mikael (bitcanon) :: Swedish
Matthias Mai (schnapsidee) :: German; German Informal
Matthias Mai (schnapsidee) :: German
Ufuk Ayyıldız (ufukayyildiz) :: Turkish
Jan Mitrof (jan.kachlik) :: Czech
edwardsmirnov :: Russian
@@ -298,49 +298,3 @@ shotu :: French
Cesar_Lopez_Aguillon :: Spanish
bdewoop :: German
dina davoudi (dina.davoudi) :: Persian
Angelos Chouvardas (achouvardas) :: Greek
rndrss :: Portuguese, Brazilian
rirac294 :: Russian
David Furman (thefourCraft) :: Hebrew
Pafzedog :: French
Yllelder :: Spanish
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
Le Van Chinh (Chino) (lvanchinh86) :: Vietnamese
Rubens nagios (rubenix) :: Catalan
Patrick Dantas (pa-tiq) :: Portuguese, Brazilian
Michal (michalgurcik) :: Slovak
Nepomacs :: German
Rubens (rubenix) :: Catalan
m4z :: German; German Informal
TheRazvy :: Romanian
Yossi Zilber (lortens) :: Hebrew; Uzbek
desdinova :: French
Ingus Rūķis (ingus.rukis) :: Latvian
Eugene Pershin (SilentEugene) :: Russian
周盛道 (zhoushengdao) :: Chinese Simplified
hamidreza amini (hamidrezaamini2022) :: Persian
Tomislav Kraljević (tomislav.kraljevic) :: Croatian
Taygun Yıldırım (yildirimtaygun) :: Turkish
robing29 :: German

View File

@@ -1,16 +0,0 @@
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

View File

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

View File

@@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-22.04
strategy:
matrix:
php: ['8.0', '8.1', '8.2']
php: ['7.4', '8.0', '8.1', '8.2']
steps:
- uses: actions/checkout@v1
@@ -16,7 +16,7 @@ jobs:
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: gd, mbstring, json, curl, xml, mysql, ldap, gmp
extensions: gd, mbstring, json, curl, xml, mysql, ldap
- name: Get Composer Cache Directory
id: composer-cache

13
.gitignore vendored
View File

@@ -1,19 +1,16 @@
/vendor
/node_modules
/.vscode
/composer
Homestead.yaml
.env
.idea
npm-debug.log
yarn-error.log
/public/dist/*.map
/public/dist
/public/plugins
/public/css/*.map
/public/js/*.map
/public/css
/public/js
/public/bower
/public/build/
/public/favicon.ico
/storage/images
_ide_helper.php
/storage/debugbar
@@ -23,10 +20,8 @@ yarn.lock
nbproject
.buildpath
.project
.nvmrc
.settings/
webpack-stats.json
.phpunit.result.cache
.DS_Store
phpstan.neon
esbuild-meta.json
phpstan.neon

View File

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

View File

@@ -1,110 +0,0 @@
<?php
namespace BookStack\Access;
/**
* Class Ldap
* An object-orientated thin abstraction wrapper for common PHP LDAP functions.
* Allows the standard LDAP functions to be mocked for testing.
*/
class Ldap
{
/**
* Connect to an LDAP server.
*
* @return resource|\LDAP\Connection|false
*/
public function connect(string $hostName)
{
return ldap_connect($hostName);
}
/**
* Set the value of an LDAP option for the given connection.
*
* @param resource|\LDAP\Connection|null $ldapConnection
*/
public function setOption($ldapConnection, int $option, mixed $value): bool
{
return ldap_set_option($ldapConnection, $option, $value);
}
/**
* Start TLS on the given LDAP connection.
*/
public function startTls($ldapConnection): bool
{
return ldap_start_tls($ldapConnection);
}
/**
* Set the version number for the given LDAP connection.
*
* @param resource|\LDAP\Connection $ldapConnection
*/
public function setVersion($ldapConnection, int $version): bool
{
return $this->setOption($ldapConnection, LDAP_OPT_PROTOCOL_VERSION, $version);
}
/**
* Search LDAP tree using the provided filter.
*
* @param resource|\LDAP\Connection $ldapConnection
*
* @return resource|\LDAP\Result
*/
public function search($ldapConnection, string $baseDn, string $filter, array $attributes = null)
{
return ldap_search($ldapConnection, $baseDn, $filter, $attributes);
}
/**
* Get entries from an LDAP search result.
*
* @param resource|\LDAP\Connection $ldapConnection
* @param resource|\LDAP\Result $ldapSearchResult
*/
public function getEntries($ldapConnection, $ldapSearchResult): array|false
{
return ldap_get_entries($ldapConnection, $ldapSearchResult);
}
/**
* Search and get entries immediately.
*
* @param resource|\LDAP\Connection $ldapConnection
*/
public function searchAndGetEntries($ldapConnection, string $baseDn, string $filter, array $attributes = null): array|false
{
$search = $this->search($ldapConnection, $baseDn, $filter, $attributes);
return $this->getEntries($ldapConnection, $search);
}
/**
* Bind to LDAP directory.
*
* @param resource|\LDAP\Connection $ldapConnection
*/
public function bind($ldapConnection, string $bindRdn = null, string $bindPassword = null): bool
{
return ldap_bind($ldapConnection, $bindRdn, $bindPassword);
}
/**
* Explode an LDAP dn string into an array of components.
*/
public function explodeDn(string $dn, int $withAttrib): array|false
{
return ldap_explode_dn($dn, $withAttrib);
}
/**
* Escape a string for use in an LDAP filter.
*/
public function escape(string $value, string $ignore = '', int $flags = 0): string
{
return ldap_escape($value, $ignore, $flags);
}
}

View File

@@ -1,13 +1,11 @@
<?php
namespace BookStack\Activity\Models;
namespace BookStack\Actions;
use BookStack\App\Model;
use BookStack\Auth\User;
use BookStack\Entities\Models\Entity;
use BookStack\Permissions\Models\JointPermission;
use BookStack\Users\Models\User;
use BookStack\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Support\Str;
@@ -19,8 +17,6 @@ use Illuminate\Support\Str;
* @property string $entity_type
* @property int $entity_id
* @property int $user_id
* @property Carbon $created_at
* @property Carbon $updated_at
*/
class Activity extends Model
{
@@ -44,12 +40,6 @@ class Activity extends Model
return $this->belongsTo(User::class);
}
public function jointPermissions(): HasMany
{
return $this->hasMany(JointPermission::class, 'entity_id', 'entity_id')
->whereColumn('activities.entity_type', '=', 'joint_permissions.entity_type');
}
/**
* Returns text from the language files, Looks up by using the activity key.
*/

View File

@@ -1,13 +1,10 @@
<?php
namespace BookStack\Activity\Tools;
namespace BookStack\Actions;
use BookStack\Activity\DispatchWebhookJob;
use BookStack\Activity\Models\Activity;
use BookStack\Activity\Models\Loggable;
use BookStack\Activity\Models\Webhook;
use BookStack\Entities\Models\Entity;
use BookStack\Facades\Theme;
use BookStack\Interfaces\Loggable;
use BookStack\Theming\ThemeEvents;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Log;

View File

@@ -1,14 +1,13 @@
<?php
namespace BookStack\Activity;
namespace BookStack\Actions;
use BookStack\Activity\Models\Activity;
use BookStack\Auth\Permissions\PermissionApplicator;
use BookStack\Auth\User;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\Page;
use BookStack\Permissions\PermissionApplicator;
use BookStack\Users\Models\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\Relation;
@@ -16,6 +15,8 @@ class ActivityQueries
{
protected PermissionApplicator $permissions;
protected array $fieldsForLists = ['id', 'type', 'detail', 'activities.entity_type', 'activities.entity_id', 'user_id', 'created_at'];
public function __construct(PermissionApplicator $permissions)
{
$this->permissions = $permissions;
@@ -26,9 +27,11 @@ class ActivityQueries
*/
public function latest(int $count = 20, int $page = 0): array
{
$query = Activity::query()->select($this->fieldsForLists);
$activityList = $this->permissions
->restrictEntityRelationQuery(Activity::query(), 'activities', 'entity_id', 'entity_type')
->restrictEntityRelationQuery($query, 'activities', 'entity_id', 'entity_type')
->orderBy('created_at', 'desc')
->whereNotNull('activities.entity_id')
->with(['user', 'entity'])
->skip($count * $page)
->take($count)
@@ -79,10 +82,12 @@ class ActivityQueries
*/
public function userActivity(User $user, int $count = 20, int $page = 0): array
{
$query = Activity::query()->select($this->fieldsForLists);
$activityList = $this->permissions
->restrictEntityRelationQuery(Activity::query(), 'activities', 'entity_id', 'entity_type')
->restrictEntityRelationQuery($query, 'activities', 'entity_id', 'entity_type')
->orderBy('created_at', 'desc')
->where('user_id', '=', $user->id)
->whereNotNull('activities.entity_id')
->skip($count * $page)
->take($count)
->get();

View File

@@ -1,6 +1,6 @@
<?php
namespace BookStack\Activity;
namespace BookStack\Actions;
class ActivityType
{

View File

@@ -1,9 +1,9 @@
<?php
namespace BookStack\Activity\Models;
namespace BookStack\Actions;
use BookStack\App\Model;
use BookStack\Users\Models\HasCreatorAndUpdater;
use BookStack\Model;
use BookStack\Traits\HasCreatorAndUpdater;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\MorphTo;

View File

@@ -1,20 +1,32 @@
<?php
namespace BookStack\Activity;
namespace BookStack\Actions;
use BookStack\Activity\Models\Comment;
use BookStack\Entities\Models\Entity;
use BookStack\Facades\Activity as ActivityService;
use League\CommonMark\CommonMarkConverter;
/**
* Class CommentRepo.
*/
class CommentRepo
{
/**
* @var Comment
*/
protected $comment;
public function __construct(Comment $comment)
{
$this->comment = $comment;
}
/**
* Get a comment by ID.
*/
public function getById(int $id): Comment
{
return Comment::query()->findOrFail($id);
return $this->comment->newQuery()->findOrFail($id);
}
/**
@@ -23,7 +35,7 @@ class CommentRepo
public function create(Entity $entity, string $text, ?int $parent_id): Comment
{
$userId = user()->id;
$comment = new Comment();
$comment = $this->comment->newInstance();
$comment->text = $text;
$comment->html = $this->commentToHtml($text);
@@ -70,7 +82,7 @@ class CommentRepo
'allow_unsafe_links' => false,
]);
return $converter->convert($commentText);
return $converter->convertToHtml($commentText);
}
/**
@@ -78,8 +90,9 @@ class CommentRepo
*/
protected function getNextLocalId(Entity $entity): int
{
$currentMaxId = $entity->comments()->max('local_id');
/** @var Comment $comment */
$comment = $entity->comments(false)->orderBy('local_id', 'desc')->first();
return $currentMaxId + 1;
return ($comment->local_id ?? 0) + 1;
}
}

View File

@@ -1,13 +1,11 @@
<?php
namespace BookStack\Activity;
namespace BookStack\Actions;
use BookStack\Activity\Models\Loggable;
use BookStack\Activity\Models\Webhook;
use BookStack\Activity\Tools\WebhookFormatter;
use BookStack\Auth\User;
use BookStack\Facades\Theme;
use BookStack\Interfaces\Loggable;
use BookStack\Theming\ThemeEvents;
use BookStack\Users\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
@@ -24,23 +22,27 @@ class DispatchWebhookJob implements ShouldQueue
use SerializesModels;
protected Webhook $webhook;
protected string $event;
protected User $initiator;
protected int $initiatedTime;
protected array $webhookData;
/**
* @var string|Loggable
*/
protected $detail;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(Webhook $webhook, string $event, Loggable|string $detail)
public function __construct(Webhook $webhook, string $event, $detail)
{
$this->webhook = $webhook;
$this->event = $event;
$this->detail = $detail;
$this->initiator = user();
$this->initiatedTime = time();
$themeResponse = Theme::dispatch(ThemeEvents::WEBHOOK_CALL_BEFORE, $event, $this->webhook, $detail, $this->initiator, $this->initiatedTime);
$this->webhookData = $themeResponse ?? WebhookFormatter::getDefault($event, $this->webhook, $detail, $this->initiator, $this->initiatedTime)->format();
}
/**
@@ -50,13 +52,15 @@ class DispatchWebhookJob implements ShouldQueue
*/
public function handle()
{
$themeResponse = Theme::dispatch(ThemeEvents::WEBHOOK_CALL_BEFORE, $this->event, $this->webhook, $this->detail, $this->initiator, $this->initiatedTime);
$webhookData = $themeResponse ?? WebhookFormatter::getDefault($this->event, $this->webhook, $this->detail, $this->initiator, $this->initiatedTime)->format();
$lastError = null;
try {
$response = Http::asJson()
->withOptions(['allow_redirects' => ['strict' => true]])
->timeout($this->webhook->timeout)
->post($this->webhook->endpoint, $this->webhookData);
->post($this->webhook->endpoint, $webhookData);
} catch (\Exception $exception) {
$lastError = $exception->getMessage();
Log::error("Webhook call to endpoint {$this->webhook->endpoint} failed with error \"{$lastError}\"");

19
app/Actions/Favourite.php Normal file
View File

@@ -0,0 +1,19 @@
<?php
namespace BookStack\Actions;
use BookStack\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
class Favourite extends Model
{
protected $fillable = ['user_id'];
/**
* Get the related model that can be favourited.
*/
public function favouritable(): MorphTo
{
return $this->morphTo();
}
}

View File

@@ -1,6 +1,6 @@
<?php
namespace BookStack\Activity\Tools;
namespace BookStack\Actions;
class IpFormatter
{

View File

@@ -1,8 +1,8 @@
<?php
namespace BookStack\Activity\Queries;
namespace BookStack\Actions\Queries;
use BookStack\Activity\Models\Webhook;
use BookStack\Actions\Webhook;
use BookStack\Util\SimpleListOptions;
use Illuminate\Pagination\LengthAwarePaginator;

View File

@@ -1,11 +1,9 @@
<?php
namespace BookStack\Activity\Models;
namespace BookStack\Actions;
use BookStack\App\Model;
use BookStack\Permissions\Models\JointPermission;
use BookStack\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphTo;
/**
@@ -29,12 +27,6 @@ class Tag extends Model
return $this->morphTo('entity');
}
public function jointPermissions(): HasMany
{
return $this->hasMany(JointPermission::class, 'entity_id', 'entity_id')
->whereColumn('tags.entity_type', '=', 'joint_permissions.entity_type');
}
/**
* Get a full URL to start a tag name search for this tag name.
*/

View File

@@ -1,8 +1,6 @@
<?php
namespace BookStack\Activity\Tools;
use BookStack\Activity\Models\Tag;
namespace BookStack\Actions;
class TagClassGenerator
{

View File

@@ -1,10 +1,9 @@
<?php
namespace BookStack\Activity;
namespace BookStack\Actions;
use BookStack\Activity\Models\Tag;
use BookStack\Auth\Permissions\PermissionApplicator;
use BookStack\Entities\Models\Entity;
use BookStack\Permissions\PermissionApplicator;
use BookStack\Util\SimpleListOptions;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
@@ -12,9 +11,11 @@ use Illuminate\Support\Facades\DB;
class TagRepo
{
public function __construct(
protected PermissionApplicator $permissions
) {
protected PermissionApplicator $permissions;
public function __construct(PermissionApplicator $permissions)
{
$this->permissions = $permissions;
}
/**
@@ -28,15 +29,16 @@ class TagRepo
$sort = 'value';
}
$entityTypeCol = DB::getTablePrefix() . 'tags.entity_type';
$query = Tag::query()
->select([
'name',
($searchTerm || $nameFilter) ? 'value' : DB::raw('COUNT(distinct value) as `values`'),
DB::raw('COUNT(id) as usages'),
DB::raw('SUM(IF(entity_type = \'page\', 1, 0)) as page_count'),
DB::raw('SUM(IF(entity_type = \'chapter\', 1, 0)) as chapter_count'),
DB::raw('SUM(IF(entity_type = \'book\', 1, 0)) as book_count'),
DB::raw('SUM(IF(entity_type = \'bookshelf\', 1, 0)) as shelf_count'),
DB::raw("SUM(IF({$entityTypeCol} = 'page', 1, 0)) as page_count"),
DB::raw("SUM(IF({$entityTypeCol} = 'chapter', 1, 0)) as chapter_count"),
DB::raw("SUM(IF({$entityTypeCol} = 'book', 1, 0)) as book_count"),
DB::raw("SUM(IF({$entityTypeCol} = 'bookshelf', 1, 0)) as shelf_count"),
])
->orderBy($sort, $listOptions->getOrder());
@@ -89,7 +91,6 @@ class TagRepo
{
$query = Tag::query()
->select('*', DB::raw('count(*) as count'))
->where('value', '!=', '')
->groupBy('value');
if ($searchTerm) {

View File

@@ -1,10 +1,9 @@
<?php
namespace BookStack\Activity\Models;
namespace BookStack\Actions;
use BookStack\App\Model;
use BookStack\Permissions\Models\JointPermission;
use Illuminate\Database\Eloquent\Relations\HasMany;
use BookStack\Interfaces\Viewable;
use BookStack\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
/**
@@ -29,12 +28,6 @@ class View extends Model
return $this->morphTo();
}
public function jointPermissions(): HasMany
{
return $this->hasMany(JointPermission::class, 'entity_id', 'viewable_id')
->whereColumn('views.viewable_type', '=', 'joint_permissions.entity_type');
}
/**
* Increment the current user's view count for the given viewable model.
*/
@@ -54,4 +47,12 @@ class View extends Model
return $view->views;
}
/**
* Clear all views from the system.
*/
public static function clearAll()
{
static::query()->truncate();
}
}

View File

@@ -1,8 +1,8 @@
<?php
namespace BookStack\Activity\Models;
namespace BookStack\Actions;
use BookStack\Activity\ActivityType;
use BookStack\Interfaces\Loggable;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;

View File

@@ -1,14 +1,12 @@
<?php
namespace BookStack\Activity\Tools;
namespace BookStack\Actions;
use BookStack\Activity\ActivityType;
use BookStack\Activity\Models\Loggable;
use BookStack\Activity\Models\Webhook;
use BookStack\App\Model;
use BookStack\Auth\User;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\Page;
use BookStack\Users\Models\User;
use BookStack\Interfaces\Loggable;
use BookStack\Model;
use Illuminate\Support\Carbon;
class WebhookFormatter
@@ -17,14 +15,18 @@ class WebhookFormatter
protected string $event;
protected User $initiator;
protected int $initiatedTime;
protected string|Loggable $detail;
/**
* @var string|Loggable
*/
protected $detail;
/**
* @var array{condition: callable(string, Model):bool, format: callable(Model):void}[]
*/
protected $modelFormatters = [];
public function __construct(string $event, Webhook $webhook, string|Loggable $detail, User $initiator, int $initiatedTime)
public function __construct(string $event, Webhook $webhook, $detail, User $initiator, int $initiatedTime)
{
$this->webhook = $webhook;
$this->event = $event;

View File

@@ -1,6 +1,6 @@
<?php
namespace BookStack\Activity\Models;
namespace BookStack\Actions;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

View File

@@ -1,27 +0,0 @@
<?php
namespace BookStack\Activity\Models;
use BookStack\App\Model;
use BookStack\Permissions\Models\JointPermission;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphTo;
class Favourite extends Model
{
protected $fillable = ['user_id'];
/**
* Get the related model that can be favourited.
*/
public function favouritable(): MorphTo
{
return $this->morphTo();
}
public function jointPermissions(): HasMany
{
return $this->hasMany(JointPermission::class, 'entity_id', 'favouritable_id')
->whereColumn('favourites.favouritable_type', '=', 'joint_permissions.entity_type');
}
}

View File

@@ -1,102 +0,0 @@
<?php
namespace BookStack\Activity\Tools;
use BookStack\Activity\Models\Comment;
use BookStack\Entities\Models\Page;
class CommentTree
{
/**
* The built nested tree structure array.
* @var array{comment: Comment, depth: int, children: array}[]
*/
protected array $tree;
protected array $comments;
public function __construct(
protected Page $page
) {
$this->comments = $this->loadComments();
$this->tree = $this->createTree($this->comments);
}
public function enabled(): bool
{
return !setting('app-disable-comments');
}
public function empty(): bool
{
return count($this->tree) === 0;
}
public function count(): int
{
return count($this->comments);
}
public function get(): array
{
return $this->tree;
}
/**
* @param Comment[] $comments
*/
protected function createTree(array $comments): array
{
$byId = [];
foreach ($comments as $comment) {
$byId[$comment->local_id] = $comment;
}
$childMap = [];
foreach ($comments as $comment) {
$parent = $comment->parent_id;
if (is_null($parent) || !isset($byId[$parent])) {
$parent = 0;
}
if (!isset($childMap[$parent])) {
$childMap[$parent] = [];
}
$childMap[$parent][] = $comment->local_id;
}
$tree = [];
foreach ($childMap[0] ?? [] as $childId) {
$tree[] = $this->createTreeForId($childId, 0, $byId, $childMap);
}
return $tree;
}
protected function createTreeForId(int $id, int $depth, array &$byId, array &$childMap): array
{
$childIds = $childMap[$id] ?? [];
$children = [];
foreach ($childIds as $childId) {
$children[] = $this->createTreeForId($childId, $depth + 1, $byId, $childMap);
}
return [
'comment' => $byId[$id],
'depth' => $depth,
'children' => $children,
];
}
protected function loadComments(): array
{
if (!$this->enabled()) {
return [];
}
return $this->page->comments()
->with('createdBy')
->get()
->all();
}
}

View File

@@ -2,7 +2,7 @@
namespace BookStack\Api;
use BookStack\Http\ApiController;
use BookStack\Http\Controllers\Api\ApiController;
use Exception;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Support\Collection;
@@ -16,8 +16,8 @@ use ReflectionMethod;
class ApiDocsGenerator
{
protected array $reflectionClasses = [];
protected array $controllerClasses = [];
protected $reflectionClasses = [];
protected $controllerClasses = [];
/**
* Load the docs form the cache if existing
@@ -139,10 +139,9 @@ class ApiDocsGenerator
protected function parseDescriptionFromMethodComment(string $comment): string
{
$matches = [];
preg_match_all('/^\s*?\*\s?($|((?![\/@\s]).*?))$/m', $comment, $matches);
preg_match_all('/^\s*?\*\s((?![@\s]).*?)$/m', $comment, $matches);
$text = implode(' ', $matches[1] ?? []);
return str_replace(' ', "\n", $text);
return implode(' ', $matches[1] ?? []);
}
/**

View File

@@ -2,9 +2,8 @@
namespace BookStack\Api;
use BookStack\Activity\Models\Loggable;
use BookStack\Users\Models\User;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use BookStack\Auth\User;
use BookStack\Interfaces\Loggable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Carbon;
@@ -21,8 +20,6 @@ use Illuminate\Support\Carbon;
*/
class ApiToken extends Model implements Loggable
{
use HasFactory;
protected $fillable = ['name', 'expires_at'];
protected $casts = [
'expires_at' => 'date:Y-m-d',

View File

@@ -2,7 +2,7 @@
namespace BookStack\Api;
use BookStack\Access\LoginService;
use BookStack\Auth\Access\LoginService;
use BookStack\Exceptions\ApiAuthException;
use Illuminate\Auth\GuardHelpers;
use Illuminate\Contracts\Auth\Authenticatable;

View File

@@ -1,53 +0,0 @@
<?php
namespace BookStack\App\Providers;
use BookStack\Translation\FileLoader;
use BookStack\Translation\MessageSelector;
use Illuminate\Translation\TranslationServiceProvider as BaseProvider;
use Illuminate\Translation\Translator;
class TranslationServiceProvider extends BaseProvider
{
/**
* Register the service provider.
*
* @return void
*/
public function register()
{
$this->registerLoader();
// This is a tweak upon Laravel's based translation service registration to allow
// usage of a custom MessageSelector class
$this->app->singleton('translator', function ($app) {
$loader = $app['translation.loader'];
// When registering the translator component, we'll need to set the default
// locale as well as the fallback locale. So, we'll grab the application
// configuration so we can easily get both of these values from there.
$locale = $app['config']['app.locale'];
$trans = new Translator($loader, $locale);
$trans->setFallback($app['config']['app.fallback_locale']);
$trans->setSelector(new MessageSelector());
return $trans;
});
}
/**
* Register the translation line loader.
* Overrides the default register action from Laravel so a custom loader can be used.
*
* @return void
*/
protected function registerLoader()
{
$this->app->singleton('translation.loader', function ($app) {
return new FileLoader($app['files'], $app['path.lang']);
});
}
}

View File

@@ -1,6 +1,6 @@
<?php
namespace BookStack\App;
namespace BookStack;
class Application extends \Illuminate\Foundation\Application
{

View File

@@ -1,15 +1,15 @@
<?php
namespace BookStack\Access;
namespace BookStack\Auth\Access;
use BookStack\Auth\User;
use BookStack\Exceptions\ConfirmationEmailException;
use BookStack\Notifications\ConfirmEmail;
use BookStack\Users\Models\User;
class EmailConfirmationService extends UserTokenService
{
protected string $tokenTable = 'email_confirmations';
protected int $expiryTime = 24;
protected $tokenTable = 'email_confirmations';
protected $expiryTime = 24;
/**
* Create new confirmation for a user,

View File

@@ -1,6 +1,6 @@
<?php
namespace BookStack\Access;
namespace BookStack\Auth\Access;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\UserProvider;

View File

@@ -1,9 +1,9 @@
<?php
namespace BookStack\Access;
namespace BookStack\Auth\Access;
use BookStack\Users\Models\Role;
use BookStack\Users\Models\User;
use BookStack\Auth\Role;
use BookStack\Auth\User;
use Illuminate\Support\Collection;
class GroupSyncService

View File

@@ -1,6 +1,6 @@
<?php
namespace BookStack\Access\Guards;
namespace BookStack\Auth\Access\Guards;
/**
* Saml2 Session Guard.

View File

@@ -1,8 +1,8 @@
<?php
namespace BookStack\Access\Guards;
namespace BookStack\Auth\Access\Guards;
use BookStack\Access\RegistrationService;
use BookStack\Auth\Access\RegistrationService;
use Illuminate\Auth\GuardHelpers;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\StatefulGuard;

View File

@@ -1,15 +1,15 @@
<?php
namespace BookStack\Access\Guards;
namespace BookStack\Auth\Access\Guards;
use BookStack\Access\LdapService;
use BookStack\Access\RegistrationService;
use BookStack\Auth\Access\LdapService;
use BookStack\Auth\Access\RegistrationService;
use BookStack\Auth\User;
use BookStack\Exceptions\JsonDebugException;
use BookStack\Exceptions\LdapException;
use BookStack\Exceptions\LoginAttemptEmailNeededException;
use BookStack\Exceptions\LoginAttemptException;
use BookStack\Exceptions\UserRegistrationException;
use BookStack\Users\Models\User;
use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Contracts\Session\Session;
use Illuminate\Support\Str;

136
app/Auth/Access/Ldap.php Normal file
View File

@@ -0,0 +1,136 @@
<?php
namespace BookStack\Auth\Access;
/**
* Class Ldap
* An object-orientated thin abstraction wrapper for common PHP LDAP functions.
* Allows the standard LDAP functions to be mocked for testing.
*/
class Ldap
{
/**
* Connect to an LDAP server.
*
* @return resource
*/
public function connect(string $hostName, int $port)
{
return ldap_connect($hostName, $port);
}
/**
* Set the value of a LDAP option for the given connection.
*
* @param resource $ldapConnection
* @param mixed $value
*/
public function setOption($ldapConnection, int $option, $value): bool
{
return ldap_set_option($ldapConnection, $option, $value);
}
/**
* Start TLS on the given LDAP connection.
*/
public function startTls($ldapConnection): bool
{
return ldap_start_tls($ldapConnection);
}
/**
* Set the version number for the given ldap connection.
*
* @param resource $ldapConnection
*/
public function setVersion($ldapConnection, int $version): bool
{
return $this->setOption($ldapConnection, LDAP_OPT_PROTOCOL_VERSION, $version);
}
/**
* Search LDAP tree using the provided filter.
*
* @param resource $ldapConnection
* @param string $baseDn
* @param string $filter
* @param array|null $attributes
*
* @return resource
*/
public function search($ldapConnection, $baseDn, $filter, array $attributes = null)
{
return ldap_search($ldapConnection, $baseDn, $filter, $attributes);
}
/**
* Get entries from an ldap search result.
*
* @param resource $ldapConnection
* @param resource $ldapSearchResult
*
* @return array
*/
public function getEntries($ldapConnection, $ldapSearchResult)
{
return ldap_get_entries($ldapConnection, $ldapSearchResult);
}
/**
* Search and get entries immediately.
*
* @param resource $ldapConnection
* @param string $baseDn
* @param string $filter
* @param array|null $attributes
*
* @return resource
*/
public function searchAndGetEntries($ldapConnection, $baseDn, $filter, array $attributes = null)
{
$search = $this->search($ldapConnection, $baseDn, $filter, $attributes);
return $this->getEntries($ldapConnection, $search);
}
/**
* Bind to LDAP directory.
*
* @param resource $ldapConnection
* @param string $bindRdn
* @param string $bindPassword
*
* @return bool
*/
public function bind($ldapConnection, $bindRdn = null, $bindPassword = null)
{
return ldap_bind($ldapConnection, $bindRdn, $bindPassword);
}
/**
* Explode a LDAP dn string into an array of components.
*
* @param string $dn
* @param int $withAttrib
*
* @return array
*/
public function explodeDn(string $dn, int $withAttrib)
{
return ldap_explode_dn($dn, $withAttrib);
}
/**
* Escape a string for use in an LDAP filter.
*
* @param string $value
* @param string $ignore
* @param int $flags
*
* @return string
*/
public function escape(string $value, string $ignore = '', int $flags = 0)
{
return ldap_escape($value, $ignore, $flags);
}
}

View File

@@ -1,11 +1,11 @@
<?php
namespace BookStack\Access;
namespace BookStack\Auth\Access;
use BookStack\Auth\User;
use BookStack\Exceptions\JsonDebugException;
use BookStack\Exceptions\LdapException;
use BookStack\Uploads\UserAvatars;
use BookStack\Users\Models\User;
use ErrorException;
use Illuminate\Support\Facades\Log;
@@ -15,19 +15,26 @@ use Illuminate\Support\Facades\Log;
*/
class LdapService
{
protected Ldap $ldap;
protected GroupSyncService $groupSyncService;
protected UserAvatars $userAvatars;
/**
* @var resource|\LDAP\Connection
* @var resource
*/
protected $ldapConnection;
protected array $config;
protected bool $enabled;
public function __construct(
protected Ldap $ldap,
protected UserAvatars $userAvatars,
protected GroupSyncService $groupSyncService
) {
/**
* LdapService constructor.
*/
public function __construct(Ldap $ldap, UserAvatars $userAvatars, GroupSyncService $groupSyncService)
{
$this->ldap = $ldap;
$this->userAvatars = $userAvatars;
$this->groupSyncService = $groupSyncService;
$this->config = config('services.ldap');
$this->enabled = config('auth.method') === 'ldap';
}
@@ -52,7 +59,7 @@ class LdapService
// Clean attributes
foreach ($attributes as $index => $attribute) {
if (str_starts_with($attribute, 'BIN;')) {
if (strpos($attribute, 'BIN;') === 0) {
$attributes[$index] = substr($attribute, strlen('BIN;'));
}
}
@@ -75,7 +82,7 @@ class LdapService
* Get the details of a user from LDAP using the given username.
* User found via configurable user filter.
*
* @throws LdapException|JsonDebugException
* @throws LdapException
*/
public function getUserDetails(string $userName): ?array
{
@@ -119,7 +126,7 @@ class LdapService
*/
protected function getUserResponseProperty(array $userDetails, string $propertyKey, $defaultValue)
{
$isBinary = str_starts_with($propertyKey, 'BIN;');
$isBinary = strpos($propertyKey, 'BIN;') === 0;
$propertyKey = strtolower($propertyKey);
$value = $defaultValue;
@@ -163,11 +170,11 @@ class LdapService
* Bind the system user to the LDAP connection using the given credentials
* otherwise anonymous access is attempted.
*
* @param resource|\LDAP\Connection $connection
* @param resource $connection
*
* @throws LdapException
*/
protected function bindSystemUser($connection): void
protected function bindSystemUser($connection)
{
$ldapDn = $this->config['dn'];
$ldapPass = $this->config['pass'];
@@ -190,7 +197,7 @@ class LdapService
*
* @throws LdapException
*
* @return resource|\LDAP\Connection
* @return resource
*/
protected function getConnection()
{
@@ -209,8 +216,8 @@ class LdapService
$this->ldap->setOption(null, LDAP_OPT_X_TLS_REQUIRE_CERT, LDAP_OPT_X_TLS_NEVER);
}
$ldapHost = $this->parseServerString($this->config['server']);
$ldapConnection = $this->ldap->connect($ldapHost);
$serverDetails = $this->parseServerString($this->config['server']);
$ldapConnection = $this->ldap->connect($serverDetails['host'], $serverDetails['port']);
if ($ldapConnection === false) {
throw new LdapException(trans('errors.ldap_cannot_connect'));
@@ -235,16 +242,23 @@ class LdapService
}
/**
* Parse an LDAP server string and return the host suitable for a connection.
* Parse a LDAP server string and return the host and port for a connection.
* Is flexible to formats such as 'ldap.example.com:8069' or 'ldaps://ldap.example.com'.
*/
protected function parseServerString(string $serverString): string
protected function parseServerString(string $serverString): array
{
if (str_starts_with($serverString, 'ldaps://') || str_starts_with($serverString, 'ldap://')) {
return $serverString;
$serverNameParts = explode(':', $serverString);
// If we have a protocol just return the full string since PHP will ignore a separate port.
if ($serverNameParts[0] === 'ldaps' || $serverNameParts[0] === 'ldap') {
return ['host' => $serverString, 'port' => 389];
}
return "ldap://{$serverString}";
// Otherwise, extract the port out
$hostName = $serverNameParts[0];
$ldapPort = (count($serverNameParts) > 1) ? intval($serverNameParts[1]) : 389;
return ['host' => $hostName, 'port' => $ldapPort];
}
/**
@@ -372,7 +386,7 @@ class LdapService
* @throws LdapException
* @throws JsonDebugException
*/
public function syncGroups(User $user, string $username): void
public function syncGroups(User $user, string $username)
{
$userLdapGroups = $this->getUserGroups($username);
$this->groupSyncService->syncUserWithFoundGroups($user, $userLdapGroups, $this->config['remove_from_groups']);

View File

@@ -1,15 +1,15 @@
<?php
namespace BookStack\Access;
namespace BookStack\Auth\Access;
use BookStack\Access\Mfa\MfaSession;
use BookStack\Activity\ActivityType;
use BookStack\Actions\ActivityType;
use BookStack\Auth\Access\Mfa\MfaSession;
use BookStack\Auth\User;
use BookStack\Exceptions\LoginAttemptException;
use BookStack\Exceptions\StoppedAuthenticationException;
use BookStack\Facades\Activity;
use BookStack\Facades\Theme;
use BookStack\Theming\ThemeEvents;
use BookStack\Users\Models\User;
use Exception;
class LoginService

View File

@@ -1,6 +1,6 @@
<?php
namespace BookStack\Access\Mfa;
namespace BookStack\Auth\Access\Mfa;
use Illuminate\Support\Str;

View File

@@ -1,8 +1,8 @@
<?php
namespace BookStack\Access\Mfa;
namespace BookStack\Auth\Access\Mfa;
use BookStack\Users\Models\User;
use BookStack\Auth\User;
class MfaSession
{

View File

@@ -1,8 +1,8 @@
<?php
namespace BookStack\Access\Mfa;
namespace BookStack\Auth\Access\Mfa;
use BookStack\Users\Models\User;
use BookStack\Auth\User;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;

View File

@@ -1,6 +1,6 @@
<?php
namespace BookStack\Access\Mfa;
namespace BookStack\Auth\Access\Mfa;
use BaconQrCode\Renderer\Color\Rgb;
use BaconQrCode\Renderer\Image\SvgImageBackEnd;
@@ -8,7 +8,7 @@ use BaconQrCode\Renderer\ImageRenderer;
use BaconQrCode\Renderer\RendererStyle\Fill;
use BaconQrCode\Renderer\RendererStyle\RendererStyle;
use BaconQrCode\Writer;
use BookStack\Users\Models\User;
use BookStack\Auth\User;
use PragmaRX\Google2FA\Google2FA;
use PragmaRX\Google2FA\Support\Constants;

View File

@@ -1,6 +1,6 @@
<?php
namespace BookStack\Access\Mfa;
namespace BookStack\Auth\Access\Mfa;
use Illuminate\Contracts\Validation\Rule;

View File

@@ -1,6 +1,6 @@
<?php
namespace BookStack\Access\Oidc;
namespace BookStack\Auth\Access\Oidc;
use InvalidArgumentException;
use League\OAuth2\Client\Token\AccessToken;

View File

@@ -1,6 +1,6 @@
<?php
namespace BookStack\Access\Oidc;
namespace BookStack\Auth\Access\Oidc;
use Exception;

View File

@@ -1,19 +1,38 @@
<?php
namespace BookStack\Access\Oidc;
namespace BookStack\Auth\Access\Oidc;
class OidcIdToken
{
protected array $header;
protected array $payload;
protected string $signature;
protected string $issuer;
protected array $tokenParts = [];
/**
* @var array
*/
protected $header;
/**
* @var array
*/
protected $payload;
/**
* @var string
*/
protected $signature;
/**
* @var array[]|string[]
*/
protected array $keys;
protected $keys;
/**
* @var string
*/
protected $issuer;
/**
* @var array
*/
protected $tokenParts = [];
public function __construct(string $token, string $issuer, array $keys)
{
@@ -87,14 +106,6 @@ 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.

View File

@@ -1,6 +1,6 @@
<?php
namespace BookStack\Access\Oidc;
namespace BookStack\Auth\Access\Oidc;
class OidcInvalidKeyException extends \Exception
{

View File

@@ -1,6 +1,6 @@
<?php
namespace BookStack\Access\Oidc;
namespace BookStack\Auth\Access\Oidc;
use Exception;

View File

@@ -1,6 +1,6 @@
<?php
namespace BookStack\Access\Oidc;
namespace BookStack\Auth\Access\Oidc;
use Exception;

View File

@@ -1,6 +1,6 @@
<?php
namespace BookStack\Access\Oidc;
namespace BookStack\Auth\Access\Oidc;
use phpseclib3\Crypt\Common\PublicKey;
use phpseclib3\Crypt\PublicKeyLoader;

View File

@@ -1,6 +1,6 @@
<?php
namespace BookStack\Access\Oidc;
namespace BookStack\Auth\Access\Oidc;
use League\OAuth2\Client\Grant\AbstractGrant;
use League\OAuth2\Client\Provider\AbstractProvider;

View File

@@ -1,6 +1,6 @@
<?php
namespace BookStack\Access\Oidc;
namespace BookStack\Auth\Access\Oidc;
use GuzzleHttp\Psr7\Request;
use Illuminate\Contracts\Cache\Repository;

View File

@@ -1,16 +1,14 @@
<?php
namespace BookStack\Access\Oidc;
namespace BookStack\Auth\Access\Oidc;
use BookStack\Access\GroupSyncService;
use BookStack\Access\LoginService;
use BookStack\Access\RegistrationService;
use BookStack\Auth\Access\GroupSyncService;
use BookStack\Auth\Access\LoginService;
use BookStack\Auth\Access\RegistrationService;
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 BookStack\Users\Models\User;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Cache;
use League\OAuth2\Client\OptionProvider\HttpBasicAuthOptionProvider;
@@ -23,12 +21,24 @@ 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(
protected RegistrationService $registrationService,
protected LoginService $loginService,
protected HttpClient $httpClient,
protected GroupSyncService $groupService
RegistrationService $registrationService,
LoginService $loginService,
HttpClient $httpClient,
GroupSyncService $groupService
) {
$this->registrationService = $registrationService;
$this->loginService = $loginService;
$this->httpClient = $httpClient;
$this->groupService = $groupService;
}
/**
@@ -188,8 +198,7 @@ class OidcService
*/
protected function getUserDetails(OidcIdToken $token): array
{
$idClaim = $this->config()['external_id_claim'];
$id = $token->getClaim($idClaim);
$id = $token->getClaim('sub');
return [
'external_id' => $id,
@@ -216,16 +225,6 @@ 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());
}

View File

@@ -1,14 +1,15 @@
<?php
namespace BookStack\Access;
namespace BookStack\Auth\Access;
use BookStack\Activity\ActivityType;
use BookStack\Actions\ActivityType;
use BookStack\Auth\SocialAccount;
use BookStack\Auth\User;
use BookStack\Auth\UserRepo;
use BookStack\Exceptions\UserRegistrationException;
use BookStack\Facades\Activity;
use BookStack\Facades\Theme;
use BookStack\Theming\ThemeEvents;
use BookStack\Users\Models\User;
use BookStack\Users\UserRepo;
use Exception;
use Illuminate\Support\Str;

View File

@@ -1,12 +1,12 @@
<?php
namespace BookStack\Access;
namespace BookStack\Auth\Access;
use BookStack\Auth\User;
use BookStack\Exceptions\JsonDebugException;
use BookStack\Exceptions\SamlException;
use BookStack\Exceptions\StoppedAuthenticationException;
use BookStack\Exceptions\UserRegistrationException;
use BookStack\Users\Models\User;
use Exception;
use OneLogin\Saml2\Auth;
use OneLogin\Saml2\Constants;
@@ -67,7 +67,7 @@ class Saml2Service
$returnRoute,
[],
$user->email,
session()->get('saml2_session_index'),
null,
true,
Constants::NAMEID_EMAIL_ADDRESS
);
@@ -118,7 +118,6 @@ class Saml2Service
$attrs = $toolkit->getAttributes();
$id = $toolkit->getNameId();
session()->put('saml2_session_index', $toolkit->getSessionIndex());
return $this->processLoginCallback($id, $attrs);
}

View File

@@ -1,12 +1,12 @@
<?php
namespace BookStack\Access;
namespace BookStack\Auth\Access;
use BookStack\Auth\Access\handler;
use BookStack\Auth\SocialAccount;
use BookStack\Auth\User;
use BookStack\Exceptions\SocialDriverNotConfigured;
use BookStack\Exceptions\SocialSignInAccountNotUsed;
use BookStack\Exceptions\UserRegistrationException;
use BookStack\Users\Models\User;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Str;
use Laravel\Socialite\Contracts\Factory as Socialite;

View File

@@ -1,18 +1,20 @@
<?php
namespace BookStack\Access;
namespace BookStack\Auth\Access;
use BookStack\Auth\User;
use BookStack\Notifications\UserInvite;
use BookStack\Users\Models\User;
class UserInviteService extends UserTokenService
{
protected string $tokenTable = 'user_invites';
protected int $expiryTime = 336; // Two weeks
protected $tokenTable = 'user_invites';
protected $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)
{

View File

@@ -1,10 +1,10 @@
<?php
namespace BookStack\Access;
namespace BookStack\Auth\Access;
use BookStack\Auth\User;
use BookStack\Exceptions\UserTokenExpiredException;
use BookStack\Exceptions\UserTokenNotFoundException;
use BookStack\Users\Models\User;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
@@ -14,29 +14,41 @@ class UserTokenService
{
/**
* Name of table where user tokens are stored.
*
* @var string
*/
protected string $tokenTable = 'user_tokens';
protected $tokenTable = 'user_tokens';
/**
* Token expiry time in hours.
*
* @var int
*/
protected int $expiryTime = 24;
protected $expiryTime = 24;
/**
* Delete all tokens that belong to a user.
* Delete all email confirmations that belong to a user.
*
* @param User $user
*
* @return mixed
*/
public function deleteByUser(User $user): void
public function deleteByUser(User $user)
{
DB::table($this->tokenTable)
return DB::table($this->tokenTable)
->where('user_id', '=', $user->id)
->delete();
}
/**
* Get the user id from a token, while checking the token exists and has not expired.
* Get the user id from a token, while check the token exists and has not expired.
*
* @param string $token
*
* @throws UserTokenNotFoundException
* @throws UserTokenExpiredException
*
* @return int
*/
public function checkTokenAndGetUserId(string $token): int
{
@@ -55,6 +67,8 @@ class UserTokenService
/**
* Creates a unique token within the email confirmation database.
*
* @return string
*/
protected function generateToken(): string
{
@@ -68,6 +82,10 @@ class UserTokenService
/**
* Generate and store a token for the given user.
*
* @param User $user
*
* @return string
*/
protected function createTokenForUser(User $user): string
{
@@ -84,6 +102,10 @@ class UserTokenService
/**
* Check if the given token exists.
*
* @param string $token
*
* @return bool
*/
protected function tokenExists(string $token): bool
{
@@ -93,8 +115,12 @@ class UserTokenService
/**
* Get a token entry for the given token.
*
* @param string $token
*
* @return object|null
*/
protected function getEntryByToken(string $token): ?stdClass
protected function getEntryByToken(string $token)
{
return DB::table($this->tokenTable)
->where('token', '=', $token)
@@ -103,6 +129,10 @@ class UserTokenService
/**
* Check if the given token entry has expired.
*
* @param stdClass $tokenEntry
*
* @return bool
*/
protected function entryExpired(stdClass $tokenEntry): bool
{

View File

@@ -0,0 +1,18 @@
<?php
namespace BookStack\Auth\Permissions;
use BookStack\Model;
/**
* @property int $id
* @property ?int $role_id
* @property ?int $user_id
* @property string $entity_type
* @property int $entity_id
* @property bool $view
*/
class CollapsedPermission extends Model
{
protected $table = 'entity_permissions_collapsed';
}

View File

@@ -0,0 +1,278 @@
<?php
namespace BookStack\Auth\Permissions;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\BookChild;
use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\Page;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Support\Facades\DB;
/**
* Collapsed permissions act as a "flattened" view of entity-level permissions in the system
* so inheritance does not have to managed as part of permission querying.
*/
class CollapsedPermissionBuilder
{
/**
* Re-generate all collapsed permissions from scratch.
*/
public function rebuildForAll()
{
DB::table('entity_permissions_collapsed')->truncate();
// Chunk through all books
$this->bookFetchQuery()->chunk(5, function (EloquentCollection $books) {
$this->buildForBooks($books, false);
});
// Chunk through all bookshelves
Bookshelf::query()->withTrashed()
->select(['id'])
->chunk(50, function (EloquentCollection $shelves) {
$this->generateCollapsedPermissions($shelves->all());
});
}
/**
* Rebuild the collapsed permissions for a particular entity.
*/
public function rebuildForEntity(Entity $entity)
{
$entities = [$entity];
if ($entity instanceof Book) {
$books = $this->bookFetchQuery()->where('id', '=', $entity->id)->get();
$this->buildForBooks($books, true);
return;
}
/** @var BookChild $entity */
if ($entity->book) {
$entities[] = $entity->book;
}
if ($entity instanceof Page && $entity->chapter_id) {
$entities[] = $entity->chapter;
}
if ($entity instanceof Chapter) {
foreach ($entity->pages as $page) {
$entities[] = $page;
}
}
$this->buildForEntities($entities);
}
/**
* Get a query for fetching a book with its children.
*/
protected function bookFetchQuery(): Builder
{
return Book::query()->withTrashed()
->select(['id'])->with([
'chapters' => function ($query) {
$query->withTrashed()->select(['id', 'book_id']);
},
'pages' => function ($query) {
$query->withTrashed()->select(['id', 'book_id', 'chapter_id']);
},
]);
}
/**
* Build collapsed permissions for the given books.
*/
protected function buildForBooks(EloquentCollection $books, bool $deleteOld)
{
$entities = clone $books;
/** @var Book $book */
foreach ($books->all() as $book) {
foreach ($book->getRelation('chapters') as $chapter) {
$entities->push($chapter);
}
foreach ($book->getRelation('pages') as $page) {
$entities->push($page);
}
}
if ($deleteOld) {
$this->deleteForEntities($entities->all());
}
$this->generateCollapsedPermissions($entities->all());
}
/**
* Rebuild the collapsed permissions for a collection of entities.
*/
protected function buildForEntities(array $entities)
{
$this->deleteForEntities($entities);
$this->generateCollapsedPermissions($entities);
}
/**
* Delete the stored collapsed permissions for a list of entities.
*
* @param Entity[] $entities
*/
protected function deleteForEntities(array $entities)
{
$simpleEntities = $this->entitiesToSimpleEntities($entities);
$idsByType = $this->entitiesToTypeIdMap($simpleEntities);
DB::transaction(function () use ($idsByType) {
foreach ($idsByType as $type => $ids) {
foreach (array_chunk($ids, 1000) as $idChunk) {
DB::table('entity_permissions_collapsed')
->where('entity_type', '=', $type)
->whereIn('entity_id', $idChunk)
->delete();
}
}
});
}
/**
* Convert the given list of entities into "SimpleEntityData" representations
* for faster usage and property access.
*
* @param Entity[] $entities
*
* @return SimpleEntityData[]
*/
protected function entitiesToSimpleEntities(array $entities): array
{
$simpleEntities = [];
foreach ($entities as $entity) {
$attrs = $entity->getAttributes();
$simple = new SimpleEntityData();
$simple->id = $attrs['id'];
$simple->type = $entity->getMorphClass();
$simple->book_id = $attrs['book_id'] ?? null;
$simple->chapter_id = $attrs['chapter_id'] ?? null;
$simpleEntities[] = $simple;
}
return $simpleEntities;
}
/**
* Create & Save collapsed entity permissions.
*
* @param Entity[] $originalEntities
*/
protected function generateCollapsedPermissions(array $originalEntities)
{
$entities = $this->entitiesToSimpleEntities($originalEntities);
$collapsedPermData = [];
// Fetch related entity permissions
$permissions = $this->getEntityPermissionsForEntities($entities);
// Create a mapping of explicit entity permissions
$permissionMap = new EntityPermissionMap($permissions);
// Create Joint Permission Data
foreach ($entities as $entity) {
array_push($collapsedPermData, ...$this->createCollapsedPermissionData($entity, $permissionMap));
}
DB::transaction(function () use ($collapsedPermData) {
foreach (array_chunk($collapsedPermData, 1000) as $dataChunk) {
DB::table('entity_permissions_collapsed')->insert($dataChunk);
}
});
}
/**
* Create collapsed permission data for the given entity using the given permission map.
*/
protected function createCollapsedPermissionData(SimpleEntityData $entity, EntityPermissionMap $permissionMap): array
{
$chain = [
$entity->type . ':' . $entity->id,
$entity->chapter_id ? ('chapter:' . $entity->chapter_id) : null,
$entity->book_id ? ('book:' . $entity->book_id) : null,
];
$permissionData = [];
$overridesApplied = [];
foreach ($chain as $entityTypeId) {
if ($entityTypeId === null) {
continue;
}
$permissions = $permissionMap->getForEntity($entityTypeId);
foreach ($permissions as $permission) {
$related = $permission->getAssignedType() . ':' . $permission->getAssignedTypeId();
if (!isset($overridesApplied[$related])) {
$permissionData[] = [
'role_id' => $permission->role_id,
'user_id' => $permission->user_id,
'view' => $permission->view,
'entity_type' => $entity->type,
'entity_id' => $entity->id,
];
$overridesApplied[$related] = true;
}
}
}
return $permissionData;
}
/**
* From the given entity list, provide back a mapping of entity types to
* the ids of that given type. The type used is the DB morph class.
*
* @param SimpleEntityData[] $entities
*
* @return array<string, int[]>
*/
protected function entitiesToTypeIdMap(array $entities): array
{
$idsByType = [];
foreach ($entities as $entity) {
if (!isset($idsByType[$entity->type])) {
$idsByType[$entity->type] = [];
}
$idsByType[$entity->type][] = $entity->id;
}
return $idsByType;
}
/**
* Get the entity permissions for all the given entities.
*
* @param SimpleEntityData[] $entities
*
* @return EntityPermission[]
*/
protected function getEntityPermissionsForEntities(array $entities): array
{
$idsByType = $this->entitiesToTypeIdMap($entities);
$permissionFetch = EntityPermission::query()
->where(function (Builder $query) use ($idsByType) {
foreach ($idsByType as $type => $ids) {
$query->orWhere(function (Builder $query) use ($type, $ids) {
$query->where('entity_type', '=', $type)->whereIn('entity_id', $ids);
});
}
});
return $permissionFetch->get()->all();
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace BookStack\Auth\Permissions;
use BookStack\Auth\Role;
use BookStack\Auth\User;
use BookStack\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* @property int $id
* @property int $role_id
* @property int $user_id
* @property int $entity_id
* @property string $entity_type
* @property boolean $view
* @property boolean $create
* @property boolean $update
* @property boolean $delete
*/
class EntityPermission extends Model
{
public const PERMISSIONS = ['view', 'create', 'update', 'delete'];
protected $fillable = ['role_id', 'user_id', 'view', 'create', 'update', 'delete'];
public $timestamps = false;
/**
* Get the role assigned to this entity permission.
*/
public function role(): BelongsTo
{
return $this->belongsTo(Role::class);
}
/**
* Get the user assigned to this entity permission.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
/**
* Get the type of entity permission this is.
* Will be one of: user, role, fallback
*/
public function getAssignedType(): string
{
if ($this->user_id) {
return 'user';
}
if ($this->role_id) {
return 'role';
}
return 'fallback';
}
/**
* Get the ID for the assigned type of permission.
* (Role/User ID). Defaults to 0 for fallback.
*/
public function getAssignedTypeId(): int
{
return $this->user_id ?? $this->role_id ?? 0;
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace BookStack\Auth\Permissions;
class EntityPermissionMap
{
protected array $map = [];
/**
* @param EntityPermission[] $permissions
*/
public function __construct(array $permissions = [])
{
foreach ($permissions as $entityPermission) {
$this->addPermission($entityPermission);
}
}
protected function addPermission(EntityPermission $permission)
{
$entityCombinedId = $permission->entity_type . ':' . $permission->entity_id;
if (!isset($this->map[$entityCombinedId])) {
$this->map[$entityCombinedId] = [];
}
$this->map[$entityCombinedId][] = $permission;
}
/**
* @return EntityPermission[]
*/
public function getForEntity(string $typeIdString): array
{
return $this->map[$typeIdString] ?? [];
}
}

View File

@@ -0,0 +1,389 @@
<?php
namespace BookStack\Auth\Permissions;
use BookStack\Auth\Role;
use BookStack\Auth\User;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\Page;
use BookStack\Model;
use BookStack\Traits\HasCreatorAndUpdater;
use BookStack\Traits\HasOwner;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Query\Builder as QueryBuilder;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Facades\DB;
use InvalidArgumentException;
class PermissionApplicator
{
/**
* Checks if an entity has a restriction set upon it.
*
* @param HasCreatorAndUpdater|HasOwner $ownable
*/
public function checkOwnableUserAccess(Model $ownable, string $permission): bool
{
$explodedPermission = explode('-', $permission);
$action = $explodedPermission[1] ?? $explodedPermission[0];
$fullPermission = count($explodedPermission) > 1 ? $permission : $ownable->getMorphClass() . '-' . $permission;
$user = $this->currentUser();
$userRoleIds = $this->getCurrentUserRoleIds();
$allRolePermission = $user->can($fullPermission . '-all');
$ownRolePermission = $user->can($fullPermission . '-own');
$nonJointPermissions = ['restrictions', 'image', 'attachment', 'comment'];
$ownerField = ($ownable instanceof Entity) ? 'owned_by' : 'created_by';
$ownableFieldVal = $ownable->getAttribute($ownerField);
if (is_null($ownableFieldVal)) {
throw new InvalidArgumentException("{$ownerField} field used but has not been loaded");
}
$isOwner = $user->id === $ownableFieldVal;
$hasRolePermission = $allRolePermission || ($isOwner && $ownRolePermission);
// Handle non entity specific jointPermissions
if (in_array($explodedPermission[0], $nonJointPermissions)) {
return $hasRolePermission;
}
$hasApplicableEntityPermissions = $this->hasEntityPermission($ownable, $userRoleIds, $user->id, $action);
return is_null($hasApplicableEntityPermissions) ? $hasRolePermission : $hasApplicableEntityPermissions;
}
/**
* Check if there are permissions that are applicable for the given entity item, action and roles.
* Returns null when no entity permissions are in force.
*/
protected function hasEntityPermission(Entity $entity, array $userRoleIds, int $userId, string $action): ?bool
{
$this->ensureValidEntityAction($action);
$adminRoleId = Role::getSystemRole('admin')->id;
if (in_array($adminRoleId, $userRoleIds)) {
return true;
}
// The array order here is very important due to the fact we walk up the chain
// in the flattening loop below. Earlier items in the chain have higher priority.
$typeIdList = [$entity->getMorphClass() . ':' . $entity->id];
if ($entity instanceof Page && $entity->chapter_id) {
$typeIdList[] = 'chapter:' . $entity->chapter_id;
}
if ($entity instanceof Page || $entity instanceof Chapter) {
$typeIdList[] = 'book:' . $entity->book_id;
}
$relevantPermissions = EntityPermission::query()
->where(function (Builder $query) use ($typeIdList) {
foreach ($typeIdList as $typeId) {
$query->orWhere(function (Builder $query) use ($typeId) {
[$type, $id] = explode(':', $typeId);
$query->where('entity_type', '=', $type)
->where('entity_id', '=', $id);
});
}
})->where(function (Builder $query) use ($userRoleIds, $userId) {
$query->whereIn('role_id', $userRoleIds)
->orWhere('user_id', '=', $userId)
->orWhere(function (Builder $query) {
$query->whereNull(['role_id', 'user_id']);
});
})->get(['entity_id', 'entity_type', 'role_id', 'user_id', $action])
->all();
$permissionMap = new EntityPermissionMap($relevantPermissions);
$permitsByType = ['user' => [], 'fallback' => [], 'role' => []];
// Collapse and simplify permission structure
foreach ($typeIdList as $typeId) {
$permissions = $permissionMap->getForEntity($typeId);
foreach ($permissions as $permission) {
$related = $permission->getAssignedType();
$relatedId = $permission->getAssignedTypeId();
if (!isset($permitsByType[$related][$relatedId])) {
$permitsByType[$related][$relatedId] = $permission->$action;
}
}
}
// Return user-level permission if exists
if (count($permitsByType['user']) > 0) {
return boolval(array_values($permitsByType['user'])[0]);
}
// Return grant or reject from role-level if exists
if (count($permitsByType['role']) > 0) {
return boolval(max($permitsByType['role']));
}
// Return fallback permission if exists
if (count($permitsByType['fallback']) > 0) {
return boolval($permitsByType['fallback'][0]);
}
return null;
}
/**
* Checks if a user has the given permission for any items in the system.
* Can be passed an entity instance to filter on a specific type.
*/
public function checkUserHasEntityPermissionOnAny(string $action, string $entityClass = ''): bool
{
$this->ensureValidEntityAction($action);
$permissionQuery = EntityPermission::query()
->where($action, '=', true)
->where(function (Builder $query) {
$query->whereIn('role_id', $this->getCurrentUserRoleIds())
->orWhere('user_id', '=', $this->currentUser()->id);
});
if (!empty($entityClass)) {
/** @var Entity $entityInstance */
$entityInstance = app()->make($entityClass);
$permissionQuery = $permissionQuery->where('entity_type', '=', $entityInstance->getMorphClass());
}
$hasPermission = $permissionQuery->count() > 0;
return $hasPermission;
}
/**
* Limit the given entity query so that the query will only
* return items that the user has view permission for.
*/
public function restrictEntityQuery(Builder $query, string $morphClass): Builder
{
$this->applyPermissionsToQuery($query, $query->getModel()->getTable(), $morphClass, 'id', '');
return $query;
}
/**
* @param Builder|QueryBuilder $query
*/
protected function applyPermissionsToQuery($query, string $queryTable, string $entityTypeLimiter, string $entityIdColumn, string $entityTypeColumn): void
{
if ($this->currentUser()->hasSystemRole('admin')) {
return;
}
$this->applyFallbackJoin($query, $queryTable, $entityTypeLimiter, $entityIdColumn, $entityTypeColumn);
$this->applyRoleJoin($query, $queryTable, $entityTypeLimiter, $entityIdColumn, $entityTypeColumn);
$this->applyUserJoin($query, $queryTable, $entityTypeLimiter, $entityIdColumn, $entityTypeColumn);
$this->applyPermissionWhereFilter($query, $queryTable, $entityTypeLimiter, $entityTypeColumn);
}
/**
* Apply the where condition to a permission restricting query, to limit based upon the values of the joined
* permission data. Query must have joins pre-applied.
* Either entityTypeLimiter or entityTypeColumn should be supplied, with the other empty.
* Both should not be applied since that would conflict upon intent.
* @param Builder|QueryBuilder $query
*/
protected function applyPermissionWhereFilter($query, string $queryTable, string $entityTypeLimiter, string $entityTypeColumn)
{
$abilities = ['all' => [], 'own' => []];
$types = $entityTypeLimiter ? [$entityTypeLimiter] : ['page', 'chapter', 'bookshelf', 'book'];
$fullEntityTypeColumn = $queryTable . '.' . $entityTypeColumn;
foreach ($types as $type) {
$abilities['all'][$type] = userCan($type . '-view-all');
$abilities['own'][$type] = userCan($type . '-view-own');
}
$abilities['all'] = array_filter($abilities['all']);
$abilities['own'] = array_filter($abilities['own']);
$query->where(function (Builder $query) use ($abilities, $fullEntityTypeColumn, $entityTypeColumn) {
$query->where('perms_user', '=', 1)
->orWhere(function (Builder $query) {
$query->whereNull('perms_user')->where('perms_role', '=', 1);
})->orWhere(function (Builder $query) {
$query->whereNull(['perms_user', 'perms_role'])
->where('perms_fallback', '=', 1);
});
if (count($abilities['all']) > 0) {
$query->orWhere(function (Builder $query) use ($abilities, $fullEntityTypeColumn, $entityTypeColumn) {
$query->whereNull(['perms_user', 'perms_role', 'perms_fallback']);
if ($entityTypeColumn) {
$query->whereIn($fullEntityTypeColumn, array_keys($abilities['all']));
}
});
}
if (count($abilities['own']) > 0) {
$query->orWhere(function (Builder $query) use ($abilities, $fullEntityTypeColumn, $entityTypeColumn) {
$query->whereNull(['perms_user', 'perms_role', 'perms_fallback'])
->where('owned_by', '=', $this->currentUser()->id);
if ($entityTypeColumn) {
$query->whereIn($fullEntityTypeColumn, array_keys($abilities['all']));
}
});
}
});
}
/**
* @param Builder|QueryBuilder $query
*/
protected function applyPermissionJoin(callable $joinCallable, string $subAlias, $query, string $queryTable, string $entityTypeLimiter, string $entityIdColumn, string $entityTypeColumn)
{
$joinCondition = $this->getJoinCondition($queryTable, $subAlias, $entityIdColumn, $entityTypeColumn);
$query->joinSub(function (QueryBuilder $joinQuery) use ($joinCallable, $entityTypeLimiter) {
$joinQuery->select(['entity_id', 'entity_type'])->from('entity_permissions_collapsed')
->groupBy('entity_id', 'entity_type');
$joinCallable($joinQuery);
if ($entityTypeLimiter) {
$joinQuery->where('entity_type', '=', $entityTypeLimiter);
}
}, $subAlias, $joinCondition, null, null, 'left');
}
/**
* @param Builder|QueryBuilder $query
*/
protected function applyUserJoin($query, string $queryTable, string $entityTypeLimiter, string $entityIdColumn, string $entityTypeColumn)
{
$this->applyPermissionJoin(function (QueryBuilder $joinQuery) {
$joinQuery->selectRaw('max(view) as perms_user')
->where('user_id', '=', $this->currentUser()->id);
}, 'p_u', $query, $queryTable, $entityTypeLimiter, $entityIdColumn, $entityTypeColumn);
}
/**
* @param Builder|QueryBuilder $query
*/
protected function applyRoleJoin($query, string $queryTable, string $entityTypeLimiter, string $entityIdColumn, string $entityTypeColumn)
{
$this->applyPermissionJoin(function (QueryBuilder $joinQuery) {
$joinQuery->selectRaw('max(view) as perms_role')
->whereIn('role_id', $this->getCurrentUserRoleIds());
}, 'p_r', $query, $queryTable, $entityTypeLimiter, $entityIdColumn, $entityTypeColumn);
}
/**
* @param Builder|QueryBuilder $query
*/
protected function applyFallbackJoin($query, string $queryTable, string $entityTypeLimiter, string $entityIdColumn, string $entityTypeColumn)
{
$this->applyPermissionJoin(function (QueryBuilder $joinQuery) {
$joinQuery->selectRaw('max(view) as perms_fallback')
->whereNull(['role_id', 'user_id']);
}, 'p_f', $query, $queryTable, $entityTypeLimiter, $entityIdColumn, $entityTypeColumn);
}
protected function getJoinCondition(string $queryTable, string $joinTableName, string $entityIdColumn, string $entityTypeColumn): callable
{
return function (JoinClause $join) use ($queryTable, $joinTableName, $entityIdColumn, $entityTypeColumn) {
$join->on($queryTable . '.' . $entityIdColumn, '=', $joinTableName . '.entity_id');
if ($entityTypeColumn) {
$join->on($queryTable . '.' . $entityTypeColumn, '=', $joinTableName . '.entity_type');
}
};
}
/**
* Extend the given page query to ensure draft items are not visible
* unless created by the given user.
*/
public function restrictDraftsOnPageQuery(Builder $query): Builder
{
return $query->where(function (Builder $query) {
$query->where('draft', '=', false)
->orWhere(function (Builder $query) {
$query->where('draft', '=', true)
->where('owned_by', '=', $this->currentUser()->id);
});
});
}
/**
* Filter items that have entities set as a polymorphic relation.
* For simplicity, this will not return results attached to draft pages.
* Draft pages should never really have related items though.
*
* @param Builder|QueryBuilder $query
*/
public function restrictEntityRelationQuery($query, string $tableName, string $entityIdColumn, string $entityTypeColumn)
{
$query->leftJoinSub(function (QueryBuilder $query) {
$query->select(['id as entity_id', DB::raw("'page' as entity_type"), 'owned_by', 'deleted_at', 'draft'])->from('pages');
$tablesByType = ['page' => 'pages', 'book' => 'books', 'chapter' => 'chapters', 'bookshelf' => 'bookshelves'];
foreach ($tablesByType as $type => $table) {
$query->unionAll(function (QueryBuilder $query) use ($type, $table) {
$query->select(['id as entity_id', DB::raw("'{$type}' as entity_type"), 'owned_by', 'deleted_at', DB::raw('0 as draft')])->from($table);
});
}
}, 'entities', function (JoinClause $join) use ($tableName, $entityIdColumn, $entityTypeColumn) {
$join->on($tableName . '.' . $entityIdColumn, '=', 'entities.entity_id')
->on($tableName . '.' . $entityTypeColumn, '=', 'entities.entity_type');
});
$this->applyPermissionsToQuery($query, $tableName, '', $entityIdColumn, $entityTypeColumn);
// TODO - Test page draft access (Might allow drafts which should not be seen)
return $query;
}
/**
* Add conditions to a query for a model that's a relation of a page, so only the model results
* on visible pages are returned by the query.
* Is effectively the same as "restrictEntityRelationQuery" but takes into account page drafts
* while not expecting a polymorphic relation, Just a simpler one-page-to-many-relations set-up.
*/
public function restrictPageRelationQuery(Builder $query, string $tableName, string $pageIdColumn): Builder
{
$morphClass = (new Page())->getMorphClass();
$this->applyPermissionsToQuery($query, $tableName, $morphClass, $pageIdColumn, '');
// TODO - Draft display
// TODO - Likely need owned_by entity join workaround as used above
return $query;
}
/**
* Get the current user.
*/
protected function currentUser(): User
{
return user();
}
/**
* Get the roles for the current logged-in user.
*
* @return int[]
*/
protected function getCurrentUserRoleIds(): array
{
if (auth()->guest()) {
return [Role::getSystemRole('public')->id];
}
return $this->currentUser()->roles->pluck('id')->values()->all();
}
/**
* Ensure the given action is a valid and expected entity action.
* Throws an exception if invalid otherwise does nothing.
* @throws InvalidArgumentException
*/
protected function ensureValidEntityAction(string $action): void
{
if (!in_array($action, EntityPermission::PERMISSIONS)) {
throw new InvalidArgumentException('Action should be a simple entity permission action, not a role permission');
}
}
}

View File

@@ -1,10 +1,9 @@
<?php
namespace BookStack\Permissions;
namespace BookStack\Auth\Permissions;
use BookStack\Auth\Role;
use BookStack\Entities\Models\Entity;
use BookStack\Permissions\Models\EntityPermission;
use BookStack\Users\Models\Role;
class PermissionFormData
{
@@ -22,19 +21,32 @@ class PermissionFormData
{
return $this->entity->permissions()
->with('role')
->where('role_id', '!=', 0)
->whereNotNull('role_id')
->get()
->sortBy('role.display_name')
->all();
}
/**
* Get the permissions with assigned users.
*/
public function permissionsWithUsers(): array
{
return $this->entity->permissions()
->with('user')
->whereNotNull('user_id')
->get()
->sortBy('user.name')
->all();
}
/**
* Get the roles that don't yet have specific permissions for the
* entity we're managing permissions for.
*/
public function rolesNotAssigned(): array
{
$assigned = $this->entity->permissions()->pluck('role_id');
$assigned = $this->entity->permissions()->whereNotNull('role_id')->pluck('role_id');
return Role::query()
->where('system_name', '!=', 'admin')
->whereNotIn('id', $assigned)
@@ -50,20 +62,19 @@ class PermissionFormData
{
/** @var ?EntityPermission $permission */
$permission = $this->entity->permissions()
->where('role_id', '=', 0)
->whereNull(['role_id', 'user_id'])
->first();
return $permission ?? (new EntityPermission());
}
/**
* Get the "Everyone Else" role entry.
* Check if the "Everyone else" option is inheriting default role system permissions.
* Is determined by any system entity_permission existing for the current entity.
*/
public function everyoneElseRole(): Role
public function everyoneElseInheriting(): bool
{
return (new Role())->forceFill([
'id' => 0,
'display_name' => trans('entities.permissions_role_everyone_else'),
'description' => trans('entities.permissions_role_everyone_else_desc'),
]);
return !$this->entity->permissions()
->whereNull(['role_id', 'user_id'])
->exists();
}
}

View File

@@ -1,21 +1,23 @@
<?php
namespace BookStack\Permissions;
namespace BookStack\Auth\Permissions;
use BookStack\Activity\ActivityType;
use BookStack\Actions\ActivityType;
use BookStack\Auth\Role;
use BookStack\Exceptions\PermissionsException;
use BookStack\Facades\Activity;
use BookStack\Permissions\Models\RolePermission;
use BookStack\Users\Models\Role;
use Exception;
use Illuminate\Database\Eloquent\Collection;
class PermissionsRepo
{
protected JointPermissionBuilder $permissionBuilder;
protected CollapsedPermissionBuilder $permissionBuilder;
protected array $systemRoles = ['admin', 'public'];
public function __construct(JointPermissionBuilder $permissionBuilder)
/**
* PermissionsRepo constructor.
*/
public function __construct(CollapsedPermissionBuilder $permissionBuilder)
{
$this->permissionBuilder = $permissionBuilder;
}
@@ -39,7 +41,7 @@ class PermissionsRepo
/**
* Get a role via its ID.
*/
public function getRoleById(int $id): Role
public function getRoleById($id): Role
{
return Role::query()->findOrFail($id);
}
@@ -50,12 +52,11 @@ class PermissionsRepo
public function saveNewRole(array $roleData): Role
{
$role = new Role($roleData);
$role->mfa_enforced = boolval($roleData['mfa_enforced'] ?? false);
$role->mfa_enforced = ($roleData['mfa_enforced'] ?? 'false') === 'true';
$role->save();
$permissions = $roleData['permissions'] ?? [];
$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
$this->assignRolePermissions($role, $permissions);
$this->permissionBuilder->rebuildForRole($role);
Activity::add(ActivityType::ROLE_CREATE, $role);
@@ -64,45 +65,41 @@ class PermissionsRepo
/**
* Updates an existing role.
* Ensures Admin system role always have core permissions.
* Ensure Admin role always have core permissions.
*/
public function updateRole($roleId, array $roleData): Role
public function updateRole($roleId, array $roleData)
{
$role = $this->getRoleById($roleId);
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
$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
if ($role->system_name === 'admin') {
$permissionNameArray = array_unique(array_merge($permissionNameArray, [
$permissions = array_merge($permissions, [
'users-manage',
'user-roles-manage',
'restrictions-manage-all',
'restrictions-manage-own',
'settings-manage',
]));
]);
}
if (!empty($permissionNameArray)) {
$this->assignRolePermissions($role, $permissions);
$role->fill($roleData);
$role->mfa_enforced = ($roleData['mfa_enforced'] ?? 'false') === 'true';
$role->save();
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) {
$permissions = RolePermission::query()
->whereIn('name', $permissionNameArray)
->pluck('id')
@@ -115,13 +112,13 @@ class PermissionsRepo
/**
* Delete a role from the system.
* Check it's not an admin role or set as default before deleting.
* If a migration Role ID is specified the users assign to the current role
* If an 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(int $roleId, int $migrateRoleId = 0): void
public function deleteRole($roleId, $migrateRoleId)
{
$role = $this->getRoleById($roleId);
@@ -132,7 +129,7 @@ class PermissionsRepo
throw new PermissionsException(trans('errors.role_registration_default_cannot_delete'));
}
if ($migrateRoleId !== 0) {
if ($migrateRoleId) {
$newRole = Role::query()->find($migrateRoleId);
if ($newRole) {
$users = $role->users()->pluck('id')->toArray();
@@ -141,7 +138,7 @@ class PermissionsRepo
}
$role->entityPermissions()->delete();
$role->jointPermissions()->delete();
$role->collapsedPermissions()->delete();
Activity::add(ActivityType::ROLE_DELETE, $role);
$role->delete();
}

View File

@@ -1,15 +1,13 @@
<?php
namespace BookStack\Permissions\Models;
namespace BookStack\Auth\Permissions;
use BookStack\App\Model;
use BookStack\Users\Models\Role;
use BookStack\Auth\Role;
use BookStack\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
/**
* @property int $id
* @property string $name
* @property string $display_name
*/
class RolePermission extends Model
{

View File

@@ -0,0 +1,11 @@
<?php
namespace BookStack\Auth\Permissions;
class SimpleEntityData
{
public int $id;
public string $type;
public ?int $book_id;
public ?int $chapter_id;
}

View File

@@ -1,8 +1,8 @@
<?php
namespace BookStack\Users\Queries;
namespace BookStack\Auth\Queries;
use BookStack\Users\Models\Role;
use BookStack\Auth\Role;
use BookStack\Util\SimpleListOptions;
use Illuminate\Pagination\LengthAwarePaginator;
@@ -15,7 +15,7 @@ class RolesAllPaginatedAndSorted
{
$sort = $listOptions->getSort();
if ($sort === 'created_at') {
$sort = 'roles.created_at';
$sort = 'users.created_at';
}
$query = Role::query()->select(['*'])

View File

@@ -1,12 +1,12 @@
<?php
namespace BookStack\Users\Queries;
namespace BookStack\Auth\Queries;
use BookStack\Auth\User;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Page;
use BookStack\Users\Models\User;
/**
* Get asset created counts for the given user.

View File

@@ -1,12 +1,12 @@
<?php
namespace BookStack\Users\Queries;
namespace BookStack\Auth\Queries;
use BookStack\Auth\User;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Page;
use BookStack\Users\Models\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;

View File

@@ -1,8 +1,8 @@
<?php
namespace BookStack\Users\Queries;
namespace BookStack\Auth\Queries;
use BookStack\Users\Models\User;
use BookStack\Auth\User;
use BookStack\Util\SimpleListOptions;
use Illuminate\Pagination\LengthAwarePaginator;

View File

@@ -1,12 +1,12 @@
<?php
namespace BookStack\Users\Models;
namespace BookStack\Auth;
use BookStack\Activity\Models\Loggable;
use BookStack\App\Model;
use BookStack\Permissions\Models\EntityPermission;
use BookStack\Permissions\Models\JointPermission;
use BookStack\Permissions\Models\RolePermission;
use BookStack\Auth\Permissions\CollapsedPermission;
use BookStack\Auth\Permissions\EntityPermission;
use BookStack\Auth\Permissions\RolePermission;
use BookStack\Interfaces\Loggable;
use BookStack\Model;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
@@ -27,14 +27,10 @@ class Role extends Model implements Loggable
{
use HasFactory;
protected $fillable = ['display_name', 'description', 'external_auth_id', 'mfa_enforced'];
protected $fillable = ['display_name', 'description', 'external_auth_id'];
protected $hidden = ['pivot'];
protected $casts = [
'mfa_enforced' => 'boolean',
];
/**
* The roles that belong to the role.
*/
@@ -43,14 +39,6 @@ class Role extends Model implements Loggable
return $this->belongsToMany(User::class)->orderBy('name', 'asc');
}
/**
* Get all related JointPermissions.
*/
public function jointPermissions(): HasMany
{
return $this->hasMany(JointPermission::class);
}
/**
* The RolePermissions that belong to the role.
*/
@@ -67,6 +55,14 @@ class Role extends Model implements Loggable
return $this->hasMany(EntityPermission::class);
}
/**
* Get all related entity collapsed permissions.
*/
public function collapsedPermissions(): HasMany
{
return $this->hasMany(CollapsedPermission::class);
}
/**
* Check if this role has a permission.
*/
@@ -111,13 +107,7 @@ class Role extends Model implements Loggable
*/
public static function getSystemRole(string $systemName): ?self
{
static $cache = [];
if (!isset($cache[$systemName])) {
$cache[$systemName] = static::query()->where('system_name', '=', $systemName)->first();
}
return $cache[$systemName];
return static::query()->where('system_name', '=', $systemName)->first();
}
/**

View File

@@ -1,10 +1,9 @@
<?php
namespace BookStack\Access;
namespace BookStack\Auth;
use BookStack\Activity\Models\Loggable;
use BookStack\App\Model;
use BookStack\Users\Models\User;
use BookStack\Interfaces\Loggable;
use BookStack\Model;
/**
* Class SocialAccount.

View File

@@ -1,15 +1,16 @@
<?php
namespace BookStack\Users\Models;
namespace BookStack\Auth;
use BookStack\Access\Mfa\MfaValue;
use BookStack\Access\SocialAccount;
use BookStack\Activity\Models\Favourite;
use BookStack\Activity\Models\Loggable;
use BookStack\Actions\Favourite;
use BookStack\Api\ApiToken;
use BookStack\App\Model;
use BookStack\App\Sluggable;
use BookStack\Auth\Access\Mfa\MfaValue;
use BookStack\Auth\Permissions\CollapsedPermission;
use BookStack\Auth\Permissions\EntityPermission;
use BookStack\Entities\Tools\SlugGenerator;
use BookStack\Interfaces\Loggable;
use BookStack\Interfaces\Sluggable;
use BookStack\Model;
use BookStack\Notifications\ResetPassword;
use BookStack\Uploads\Image;
use Carbon\Carbon;
@@ -73,7 +74,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', 'pivot',
'created_at', 'updated_at', 'image_id', 'roles', 'avatar', 'user_id',
];
/**
@@ -201,7 +202,6 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
public function attachRole(Role $role)
{
$this->roles()->attach($role->id);
$this->unsetRelation('roles');
}
/**
@@ -300,6 +300,22 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
}, 'activities', 'users.id', '=', 'activities.user_id');
}
/**
* Get the entity permissions assigned to this specific user.
*/
public function entityPermissions(): HasMany
{
return $this->hasMany(EntityPermission::class);
}
/**
* Get all related entity collapsed permissions.
*/
public function collapsedPermissions(): HasMany
{
return $this->hasMany(CollapsedPermission::class);
}
/**
* Get the url for editing this user.
*/

View File

@@ -1,16 +1,14 @@
<?php
namespace BookStack\Users;
namespace BookStack\Auth;
use BookStack\Access\UserInviteService;
use BookStack\Activity\ActivityType;
use BookStack\Actions\ActivityType;
use BookStack\Auth\Access\UserInviteService;
use BookStack\Entities\EntityProvider;
use BookStack\Exceptions\NotifyException;
use BookStack\Exceptions\UserUpdateException;
use BookStack\Facades\Activity;
use BookStack\Uploads\UserAvatars;
use BookStack\Users\Models\Role;
use BookStack\Users\Models\User;
use Exception;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Log;
@@ -155,6 +153,8 @@ class UserRepo
$user->apiTokens()->delete();
$user->favourites()->delete();
$user->mfaValues()->delete();
$user->collapsedPermissions()->delete();
$user->entityPermissions()->delete();
$user->delete();
// Delete user profile images
@@ -236,8 +236,6 @@ class UserRepo
*/
protected function setUserRoles(User $user, array $roles)
{
$roles = array_filter(array_values($roles));
if ($this->demotingLastAdmin($user, $roles)) {
throw new UserUpdateException(trans('errors.role_cannot_remove_only_admin'), $user->getEditUrl());
}

View File

@@ -8,8 +8,6 @@
* 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.
@@ -100,13 +98,7 @@ return [
// Encryption cipher
'cipher' => 'AES-256-CBC',
// Maintenance Mode Driver
'maintenance' => [
'driver' => 'file',
// 'store' => 'redis',
],
// Application Service Providers
// Application Services Provides
'providers' => [
// Laravel Framework Service Providers...
@@ -139,19 +131,68 @@ return [
SocialiteProviders\Manager\ServiceProvider::class,
// BookStack custom service providers
\BookStack\App\Providers\ThemeServiceProvider::class,
\BookStack\App\Providers\AppServiceProvider::class,
\BookStack\App\Providers\AuthServiceProvider::class,
\BookStack\App\Providers\EventServiceProvider::class,
\BookStack\App\Providers\RouteServiceProvider::class,
\BookStack\App\Providers\TranslationServiceProvider::class,
\BookStack\App\Providers\ValidationRuleServiceProvider::class,
\BookStack\App\Providers\ViewTweaksServiceProvider::class,
BookStack\Providers\ThemeServiceProvider::class,
BookStack\Providers\AppServiceProvider::class,
BookStack\Providers\AuthServiceProvider::class,
BookStack\Providers\EventServiceProvider::class,
BookStack\Providers\RouteServiceProvider::class,
BookStack\Providers\TranslationServiceProvider::class,
BookStack\Providers\ValidationRuleServiceProvider::class,
BookStack\Providers\ViewTweaksServiceProvider::class,
],
// Class Aliases
// This array of class aliases to be registered on application start.
'aliases' => Facade::defaultAliases()->merge([
/*
|--------------------------------------------------------------------------
| 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,
// Laravel Packages
'Socialite' => Laravel\Socialite\Facades\Socialite::class,
@@ -161,7 +202,7 @@ return [
// Custom BookStack
'Activity' => BookStack\Facades\Activity::class,
'Theme' => BookStack\Facades\Theme::class,
])->toArray(),
],
// Proxy configuration
'proxies' => env('APP_PROXIES', ''),

View File

@@ -59,12 +59,12 @@ return [
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => \BookStack\Users\Models\User::class,
'model' => \BookStack\Auth\User::class,
],
'external' => [
'driver' => 'external-users',
'model' => \BookStack\Users\Models\User::class,
'model' => \BookStack\Auth\User::class,
],
// 'users' => [

View File

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

View File

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

View File

@@ -33,20 +33,17 @@ 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' => [
@@ -57,7 +54,6 @@ return [
'bucket' => env('STORAGE_S3_BUCKET', 'your-bucket'),
'endpoint' => env('STORAGE_S3_ENDPOINT', null),
'use_path_style_endpoint' => env('STORAGE_S3_ENDPOINT', null) !== null,
'throw' => true,
],
],

View File

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

View File

@@ -8,17 +8,19 @@
* Do not edit this file unless you're happy to maintain any changes yourself.
*/
// Configured mail encryption method.
// STARTTLS should still be attempted, but tls/ssl forces TLS usage.
$mailEncryption = env('MAIL_ENCRYPTION', null);
return [
// Mail driver to use.
// From Laravel 7+ this is MAIL_MAILER in laravel.
// Kept as MAIL_DRIVER in BookStack to prevent breaking change.
// Options: smtp, sendmail, log, array
'default' => env('MAIL_DRIVER', 'smtp'),
'driver' => env('MAIL_DRIVER', 'smtp'),
// SMTP host address
'host' => env('MAIL_HOST', 'smtp.mailgun.org'),
// SMTP host port
'port' => env('MAIL_PORT', 587),
// Global "From" address & name
'from' => [
@@ -26,44 +28,17 @@ return [
'name' => env('MAIL_FROM_NAME', 'BookStack'),
],
// Mailer Configurations
// Available mailing methods and their settings.
'mailers' => [
'smtp' => [
'transport' => 'smtp',
'scheme' => null,
'host' => env('MAIL_HOST', 'smtp.mailgun.org'),
'port' => env('MAIL_PORT', 587),
'username' => env('MAIL_USERNAME'),
'password' => env('MAIL_PASSWORD'),
'verify_peer' => env('MAIL_VERIFY_SSL', true),
'timeout' => null,
'local_domain' => env('MAIL_EHLO_DOMAIN'),
'tls_required' => ($mailEncryption === 'tls' || $mailEncryption === 'ssl'),
],
// Email encryption protocol
'encryption' => env('MAIL_ENCRYPTION', 'tls'),
'sendmail' => [
'transport' => 'sendmail',
'path' => env('MAIL_SENDMAIL_COMMAND', '/usr/sbin/sendmail -bs'),
],
// SMTP server username
'username' => env('MAIL_USERNAME'),
'log' => [
'transport' => 'log',
'channel' => env('MAIL_LOG_CHANNEL'),
],
// SMTP server password
'password' => env('MAIL_PASSWORD'),
'array' => [
'transport' => 'array',
],
'failover' => [
'transport' => 'failover',
'mailers' => [
'smtp',
'log',
],
],
],
// Sendmail application path
'sendmail' => '/usr/sbin/sendmail -bs',
// Email markdown configuration
'markdown' => [
@@ -72,4 +47,11 @@ return [
resource_path('views/vendor/mail'),
],
],
// Log Channel
// If you are using the "log" driver, you may specify the logging channel
// if you prefer to keep mail messages separate from other log entries
// for simpler reading. Otherwise, the default channel will be used.
'log_channel' => env('MAIL_LOG_CHANNEL'),
];

View File

@@ -8,12 +8,9 @@ return [
// Dump user details after a login request for debugging purposes
'dump_user_details' => env('OIDC_DUMP_USER_DETAILS', false),
// Claim, within an OpenId token, to find the user's display name
// Attribute, within a OpenId token, to find the user's display name
'display_name_claims' => explode('|', env('OIDC_DISPLAY_NAME_CLAIMS', 'name')),
// Claim, within an OpenID token, to use to connect a BookStack user to the OIDC user.
'external_id_claim' => env('OIDC_EXTERNAL_ID_CLAIM', 'sub'),
// OAuth2/OpenId client id, as configured in your Authorization server.
'client_id' => env('OIDC_CLIENT_ID', null),

View File

@@ -16,20 +16,11 @@ return [
'app-editor' => 'wysiwyg',
'app-color' => '#206ea7',
'app-color-light' => 'rgba(32,110,167,0.15)',
'link-color' => '#206ea7',
'bookshelf-color' => '#a94747',
'book-color' => '#077b70',
'chapter-color' => '#af4d0d',
'page-color' => '#206ea7',
'page-draft-color' => '#7e50b1',
'app-color-dark' => '#195785',
'app-color-light-dark' => 'rgba(32,110,167,0.15)',
'link-color-dark' => '#429fe3',
'bookshelf-color-dark' => '#ff5454',
'book-color-dark' => '#389f60',
'chapter-color-dark' => '#ee7a2d',
'page-color-dark' => '#429fe3',
'page-draft-color-dark' => '#a66ce8',
'app-custom-head' => false,
'registration-enabled' => false,

View File

@@ -6,7 +6,7 @@ use BookStack\Uploads\ImageService;
use Illuminate\Console\Command;
use Symfony\Component\Console\Output\OutputInterface;
class CleanupImagesCommand extends Command
class CleanupImages extends Command
{
/**
* The name and signature of the console command.
@@ -25,49 +25,60 @@ class CleanupImagesCommand extends Command
*/
protected $description = 'Cleanup images and drawings';
protected $imageService;
/**
* Create a new command instance.
*
* @param \BookStack\Uploads\ImageService $imageService
*/
public function __construct(ImageService $imageService)
{
$this->imageService = $imageService;
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle(ImageService $imageService): int
public function handle()
{
$checkRevisions = !$this->option('all');
$dryRun = !$this->option('force');
$checkRevisions = $this->option('all') ? false : true;
$dryRun = $this->option('force') ? false : true;
if (!$dryRun) {
$this->warn("This operation is destructive and is not guaranteed to be fully accurate.\nEnsure you have a backup of your images.\n");
$proceed = $this->confirm("Are you sure you want to proceed?");
$proceed = $this->confirm("This operation is destructive and is not guaranteed to be fully accurate.\nEnsure you have a backup of your images.\nAre you sure you want to proceed?");
if (!$proceed) {
return 0;
return;
}
}
$deleted = $imageService->deleteUnusedImages($checkRevisions, $dryRun);
$deleted = $this->imageService->deleteUnusedImages($checkRevisions, $dryRun);
$deleteCount = count($deleted);
if ($dryRun) {
$this->comment('Dry run, no images have been deleted');
$this->comment('Dry run, No images have been deleted');
$this->comment($deleteCount . ' images found that would have been deleted');
$this->showDeletedImages($deleted);
$this->comment('Run with -f or --force to perform deletions');
return 0;
return;
}
$this->showDeletedImages($deleted);
$this->comment($deleteCount . ' images deleted');
return 0;
}
protected function showDeletedImages($paths): void
protected function showDeletedImages($paths)
{
if ($this->getOutput()->getVerbosity() <= OutputInterface::VERBOSITY_NORMAL) {
return;
}
if (count($paths) > 0) {
$this->line('Images to delete:');
}
foreach ($paths as $path) {
$this->line($path);
}

View File

@@ -2,10 +2,10 @@
namespace BookStack\Console\Commands;
use BookStack\Activity\Models\Activity;
use BookStack\Actions\Activity;
use Illuminate\Console\Command;
class ClearActivityCommand extends Command
class ClearActivity extends Command
{
/**
* The name and signature of the console command.
@@ -21,13 +21,27 @@ class ClearActivityCommand extends Command
*/
protected $description = 'Clear user activity from the system';
protected $activity;
/**
* Create a new command instance.
*
* @param Activity $activity
*/
public function __construct(Activity $activity)
{
$this->activity = $activity;
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle(): int
public function handle()
{
Activity::query()->truncate();
$this->activity->newQuery()->truncate();
$this->comment('System activity cleared');
return 0;
}
}

View File

@@ -5,7 +5,7 @@ namespace BookStack\Console\Commands;
use BookStack\Entities\Models\PageRevision;
use Illuminate\Console\Command;
class ClearRevisionsCommand extends Command
class ClearRevisions extends Command
{
/**
* The name and signature of the console command.
@@ -23,14 +23,28 @@ class ClearRevisionsCommand extends Command
*/
protected $description = 'Clear page revisions';
protected $pageRevision;
/**
* Create a new command instance.
*
* @param PageRevision $pageRevision
*/
public function __construct(PageRevision $pageRevision)
{
$this->pageRevision = $pageRevision;
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle(): int
public function handle()
{
$deleteTypes = $this->option('all') ? ['version', 'update_draft'] : ['version'];
PageRevision::query()->whereIn('type', $deleteTypes)->delete();
$this->pageRevision->newQuery()->whereIn('type', $deleteTypes)->delete();
$this->comment('Revisions deleted');
return 0;
}
}

View File

@@ -2,10 +2,10 @@
namespace BookStack\Console\Commands;
use BookStack\Activity\Models\View;
use BookStack\Actions\View;
use Illuminate\Console\Command;
class ClearViewsCommand extends Command
class ClearViews extends Command
{
/**
* The name and signature of the console command.
@@ -22,12 +22,21 @@ class ClearViewsCommand extends Command
protected $description = 'Clear all view-counts for all entities';
/**
* Execute the console command.
* Create a new command instance.
*/
public function handle(): int
public function __construct()
{
View::query()->truncate();
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
View::clearAll();
$this->comment('Views cleared');
return 0;
}
}

View File

@@ -6,7 +6,7 @@ use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Tools\PermissionsUpdater;
use Illuminate\Console\Command;
class CopyShelfPermissionsCommand extends Command
class CopyShelfPermissions extends Command
{
/**
* The name and signature of the console command.
@@ -25,10 +25,25 @@ class CopyShelfPermissionsCommand extends Command
*/
protected $description = 'Copy shelf permissions to all child books';
protected PermissionsUpdater $permissionsUpdater;
/**
* Create a new command instance.
*
* @return void
*/
public function __construct(PermissionsUpdater $permissionsUpdater)
{
$this->permissionsUpdater = $permissionsUpdater;
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle(PermissionsUpdater $permissionsUpdater): int
public function handle()
{
$shelfSlug = $this->option('slug');
$cascadeAll = $this->option('all');
@@ -37,7 +52,7 @@ class CopyShelfPermissionsCommand extends Command
if (!$cascadeAll && !$shelfSlug) {
$this->error('Either a --slug or --all option must be provided.');
return 1;
return;
}
if ($cascadeAll) {
@@ -48,7 +63,7 @@ class CopyShelfPermissionsCommand extends Command
);
if (!$continue && !$this->hasOption('no-interaction')) {
return 0;
return;
}
$shelves = Bookshelf::query()->get(['id']);
@@ -62,11 +77,10 @@ class CopyShelfPermissionsCommand extends Command
}
foreach ($shelves as $shelf) {
$permissionsUpdater->updateBookPermissionsFromShelf($shelf, false);
$this->permissionsUpdater->updateBookPermissionsFromShelf($shelf, false);
$this->info('Copied permissions for shelf [' . $shelf->id . ']');
}
$this->info('Permissions copied for ' . $shelves->count() . ' shelves.');
return 0;
}
}

View File

@@ -2,15 +2,17 @@
namespace BookStack\Console\Commands;
use BookStack\Users\Models\Role;
use BookStack\Users\UserRepo;
use BookStack\Auth\Role;
use BookStack\Auth\UserRepo;
use BookStack\Exceptions\NotFoundException;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules\Password;
use Illuminate\Validation\Rules\Unique;
use Symfony\Component\Console\Command\Command as SymfonyCommand;
class CreateAdminCommand extends Command
class CreateAdmin extends Command
{
/**
* The name and signature of the console command.
@@ -30,10 +32,25 @@ class CreateAdminCommand extends Command
*/
protected $description = 'Add a new admin user to the system';
protected $userRepo;
/**
* Create a new command instance.
*/
public function __construct(UserRepo $userRepo)
{
$this->userRepo = $userRepo;
parent::__construct();
}
/**
* Execute the console command.
*
* @throws NotFoundException
*
* @return mixed
*/
public function handle(UserRepo $userRepo): int
public function handle()
{
$details = $this->snakeCaseOptions();
@@ -65,17 +82,17 @@ class CreateAdminCommand extends Command
$this->error($error);
}
return 1;
return SymfonyCommand::FAILURE;
}
$user = $userRepo->createWithoutActivity($validator->validated());
$user = $this->userRepo->createWithoutActivity($validator->validated());
$user->attachRole(Role::getSystemRole('admin'));
$user->email_confirmed = true;
$user->save();
$this->info("Admin account with email \"{$user->email}\" successfully created!");
return 0;
return SymfonyCommand::SUCCESS;
}
protected function snakeCaseOptions(): array

View File

@@ -0,0 +1,53 @@
<?php
namespace BookStack\Console\Commands;
use BookStack\Auth\User;
use BookStack\Auth\UserRepo;
use Illuminate\Console\Command;
class DeleteUsers extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'bookstack:delete-users';
protected $userRepo;
/**
* The console command description.
*
* @var string
*/
protected $description = 'Delete users that are not "admin" or system users';
public function __construct(UserRepo $userRepo)
{
$this->userRepo = $userRepo;
parent::__construct();
}
public function handle()
{
$confirm = $this->ask('This will delete all users from the system that are not "admin" or system users. Are you sure you want to continue? (Type "yes" to continue)');
$numDeleted = 0;
if (strtolower(trim($confirm)) === 'yes') {
$totalUsers = User::query()->count();
$users = User::query()->whereNull('system_name')->with('roles')->get();
foreach ($users as $user) {
if ($user->hasSystemRole('admin')) {
// don't delete users with "admin" role
continue;
}
$this->userRepo->destroy($user);
$numDeleted++;
}
$this->info("Deleted $numDeleted of $totalUsers total users.");
} else {
$this->info('Exiting...');
}
}
}

View File

@@ -1,53 +0,0 @@
<?php
namespace BookStack\Console\Commands;
use BookStack\Users\Models\User;
use BookStack\Users\UserRepo;
use Illuminate\Console\Command;
class DeleteUsersCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'bookstack:delete-users';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Delete users that are not "admin" or system users';
/**
* Execute the console command.
*/
public function handle(UserRepo $userRepo): int
{
$this->warn('This will delete all users from the system that are not "admin" or system users.');
$confirm = $this->confirm('Are you sure you want to continue?');
if (!$confirm) {
return 0;
}
$totalUsers = User::query()->count();
$numDeleted = 0;
$users = User::query()->whereNull('system_name')->with('roles')->get();
foreach ($users as $user) {
if ($user->hasSystemRole('admin')) {
// don't delete users with "admin" role
continue;
}
$userRepo->destroy($user);
$numDeleted++;
}
$this->info("Deleted $numDeleted of $totalUsers total users.");
return 0;
}
}

View File

@@ -2,20 +2,19 @@
namespace BookStack\Console\Commands;
use BookStack\Activity\CommentRepo;
use BookStack\Activity\Models\Comment;
use BookStack\Actions\Comment;
use BookStack\Actions\CommentRepo;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class RegenerateCommentContentCommand extends Command
class RegenerateCommentContent extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'bookstack:regenerate-comment-content
{--database= : The database connection to use}';
protected $signature = 'bookstack:regenerate-comment-content {--database= : The database connection to use.}';
/**
* The console command description.
@@ -25,18 +24,34 @@ class RegenerateCommentContentCommand extends Command
protected $description = 'Regenerate the stored HTML of all comments';
/**
* Execute the console command.
* @var CommentRepo
*/
public function handle(CommentRepo $commentRepo): int
protected $commentRepo;
/**
* Create a new command instance.
*/
public function __construct(CommentRepo $commentRepo)
{
$this->commentRepo = $commentRepo;
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$connection = DB::getDefaultConnection();
if ($this->option('database') !== null) {
DB::setDefaultConnection($this->option('database'));
}
Comment::query()->chunk(100, function ($comments) use ($commentRepo) {
Comment::query()->chunk(100, function ($comments) {
foreach ($comments as $comment) {
$comment->html = $commentRepo->commentToHtml($comment->text);
$comment->html = $this->commentRepo->commentToHtml($comment->text);
$comment->save();
}
});

View File

@@ -2,19 +2,18 @@
namespace BookStack\Console\Commands;
use BookStack\Permissions\JointPermissionBuilder;
use BookStack\Auth\Permissions\CollapsedPermissionBuilder;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class RegeneratePermissionsCommand extends Command
class RegeneratePermissions extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'bookstack:regenerate-permissions
{--database= : The database connection to use}';
protected $signature = 'bookstack:regenerate-permissions {--database= : The database connection to use.}';
/**
* The console command description.
@@ -23,10 +22,23 @@ class RegeneratePermissionsCommand extends Command
*/
protected $description = 'Regenerate all system permissions';
protected CollapsedPermissionBuilder $permissionBuilder;
/**
* Create a new command instance.
*/
public function __construct(CollapsedPermissionBuilder $permissionBuilder)
{
$this->permissionBuilder = $permissionBuilder;
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle(JointPermissionBuilder $permissionBuilder): int
public function handle()
{
$connection = DB::getDefaultConnection();
@@ -34,7 +46,7 @@ class RegeneratePermissionsCommand extends Command
DB::setDefaultConnection($this->option('database'));
}
$permissionBuilder->rebuildForAll();
$this->permissionBuilder->rebuildForAll();
DB::setDefaultConnection($connection);
$this->comment('Permissions regenerated');

View File

@@ -6,15 +6,14 @@ use BookStack\References\ReferenceStore;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class RegenerateReferencesCommand extends Command
class RegenerateReferences extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'bookstack:regenerate-references
{--database= : The database connection to use}';
protected $signature = 'bookstack:regenerate-references {--database= : The database connection to use.}';
/**
* The console command description.
@@ -23,10 +22,25 @@ class RegenerateReferencesCommand extends Command
*/
protected $description = 'Regenerate all the cross-item model reference index';
protected ReferenceStore $references;
/**
* Create a new command instance.
*
* @return void
*/
public function __construct(ReferenceStore $references)
{
$this->references = $references;
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle(ReferenceStore $references): int
public function handle()
{
$connection = DB::getDefaultConnection();
@@ -34,7 +48,7 @@ class RegenerateReferencesCommand extends Command
DB::setDefaultConnection($this->option('database'));
}
$references->updateForAllPages();
$this->references->updateForAllPages();
DB::setDefaultConnection($connection);

View File

@@ -7,15 +7,14 @@ use BookStack\Search\SearchIndex;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class RegenerateSearchCommand extends Command
class RegenerateSearch extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'bookstack:regenerate-search
{--database= : The database connection to use}';
protected $signature = 'bookstack:regenerate-search {--database= : The database connection to use.}';
/**
* The console command description.
@@ -25,16 +24,32 @@ class RegenerateSearchCommand extends Command
protected $description = 'Re-index all content for searching';
/**
* Execute the console command.
* @var SearchIndex
*/
public function handle(SearchIndex $searchIndex): int
protected $searchIndex;
/**
* Create a new command instance.
*/
public function __construct(SearchIndex $searchIndex)
{
parent::__construct();
$this->searchIndex = $searchIndex;
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$connection = DB::getDefaultConnection();
if ($this->option('database') !== null) {
DB::setDefaultConnection($this->option('database'));
}
$searchIndex->indexAllEntities(function (Entity $model, int $processed, int $total): void {
$this->searchIndex->indexAllEntities(function (Entity $model, int $processed, int $total): void {
$this->info('Indexed ' . class_basename($model) . ' entries (' . $processed . '/' . $total . ')');
});

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