Merge branch 'master' into release
This commit is contained in:
commit
9865446267
|
@ -281,6 +281,12 @@ ALLOW_CONTENT_SCRIPTS=false
|
||||||
# Contents of the robots.txt file can be overridden, making this option obsolete.
|
# Contents of the robots.txt file can be overridden, making this option obsolete.
|
||||||
ALLOW_ROBOTS=null
|
ALLOW_ROBOTS=null
|
||||||
|
|
||||||
|
# Allow server-side fetches to be performed to potentially unknown
|
||||||
|
# and user-provided locations. Primarily used in exports when loading
|
||||||
|
# in externally referenced assets.
|
||||||
|
# Can be 'true' or 'false'.
|
||||||
|
ALLOW_UNTRUSTED_SERVER_FETCHING=false
|
||||||
|
|
||||||
# A list of hosts that BookStack can be iframed within.
|
# A list of hosts that BookStack can be iframed within.
|
||||||
# Space separated if multiple. BookStack host domain is auto-inferred.
|
# Space separated if multiple. BookStack host domain is auto-inferred.
|
||||||
# For Example: ALLOWED_IFRAME_HOSTS="https://example.com https://a.example.com"
|
# For Example: ALLOWED_IFRAME_HOSTS="https://example.com https://a.example.com"
|
||||||
|
|
|
@ -167,3 +167,19 @@ whenwesober :: Indonesian
|
||||||
Rem (remkovdhoef) :: Dutch
|
Rem (remkovdhoef) :: Dutch
|
||||||
syn7ax69 :: Bulgarian; Turkish
|
syn7ax69 :: Bulgarian; Turkish
|
||||||
Blaade :: French
|
Blaade :: French
|
||||||
|
Behzad HosseinPoor (behzad.hp) :: Persian
|
||||||
|
Ole Aldric (Swoy) :: Norwegian Bokmal
|
||||||
|
fharis arabia (raednahdi) :: Arabic
|
||||||
|
Alexander Predl (Harveyhase68) :: German
|
||||||
|
Rem (Rem9000) :: Dutch
|
||||||
|
Michał Stelmach (stelmach-web) :: Polish
|
||||||
|
arniom :: French
|
||||||
|
REMOVED_USER :: Turkish
|
||||||
|
林祖年 (contagion) :: Chinese Traditional
|
||||||
|
Siamak Guodarzi (siamakgoudarzi88) :: Persian
|
||||||
|
Lis Maestrelo (lismtrl) :: Portuguese, Brazilian
|
||||||
|
Nathanaël (nathanaelhoun) :: French
|
||||||
|
A Ibnu Hibban (abd.ibnuhibban) :: Indonesian
|
||||||
|
Frost-ZX :: Chinese Simplified
|
||||||
|
Kuzma Simonov (ovmach) :: Russian
|
||||||
|
Vojtěch Krystek (acantophis) :: Czech
|
||||||
|
|
|
@ -11,16 +11,15 @@ use Illuminate\Support\Str;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property string $type
|
* @property string $type
|
||||||
* @property User $user
|
* @property User $user
|
||||||
* @property Entity $entity
|
* @property Entity $entity
|
||||||
* @property string $detail
|
* @property string $detail
|
||||||
* @property string $entity_type
|
* @property string $entity_type
|
||||||
* @property int $entity_id
|
* @property int $entity_id
|
||||||
* @property int $user_id
|
* @property int $user_id
|
||||||
*/
|
*/
|
||||||
class Activity extends Model
|
class Activity extends Model
|
||||||
{
|
{
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the entity for this activity.
|
* Get the entity for this activity.
|
||||||
*/
|
*/
|
||||||
|
@ -29,6 +28,7 @@ class Activity extends Model
|
||||||
if ($this->entity_type === '') {
|
if ($this->entity_type === '') {
|
||||||
$this->entity_type = null;
|
$this->entity_type = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->morphTo('entity');
|
return $this->morphTo('entity');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,7 +54,7 @@ class Activity extends Model
|
||||||
public function isForEntity(): bool
|
public function isForEntity(): bool
|
||||||
{
|
{
|
||||||
return Str::startsWith($this->type, [
|
return Str::startsWith($this->type, [
|
||||||
'page_', 'chapter_', 'book_', 'bookshelf_'
|
'page_', 'chapter_', 'book_', 'bookshelf_',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
<?php namespace BookStack\Actions;
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Actions;
|
||||||
|
|
||||||
use BookStack\Auth\Permissions\PermissionService;
|
use BookStack\Auth\Permissions\PermissionService;
|
||||||
use BookStack\Auth\User;
|
use BookStack\Auth\User;
|
||||||
|
@ -33,6 +35,7 @@ class ActivityService
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a generic activity event to the database.
|
* Add a generic activity event to the database.
|
||||||
|
*
|
||||||
* @param string|Loggable $detail
|
* @param string|Loggable $detail
|
||||||
*/
|
*/
|
||||||
public function add(string $type, $detail = '')
|
public function add(string $type, $detail = '')
|
||||||
|
@ -54,7 +57,7 @@ class ActivityService
|
||||||
{
|
{
|
||||||
return $this->activity->newInstance()->forceFill([
|
return $this->activity->newInstance()->forceFill([
|
||||||
'type' => strtolower($type),
|
'type' => strtolower($type),
|
||||||
'user_id' => user()->id,
|
'user_id' => user()->id,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,8 +70,8 @@ class ActivityService
|
||||||
{
|
{
|
||||||
$entity->activity()->update([
|
$entity->activity()->update([
|
||||||
'detail' => $entity->name,
|
'detail' => $entity->name,
|
||||||
'entity_id' => null,
|
'entity_id' => null,
|
||||||
'entity_type' => null,
|
'entity_type' => null,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,10 +101,10 @@ class ActivityService
|
||||||
$queryIds = [$entity->getMorphClass() => [$entity->id]];
|
$queryIds = [$entity->getMorphClass() => [$entity->id]];
|
||||||
|
|
||||||
if ($entity->isA('book')) {
|
if ($entity->isA('book')) {
|
||||||
$queryIds[(new Chapter)->getMorphClass()] = $entity->chapters()->visible()->pluck('id');
|
$queryIds[(new Chapter())->getMorphClass()] = $entity->chapters()->visible()->pluck('id');
|
||||||
}
|
}
|
||||||
if ($entity->isA('book') || $entity->isA('chapter')) {
|
if ($entity->isA('book') || $entity->isA('chapter')) {
|
||||||
$queryIds[(new Page)->getMorphClass()] = $entity->pages()->visible()->pluck('id');
|
$queryIds[(new Page())->getMorphClass()] = $entity->pages()->visible()->pluck('id');
|
||||||
}
|
}
|
||||||
|
|
||||||
$query = $this->activity->newQuery();
|
$query = $this->activity->newQuery();
|
||||||
|
@ -143,7 +146,9 @@ class ActivityService
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filters out similar activity.
|
* Filters out similar activity.
|
||||||
|
*
|
||||||
* @param Activity[] $activities
|
* @param Activity[] $activities
|
||||||
|
*
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
protected function filterSimilar(iterable $activities): array
|
protected function filterSimilar(iterable $activities): array
|
||||||
|
@ -185,7 +190,7 @@ class ActivityService
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$message = str_replace("%u", $username, $message);
|
$message = str_replace('%u', $username, $message);
|
||||||
$channel = config('logging.failed_login.channel');
|
$channel = config('logging.failed_login.channel');
|
||||||
Log::channel($channel)->warning($message);
|
Log::channel($channel)->warning($message);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
<?php namespace BookStack\Actions;
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Actions;
|
||||||
|
|
||||||
class ActivityType
|
class ActivityType
|
||||||
{
|
{
|
||||||
|
@ -48,4 +50,7 @@ class ActivityType
|
||||||
const AUTH_PASSWORD_RESET_UPDATE = 'auth_password_reset_update';
|
const AUTH_PASSWORD_RESET_UPDATE = 'auth_password_reset_update';
|
||||||
const AUTH_LOGIN = 'auth_login';
|
const AUTH_LOGIN = 'auth_login';
|
||||||
const AUTH_REGISTER = 'auth_register';
|
const AUTH_REGISTER = 'auth_register';
|
||||||
|
|
||||||
|
const MFA_SETUP_METHOD = 'mfa_setup_method';
|
||||||
|
const MFA_REMOVE_METHOD = 'mfa_remove_method';
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
<?php namespace BookStack\Actions;
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Actions;
|
||||||
|
|
||||||
use BookStack\Model;
|
use BookStack\Model;
|
||||||
use BookStack\Traits\HasCreatorAndUpdater;
|
use BookStack\Traits\HasCreatorAndUpdater;
|
||||||
|
@ -18,7 +20,7 @@ class Comment extends Model
|
||||||
protected $appends = ['created', 'updated'];
|
protected $appends = ['created', 'updated'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the entity that this comment belongs to
|
* Get the entity that this comment belongs to.
|
||||||
*/
|
*/
|
||||||
public function entity(): MorphTo
|
public function entity(): MorphTo
|
||||||
{
|
{
|
||||||
|
@ -35,6 +37,7 @@ class Comment extends Model
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get created date as a relative diff.
|
* Get created date as a relative diff.
|
||||||
|
*
|
||||||
* @return mixed
|
* @return mixed
|
||||||
*/
|
*/
|
||||||
public function getCreatedAttribute()
|
public function getCreatedAttribute()
|
||||||
|
@ -44,6 +47,7 @@ class Comment extends Model
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get updated date as a relative diff.
|
* Get updated date as a relative diff.
|
||||||
|
*
|
||||||
* @return mixed
|
* @return mixed
|
||||||
*/
|
*/
|
||||||
public function getUpdatedAttribute()
|
public function getUpdatedAttribute()
|
||||||
|
|
|
@ -1,21 +1,21 @@
|
||||||
<?php namespace BookStack\Actions;
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Actions;
|
||||||
|
|
||||||
use BookStack\Entities\Models\Entity;
|
use BookStack\Entities\Models\Entity;
|
||||||
use League\CommonMark\CommonMarkConverter;
|
|
||||||
use BookStack\Facades\Activity as ActivityService;
|
use BookStack\Facades\Activity as ActivityService;
|
||||||
|
use League\CommonMark\CommonMarkConverter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class CommentRepo
|
* Class CommentRepo.
|
||||||
*/
|
*/
|
||||||
class CommentRepo
|
class CommentRepo
|
||||||
{
|
{
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Comment $comment
|
* @var Comment
|
||||||
*/
|
*/
|
||||||
protected $comment;
|
protected $comment;
|
||||||
|
|
||||||
|
|
||||||
public function __construct(Comment $comment)
|
public function __construct(Comment $comment)
|
||||||
{
|
{
|
||||||
$this->comment = $comment;
|
$this->comment = $comment;
|
||||||
|
@ -46,6 +46,7 @@ class CommentRepo
|
||||||
|
|
||||||
$entity->comments()->save($comment);
|
$entity->comments()->save($comment);
|
||||||
ActivityService::addForEntity($entity, ActivityType::COMMENTED_ON);
|
ActivityService::addForEntity($entity, ActivityType::COMMENTED_ON);
|
||||||
|
|
||||||
return $comment;
|
return $comment;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,6 +59,7 @@ class CommentRepo
|
||||||
$comment->text = $text;
|
$comment->text = $text;
|
||||||
$comment->html = $this->commentToHtml($text);
|
$comment->html = $this->commentToHtml($text);
|
||||||
$comment->save();
|
$comment->save();
|
||||||
|
|
||||||
return $comment;
|
return $comment;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,8 +77,8 @@ class CommentRepo
|
||||||
public function commentToHtml(string $commentText): string
|
public function commentToHtml(string $commentText): string
|
||||||
{
|
{
|
||||||
$converter = new CommonMarkConverter([
|
$converter = new CommonMarkConverter([
|
||||||
'html_input' => 'strip',
|
'html_input' => 'strip',
|
||||||
'max_nesting_level' => 10,
|
'max_nesting_level' => 10,
|
||||||
'allow_unsafe_links' => false,
|
'allow_unsafe_links' => false,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -89,6 +91,7 @@ class CommentRepo
|
||||||
protected function getNextLocalId(Entity $entity): int
|
protected function getNextLocalId(Entity $entity): int
|
||||||
{
|
{
|
||||||
$comments = $entity->comments(false)->orderBy('local_id', 'desc')->first();
|
$comments = $entity->comments(false)->orderBy('local_id', 'desc')->first();
|
||||||
|
|
||||||
return ($comments->local_id ?? 0) + 1;
|
return ($comments->local_id ?? 0) + 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
<?php namespace BookStack\Actions;
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Actions;
|
||||||
|
|
||||||
use BookStack\Model;
|
use BookStack\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
<?php namespace BookStack\Actions;
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Actions;
|
||||||
|
|
||||||
use BookStack\Model;
|
use BookStack\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||||
|
@ -9,7 +11,7 @@ class Tag extends Model
|
||||||
protected $hidden = ['id', 'entity_id', 'entity_type', 'created_at', 'updated_at'];
|
protected $hidden = ['id', 'entity_id', 'entity_type', 'created_at', 'updated_at'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the entity that this tag belongs to
|
* Get the entity that this tag belongs to.
|
||||||
*/
|
*/
|
||||||
public function entity(): MorphTo
|
public function entity(): MorphTo
|
||||||
{
|
{
|
||||||
|
@ -21,7 +23,7 @@ class Tag extends Model
|
||||||
*/
|
*/
|
||||||
public function nameUrl(): string
|
public function nameUrl(): string
|
||||||
{
|
{
|
||||||
return url('/search?term=%5B' . urlencode($this->name) .'%5D');
|
return url('/search?term=%5B' . urlencode($this->name) . '%5D');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -29,6 +31,6 @@ class Tag extends Model
|
||||||
*/
|
*/
|
||||||
public function valueUrl(): string
|
public function valueUrl(): string
|
||||||
{
|
{
|
||||||
return url('/search?term=%5B' . urlencode($this->name) .'%3D' . urlencode($this->value) . '%5D');
|
return url('/search?term=%5B' . urlencode($this->name) . '%3D' . urlencode($this->value) . '%5D');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
<?php namespace BookStack\Actions;
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Actions;
|
||||||
|
|
||||||
use BookStack\Auth\Permissions\PermissionService;
|
use BookStack\Auth\Permissions\PermissionService;
|
||||||
use BookStack\Entities\Models\Entity;
|
use BookStack\Entities\Models\Entity;
|
||||||
|
@ -7,7 +9,6 @@ use Illuminate\Support\Collection;
|
||||||
|
|
||||||
class TagRepo
|
class TagRepo
|
||||||
{
|
{
|
||||||
|
|
||||||
protected $tag;
|
protected $tag;
|
||||||
protected $permissionService;
|
protected $permissionService;
|
||||||
|
|
||||||
|
@ -37,6 +38,7 @@ class TagRepo
|
||||||
}
|
}
|
||||||
|
|
||||||
$query = $this->permissionService->filterRestrictedEntityRelations($query, 'tags', 'entity_id', 'entity_type');
|
$query = $this->permissionService->filterRestrictedEntityRelations($query, 'tags', 'entity_id', 'entity_type');
|
||||||
|
|
||||||
return $query->get(['name'])->pluck('name');
|
return $query->get(['name'])->pluck('name');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,11 +64,12 @@ class TagRepo
|
||||||
}
|
}
|
||||||
|
|
||||||
$query = $this->permissionService->filterRestrictedEntityRelations($query, 'tags', 'entity_id', 'entity_type');
|
$query = $this->permissionService->filterRestrictedEntityRelations($query, 'tags', 'entity_id', 'entity_type');
|
||||||
|
|
||||||
return $query->get(['value'])->pluck('value');
|
return $query->get(['value'])->pluck('value');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save an array of tags to an entity
|
* Save an array of tags to an entity.
|
||||||
*/
|
*/
|
||||||
public function saveTagsToEntity(Entity $entity, array $tags = []): iterable
|
public function saveTagsToEntity(Entity $entity, array $tags = []): iterable
|
||||||
{
|
{
|
||||||
|
@ -89,6 +92,7 @@ class TagRepo
|
||||||
{
|
{
|
||||||
$name = trim($input['name']);
|
$name = trim($input['name']);
|
||||||
$value = isset($input['value']) ? trim($input['value']) : '';
|
$value = isset($input['value']) ? trim($input['value']) : '';
|
||||||
|
|
||||||
return $this->tag->newInstance(['name' => $name, 'value' => $value]);
|
return $this->tag->newInstance(['name' => $name, 'value' => $value]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
<?php namespace BookStack\Actions;
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Actions;
|
||||||
|
|
||||||
use BookStack\Interfaces\Viewable;
|
use BookStack\Interfaces\Viewable;
|
||||||
use BookStack\Model;
|
use BookStack\Model;
|
||||||
|
@ -16,7 +18,6 @@ use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||||
*/
|
*/
|
||||||
class View extends Model
|
class View extends Model
|
||||||
{
|
{
|
||||||
|
|
||||||
protected $fillable = ['user_id', 'views'];
|
protected $fillable = ['user_id', 'views'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
<?php namespace BookStack\Api;
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Api;
|
||||||
|
|
||||||
use BookStack\Http\Controllers\Api\ApiController;
|
use BookStack\Http\Controllers\Api\ApiController;
|
||||||
use Illuminate\Contracts\Container\BindingResolutionException;
|
use Illuminate\Contracts\Container\BindingResolutionException;
|
||||||
|
@ -12,7 +14,6 @@ use ReflectionMethod;
|
||||||
|
|
||||||
class ApiDocsGenerator
|
class ApiDocsGenerator
|
||||||
{
|
{
|
||||||
|
|
||||||
protected $reflectionClasses = [];
|
protected $reflectionClasses = [];
|
||||||
protected $controllerClasses = [];
|
protected $controllerClasses = [];
|
||||||
|
|
||||||
|
@ -30,6 +31,7 @@ class ApiDocsGenerator
|
||||||
$docs = (new static())->generate();
|
$docs = (new static())->generate();
|
||||||
Cache::put($cacheKey, $docs, 60 * 24);
|
Cache::put($cacheKey, $docs, 60 * 24);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $docs;
|
return $docs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,6 +44,7 @@ class ApiDocsGenerator
|
||||||
$apiRoutes = $this->loadDetailsFromControllers($apiRoutes);
|
$apiRoutes = $this->loadDetailsFromControllers($apiRoutes);
|
||||||
$apiRoutes = $this->loadDetailsFromFiles($apiRoutes);
|
$apiRoutes = $this->loadDetailsFromFiles($apiRoutes);
|
||||||
$apiRoutes = $apiRoutes->groupBy('base_model');
|
$apiRoutes = $apiRoutes->groupBy('base_model');
|
||||||
|
|
||||||
return $apiRoutes;
|
return $apiRoutes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,6 +60,7 @@ class ApiDocsGenerator
|
||||||
$exampleContent = file_exists($exampleFile) ? file_get_contents($exampleFile) : null;
|
$exampleContent = file_exists($exampleFile) ? file_get_contents($exampleFile) : null;
|
||||||
$route["example_{$exampleType}"] = $exampleContent;
|
$route["example_{$exampleType}"] = $exampleContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $route;
|
return $route;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -71,12 +75,14 @@ class ApiDocsGenerator
|
||||||
$comment = $method->getDocComment();
|
$comment = $method->getDocComment();
|
||||||
$route['description'] = $comment ? $this->parseDescriptionFromMethodComment($comment) : null;
|
$route['description'] = $comment ? $this->parseDescriptionFromMethodComment($comment) : null;
|
||||||
$route['body_params'] = $this->getBodyParamsFromClass($route['controller'], $route['controller_method']);
|
$route['body_params'] = $this->getBodyParamsFromClass($route['controller'], $route['controller_method']);
|
||||||
|
|
||||||
return $route;
|
return $route;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load body params and their rules by inspecting the given class and method name.
|
* Load body params and their rules by inspecting the given class and method name.
|
||||||
|
*
|
||||||
* @throws BindingResolutionException
|
* @throws BindingResolutionException
|
||||||
*/
|
*/
|
||||||
protected function getBodyParamsFromClass(string $className, string $methodName): ?array
|
protected function getBodyParamsFromClass(string $className, string $methodName): ?array
|
||||||
|
@ -92,6 +98,7 @@ class ApiDocsGenerator
|
||||||
foreach ($rules as $param => $ruleString) {
|
foreach ($rules as $param => $ruleString) {
|
||||||
$rules[$param] = explode('|', $ruleString);
|
$rules[$param] = explode('|', $ruleString);
|
||||||
}
|
}
|
||||||
|
|
||||||
return count($rules) > 0 ? $rules : null;
|
return count($rules) > 0 ? $rules : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,11 +109,13 @@ class ApiDocsGenerator
|
||||||
{
|
{
|
||||||
$matches = [];
|
$matches = [];
|
||||||
preg_match_all('/^\s*?\*\s((?![@\s]).*?)$/m', $comment, $matches);
|
preg_match_all('/^\s*?\*\s((?![@\s]).*?)$/m', $comment, $matches);
|
||||||
|
|
||||||
return implode(' ', $matches[1] ?? []);
|
return implode(' ', $matches[1] ?? []);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a reflection method from the given class name and method name.
|
* Get a reflection method from the given class name and method name.
|
||||||
|
*
|
||||||
* @throws ReflectionException
|
* @throws ReflectionException
|
||||||
*/
|
*/
|
||||||
protected function getReflectionMethod(string $className, string $methodName): ReflectionMethod
|
protected function getReflectionMethod(string $className, string $methodName): ReflectionMethod
|
||||||
|
@ -131,14 +140,15 @@ class ApiDocsGenerator
|
||||||
[$controller, $controllerMethod] = explode('@', $route->action['uses']);
|
[$controller, $controllerMethod] = explode('@', $route->action['uses']);
|
||||||
$baseModelName = explode('.', explode('/', $route->uri)[1])[0];
|
$baseModelName = explode('.', explode('/', $route->uri)[1])[0];
|
||||||
$shortName = $baseModelName . '-' . $controllerMethod;
|
$shortName = $baseModelName . '-' . $controllerMethod;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'name' => $shortName,
|
'name' => $shortName,
|
||||||
'uri' => $route->uri,
|
'uri' => $route->uri,
|
||||||
'method' => $route->methods[0],
|
'method' => $route->methods[0],
|
||||||
'controller' => $controller,
|
'controller' => $controller,
|
||||||
'controller_method' => $controllerMethod,
|
'controller_method' => $controllerMethod,
|
||||||
'controller_method_kebab' => Str::kebab($controllerMethod),
|
'controller_method_kebab' => Str::kebab($controllerMethod),
|
||||||
'base_model' => $baseModelName,
|
'base_model' => $baseModelName,
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
<?php namespace BookStack\Api;
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Api;
|
||||||
|
|
||||||
use BookStack\Auth\User;
|
use BookStack\Auth\User;
|
||||||
use BookStack\Interfaces\Loggable;
|
use BookStack\Interfaces\Loggable;
|
||||||
|
@ -7,19 +9,20 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class ApiToken
|
* Class ApiToken.
|
||||||
* @property int $id
|
*
|
||||||
|
* @property int $id
|
||||||
* @property string $token_id
|
* @property string $token_id
|
||||||
* @property string $secret
|
* @property string $secret
|
||||||
* @property string $name
|
* @property string $name
|
||||||
* @property Carbon $expires_at
|
* @property Carbon $expires_at
|
||||||
* @property User $user
|
* @property User $user
|
||||||
*/
|
*/
|
||||||
class ApiToken extends Model implements Loggable
|
class ApiToken extends Model implements Loggable
|
||||||
{
|
{
|
||||||
protected $fillable = ['name', 'expires_at'];
|
protected $fillable = ['name', 'expires_at'];
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'expires_at' => 'date:Y-m-d'
|
'expires_at' => 'date:Y-m-d',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
namespace BookStack\Api;
|
namespace BookStack\Api;
|
||||||
|
|
||||||
|
use BookStack\Auth\Access\LoginService;
|
||||||
use BookStack\Exceptions\ApiAuthException;
|
use BookStack\Exceptions\ApiAuthException;
|
||||||
use Illuminate\Auth\GuardHelpers;
|
use Illuminate\Auth\GuardHelpers;
|
||||||
use Illuminate\Contracts\Auth\Authenticatable;
|
use Illuminate\Contracts\Auth\Authenticatable;
|
||||||
|
@ -12,7 +13,6 @@ use Symfony\Component\HttpFoundation\Request;
|
||||||
|
|
||||||
class ApiTokenGuard implements Guard
|
class ApiTokenGuard implements Guard
|
||||||
{
|
{
|
||||||
|
|
||||||
use GuardHelpers;
|
use GuardHelpers;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -20,9 +20,14 @@ class ApiTokenGuard implements Guard
|
||||||
*/
|
*/
|
||||||
protected $request;
|
protected $request;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var LoginService
|
||||||
|
*/
|
||||||
|
protected $loginService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The last auth exception thrown in this request.
|
* The last auth exception thrown in this request.
|
||||||
|
*
|
||||||
* @var ApiAuthException
|
* @var ApiAuthException
|
||||||
*/
|
*/
|
||||||
protected $lastAuthException;
|
protected $lastAuthException;
|
||||||
|
@ -30,9 +35,10 @@ class ApiTokenGuard implements Guard
|
||||||
/**
|
/**
|
||||||
* ApiTokenGuard constructor.
|
* ApiTokenGuard constructor.
|
||||||
*/
|
*/
|
||||||
public function __construct(Request $request)
|
public function __construct(Request $request, LoginService $loginService)
|
||||||
{
|
{
|
||||||
$this->request = $request;
|
$this->request = $request;
|
||||||
|
$this->loginService = $loginService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -47,6 +53,7 @@ class ApiTokenGuard implements Guard
|
||||||
}
|
}
|
||||||
|
|
||||||
$user = null;
|
$user = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$user = $this->getAuthorisedUserFromRequest();
|
$user = $this->getAuthorisedUserFromRequest();
|
||||||
} catch (ApiAuthException $exception) {
|
} catch (ApiAuthException $exception) {
|
||||||
|
@ -54,19 +61,20 @@ class ApiTokenGuard implements Guard
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->user = $user;
|
$this->user = $user;
|
||||||
|
|
||||||
return $user;
|
return $user;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if current user is authenticated. If not, throw an exception.
|
* Determine if current user is authenticated. If not, throw an exception.
|
||||||
*
|
*
|
||||||
* @return \Illuminate\Contracts\Auth\Authenticatable
|
|
||||||
*
|
|
||||||
* @throws ApiAuthException
|
* @throws ApiAuthException
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Contracts\Auth\Authenticatable
|
||||||
*/
|
*/
|
||||||
public function authenticate()
|
public function authenticate()
|
||||||
{
|
{
|
||||||
if (! is_null($user = $this->user())) {
|
if (!is_null($user = $this->user())) {
|
||||||
return $user;
|
return $user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,6 +87,7 @@ class ApiTokenGuard implements Guard
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check the API token in the request and fetch a valid authorised user.
|
* Check the API token in the request and fetch a valid authorised user.
|
||||||
|
*
|
||||||
* @throws ApiAuthException
|
* @throws ApiAuthException
|
||||||
*/
|
*/
|
||||||
protected function getAuthorisedUserFromRequest(): Authenticatable
|
protected function getAuthorisedUserFromRequest(): Authenticatable
|
||||||
|
@ -93,11 +102,16 @@ class ApiTokenGuard implements Guard
|
||||||
|
|
||||||
$this->validateToken($token, $secret);
|
$this->validateToken($token, $secret);
|
||||||
|
|
||||||
|
if ($this->loginService->awaitingEmailConfirmation($token->user)) {
|
||||||
|
throw new ApiAuthException(trans('errors.email_confirmation_awaiting'));
|
||||||
|
}
|
||||||
|
|
||||||
return $token->user;
|
return $token->user;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate the format of the token header value string.
|
* Validate the format of the token header value string.
|
||||||
|
*
|
||||||
* @throws ApiAuthException
|
* @throws ApiAuthException
|
||||||
*/
|
*/
|
||||||
protected function validateTokenHeaderValue(string $authToken): void
|
protected function validateTokenHeaderValue(string $authToken): void
|
||||||
|
@ -114,6 +128,7 @@ class ApiTokenGuard implements Guard
|
||||||
/**
|
/**
|
||||||
* Validate the given secret against the given token and ensure the token
|
* Validate the given secret against the given token and ensure the token
|
||||||
* currently has access to the instance API.
|
* currently has access to the instance API.
|
||||||
|
*
|
||||||
* @throws ApiAuthException
|
* @throws ApiAuthException
|
||||||
*/
|
*/
|
||||||
protected function validateToken(?ApiToken $token, string $secret): void
|
protected function validateToken(?ApiToken $token, string $secret): void
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
<?php namespace BookStack\Api;
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Api;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Eloquent\Collection;
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
|
@ -6,7 +8,6 @@ use Illuminate\Http\Request;
|
||||||
|
|
||||||
class ListingResponseBuilder
|
class ListingResponseBuilder
|
||||||
{
|
{
|
||||||
|
|
||||||
protected $query;
|
protected $query;
|
||||||
protected $request;
|
protected $request;
|
||||||
protected $fields;
|
protected $fields;
|
||||||
|
@ -18,7 +19,7 @@ class ListingResponseBuilder
|
||||||
'lt' => '<',
|
'lt' => '<',
|
||||||
'gte' => '>=',
|
'gte' => '>=',
|
||||||
'lte' => '<=',
|
'lte' => '<=',
|
||||||
'like' => 'like'
|
'like' => 'like',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -42,7 +43,7 @@ class ListingResponseBuilder
|
||||||
$data = $this->fetchData($filteredQuery);
|
$data = $this->fetchData($filteredQuery);
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'data' => $data,
|
'data' => $data,
|
||||||
'total' => $total,
|
'total' => $total,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
@ -54,6 +55,7 @@ class ListingResponseBuilder
|
||||||
{
|
{
|
||||||
$query = $this->countAndOffsetQuery($query);
|
$query = $this->countAndOffsetQuery($query);
|
||||||
$query = $this->sortQuery($query);
|
$query = $this->sortQuery($query);
|
||||||
|
|
||||||
return $query->get($this->fields);
|
return $query->get($this->fields);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,6 +97,7 @@ class ListingResponseBuilder
|
||||||
}
|
}
|
||||||
|
|
||||||
$queryOperator = $this->filterOperators[$filterOperator];
|
$queryOperator = $this->filterOperators[$filterOperator];
|
||||||
|
|
||||||
return [$field, $queryOperator, $value];
|
return [$field, $queryOperator, $value];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,11 +4,11 @@ namespace BookStack;
|
||||||
|
|
||||||
class Application extends \Illuminate\Foundation\Application
|
class Application extends \Illuminate\Foundation\Application
|
||||||
{
|
{
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the path to the application configuration files.
|
* Get the path to the application configuration files.
|
||||||
*
|
*
|
||||||
* @param string $path Optionally, a path to append to the config path
|
* @param string $path Optionally, a path to append to the config path
|
||||||
|
*
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
public function configPath($path = '')
|
public function configPath($path = '')
|
||||||
|
@ -18,6 +18,6 @@ class Application extends \Illuminate\Foundation\Application
|
||||||
. 'app'
|
. 'app'
|
||||||
. DIRECTORY_SEPARATOR
|
. DIRECTORY_SEPARATOR
|
||||||
. 'Config'
|
. 'Config'
|
||||||
. ($path ? DIRECTORY_SEPARATOR.$path : $path);
|
. ($path ? DIRECTORY_SEPARATOR . $path : $path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
<?php namespace BookStack\Auth\Access;
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Auth\Access;
|
||||||
|
|
||||||
use BookStack\Auth\User;
|
use BookStack\Auth\User;
|
||||||
use BookStack\Exceptions\ConfirmationEmailException;
|
use BookStack\Exceptions\ConfirmationEmailException;
|
||||||
|
@ -12,7 +14,7 @@ class EmailConfirmationService extends UserTokenService
|
||||||
/**
|
/**
|
||||||
* Create new confirmation for a user,
|
* Create new confirmation for a user,
|
||||||
* Also removes any existing old ones.
|
* Also removes any existing old ones.
|
||||||
* @param User $user
|
*
|
||||||
* @throws ConfirmationEmailException
|
* @throws ConfirmationEmailException
|
||||||
*/
|
*/
|
||||||
public function sendConfirmation(User $user)
|
public function sendConfirmation(User $user)
|
||||||
|
@ -29,9 +31,8 @@ class EmailConfirmationService extends UserTokenService
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if confirmation is required in this instance.
|
* Check if confirmation is required in this instance.
|
||||||
* @return bool
|
|
||||||
*/
|
*/
|
||||||
public function confirmationRequired() : bool
|
public function confirmationRequired(): bool
|
||||||
{
|
{
|
||||||
return setting('registration-confirmation')
|
return setting('registration-confirmation')
|
||||||
|| setting('registration-restrict');
|
|| setting('registration-restrict');
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
<?php namespace BookStack\Auth\Access;
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Auth\Access;
|
||||||
|
|
||||||
use BookStack\Auth\Role;
|
use BookStack\Auth\Role;
|
||||||
use BookStack\Auth\User;
|
use BookStack\Auth\User;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
|
|
||||||
class ExternalAuthService
|
class ExternalAuthService
|
||||||
{
|
{
|
||||||
|
@ -19,6 +19,7 @@ class ExternalAuthService
|
||||||
}
|
}
|
||||||
|
|
||||||
$roleName = str_replace(' ', '-', trim(strtolower($role->display_name)));
|
$roleName = str_replace(' ', '-', trim(strtolower($role->display_name)));
|
||||||
|
|
||||||
return in_array($roleName, $groupNames);
|
return in_array($roleName, $groupNames);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,7 +58,7 @@ class ExternalAuthService
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sync the groups to the user roles for the current user
|
* Sync the groups to the user roles for the current user.
|
||||||
*/
|
*/
|
||||||
public function syncWithGroups(User $user, array $userGroups): void
|
public function syncWithGroups(User $user, array $userGroups): void
|
||||||
{
|
{
|
||||||
|
|
|
@ -7,7 +7,6 @@ use Illuminate\Contracts\Auth\UserProvider;
|
||||||
|
|
||||||
class ExternalBaseUserProvider implements UserProvider
|
class ExternalBaseUserProvider implements UserProvider
|
||||||
{
|
{
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The user model.
|
* The user model.
|
||||||
*
|
*
|
||||||
|
@ -17,7 +16,8 @@ class ExternalBaseUserProvider implements UserProvider
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* LdapUserProvider constructor.
|
* LdapUserProvider constructor.
|
||||||
* @param $model
|
*
|
||||||
|
* @param $model
|
||||||
*/
|
*/
|
||||||
public function __construct(string $model)
|
public function __construct(string $model)
|
||||||
{
|
{
|
||||||
|
@ -32,13 +32,15 @@ class ExternalBaseUserProvider implements UserProvider
|
||||||
public function createModel()
|
public function createModel()
|
||||||
{
|
{
|
||||||
$class = '\\' . ltrim($this->model, '\\');
|
$class = '\\' . ltrim($this->model, '\\');
|
||||||
return new $class;
|
|
||||||
|
return new $class();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve a user by their unique identifier.
|
* Retrieve a user by their unique identifier.
|
||||||
*
|
*
|
||||||
* @param mixed $identifier
|
* @param mixed $identifier
|
||||||
|
*
|
||||||
* @return \Illuminate\Contracts\Auth\Authenticatable|null
|
* @return \Illuminate\Contracts\Auth\Authenticatable|null
|
||||||
*/
|
*/
|
||||||
public function retrieveById($identifier)
|
public function retrieveById($identifier)
|
||||||
|
@ -49,8 +51,9 @@ class ExternalBaseUserProvider implements UserProvider
|
||||||
/**
|
/**
|
||||||
* Retrieve a user by their unique identifier and "remember me" token.
|
* Retrieve a user by their unique identifier and "remember me" token.
|
||||||
*
|
*
|
||||||
* @param mixed $identifier
|
* @param mixed $identifier
|
||||||
* @param string $token
|
* @param string $token
|
||||||
|
*
|
||||||
* @return \Illuminate\Contracts\Auth\Authenticatable|null
|
* @return \Illuminate\Contracts\Auth\Authenticatable|null
|
||||||
*/
|
*/
|
||||||
public function retrieveByToken($identifier, $token)
|
public function retrieveByToken($identifier, $token)
|
||||||
|
@ -58,12 +61,12 @@ class ExternalBaseUserProvider implements UserProvider
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the "remember me" token for the given user in storage.
|
* Update the "remember me" token for the given user in storage.
|
||||||
*
|
*
|
||||||
* @param \Illuminate\Contracts\Auth\Authenticatable $user
|
* @param \Illuminate\Contracts\Auth\Authenticatable $user
|
||||||
* @param string $token
|
* @param string $token
|
||||||
|
*
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function updateRememberToken(Authenticatable $user, $token)
|
public function updateRememberToken(Authenticatable $user, $token)
|
||||||
|
@ -74,13 +77,15 @@ class ExternalBaseUserProvider implements UserProvider
|
||||||
/**
|
/**
|
||||||
* Retrieve a user by the given credentials.
|
* Retrieve a user by the given credentials.
|
||||||
*
|
*
|
||||||
* @param array $credentials
|
* @param array $credentials
|
||||||
|
*
|
||||||
* @return \Illuminate\Contracts\Auth\Authenticatable|null
|
* @return \Illuminate\Contracts\Auth\Authenticatable|null
|
||||||
*/
|
*/
|
||||||
public function retrieveByCredentials(array $credentials)
|
public function retrieveByCredentials(array $credentials)
|
||||||
{
|
{
|
||||||
// Search current user base by looking up a uid
|
// Search current user base by looking up a uid
|
||||||
$model = $this->createModel();
|
$model = $this->createModel();
|
||||||
|
|
||||||
return $model->newQuery()
|
return $model->newQuery()
|
||||||
->where('external_auth_id', $credentials['external_auth_id'])
|
->where('external_auth_id', $credentials['external_auth_id'])
|
||||||
->first();
|
->first();
|
||||||
|
@ -89,8 +94,9 @@ class ExternalBaseUserProvider implements UserProvider
|
||||||
/**
|
/**
|
||||||
* Validate a user against the given credentials.
|
* Validate a user against the given credentials.
|
||||||
*
|
*
|
||||||
* @param \Illuminate\Contracts\Auth\Authenticatable $user
|
* @param \Illuminate\Contracts\Auth\Authenticatable $user
|
||||||
* @param array $credentials
|
* @param array $credentials
|
||||||
|
*
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function validateCredentials(Authenticatable $user, array $credentials)
|
public function validateCredentials(Authenticatable $user, array $credentials)
|
||||||
|
|
|
@ -84,7 +84,7 @@ class ExternalBaseSessionGuard implements StatefulGuard
|
||||||
// If we've already retrieved the user for the current request we can just
|
// If we've already retrieved the user for the current request we can just
|
||||||
// return it back immediately. We do not want to fetch the user data on
|
// return it back immediately. We do not want to fetch the user data on
|
||||||
// every call to this method because that would be tremendously slow.
|
// every call to this method because that would be tremendously slow.
|
||||||
if (! is_null($this->user)) {
|
if (!is_null($this->user)) {
|
||||||
return $this->user;
|
return $this->user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,7 +92,7 @@ class ExternalBaseSessionGuard implements StatefulGuard
|
||||||
|
|
||||||
// First we will try to load the user using the
|
// First we will try to load the user using the
|
||||||
// identifier in the session if one exists.
|
// identifier in the session if one exists.
|
||||||
if (! is_null($id)) {
|
if (!is_null($id)) {
|
||||||
$this->user = $this->provider->retrieveById($id);
|
$this->user = $this->provider->retrieveById($id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,7 +118,8 @@ class ExternalBaseSessionGuard implements StatefulGuard
|
||||||
/**
|
/**
|
||||||
* Log a user into the application without sessions or cookies.
|
* Log a user into the application without sessions or cookies.
|
||||||
*
|
*
|
||||||
* @param array $credentials
|
* @param array $credentials
|
||||||
|
*
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function once(array $credentials = [])
|
public function once(array $credentials = [])
|
||||||
|
@ -135,12 +136,13 @@ class ExternalBaseSessionGuard implements StatefulGuard
|
||||||
/**
|
/**
|
||||||
* Log the given user ID into the application without sessions or cookies.
|
* Log the given user ID into the application without sessions or cookies.
|
||||||
*
|
*
|
||||||
* @param mixed $id
|
* @param mixed $id
|
||||||
|
*
|
||||||
* @return \Illuminate\Contracts\Auth\Authenticatable|false
|
* @return \Illuminate\Contracts\Auth\Authenticatable|false
|
||||||
*/
|
*/
|
||||||
public function onceUsingId($id)
|
public function onceUsingId($id)
|
||||||
{
|
{
|
||||||
if (! is_null($user = $this->provider->retrieveById($id))) {
|
if (!is_null($user = $this->provider->retrieveById($id))) {
|
||||||
$this->setUser($user);
|
$this->setUser($user);
|
||||||
|
|
||||||
return $user;
|
return $user;
|
||||||
|
@ -152,7 +154,8 @@ class ExternalBaseSessionGuard implements StatefulGuard
|
||||||
/**
|
/**
|
||||||
* Validate a user's credentials.
|
* Validate a user's credentials.
|
||||||
*
|
*
|
||||||
* @param array $credentials
|
* @param array $credentials
|
||||||
|
*
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function validate(array $credentials = [])
|
public function validate(array $credentials = [])
|
||||||
|
@ -160,12 +163,12 @@ class ExternalBaseSessionGuard implements StatefulGuard
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempt to authenticate a user using the given credentials.
|
* Attempt to authenticate a user using the given credentials.
|
||||||
*
|
*
|
||||||
* @param array $credentials
|
* @param array $credentials
|
||||||
* @param bool $remember
|
* @param bool $remember
|
||||||
|
*
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function attempt(array $credentials = [], $remember = false)
|
public function attempt(array $credentials = [], $remember = false)
|
||||||
|
@ -176,26 +179,24 @@ class ExternalBaseSessionGuard implements StatefulGuard
|
||||||
/**
|
/**
|
||||||
* Log the given user ID into the application.
|
* Log the given user ID into the application.
|
||||||
*
|
*
|
||||||
* @param mixed $id
|
* @param mixed $id
|
||||||
* @param bool $remember
|
* @param bool $remember
|
||||||
|
*
|
||||||
* @return \Illuminate\Contracts\Auth\Authenticatable|false
|
* @return \Illuminate\Contracts\Auth\Authenticatable|false
|
||||||
*/
|
*/
|
||||||
public function loginUsingId($id, $remember = false)
|
public function loginUsingId($id, $remember = false)
|
||||||
{
|
{
|
||||||
if (! is_null($user = $this->provider->retrieveById($id))) {
|
// Always return false as to disable this method,
|
||||||
$this->login($user, $remember);
|
// Logins should route through LoginService.
|
||||||
|
|
||||||
return $user;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log a user into the application.
|
* Log a user into the application.
|
||||||
*
|
*
|
||||||
* @param \Illuminate\Contracts\Auth\Authenticatable $user
|
* @param \Illuminate\Contracts\Auth\Authenticatable $user
|
||||||
* @param bool $remember
|
* @param bool $remember
|
||||||
|
*
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function login(AuthenticatableContract $user, $remember = false)
|
public function login(AuthenticatableContract $user, $remember = false)
|
||||||
|
@ -208,7 +209,8 @@ class ExternalBaseSessionGuard implements StatefulGuard
|
||||||
/**
|
/**
|
||||||
* Update the session with the given ID.
|
* Update the session with the given ID.
|
||||||
*
|
*
|
||||||
* @param string $id
|
* @param string $id
|
||||||
|
*
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
protected function updateSession($id)
|
protected function updateSession($id)
|
||||||
|
@ -262,7 +264,7 @@ class ExternalBaseSessionGuard implements StatefulGuard
|
||||||
*/
|
*/
|
||||||
public function getName()
|
public function getName()
|
||||||
{
|
{
|
||||||
return 'login_'.$this->name.'_'.sha1(static::class);
|
return 'login_' . $this->name . '_' . sha1(static::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -288,7 +290,8 @@ class ExternalBaseSessionGuard implements StatefulGuard
|
||||||
/**
|
/**
|
||||||
* Set the current user.
|
* Set the current user.
|
||||||
*
|
*
|
||||||
* @param \Illuminate\Contracts\Auth\Authenticatable $user
|
* @param \Illuminate\Contracts\Auth\Authenticatable $user
|
||||||
|
*
|
||||||
* @return $this
|
* @return $this
|
||||||
*/
|
*/
|
||||||
public function setUser(AuthenticatableContract $user)
|
public function setUser(AuthenticatableContract $user)
|
||||||
|
|
|
@ -6,8 +6,8 @@ use BookStack\Auth\Access\LdapService;
|
||||||
use BookStack\Auth\Access\RegistrationService;
|
use BookStack\Auth\Access\RegistrationService;
|
||||||
use BookStack\Auth\User;
|
use BookStack\Auth\User;
|
||||||
use BookStack\Exceptions\LdapException;
|
use BookStack\Exceptions\LdapException;
|
||||||
use BookStack\Exceptions\LoginAttemptException;
|
|
||||||
use BookStack\Exceptions\LoginAttemptEmailNeededException;
|
use BookStack\Exceptions\LoginAttemptEmailNeededException;
|
||||||
|
use BookStack\Exceptions\LoginAttemptException;
|
||||||
use BookStack\Exceptions\UserRegistrationException;
|
use BookStack\Exceptions\UserRegistrationException;
|
||||||
use Illuminate\Contracts\Auth\UserProvider;
|
use Illuminate\Contracts\Auth\UserProvider;
|
||||||
use Illuminate\Contracts\Session\Session;
|
use Illuminate\Contracts\Session\Session;
|
||||||
|
@ -15,7 +15,6 @@ use Illuminate\Support\Str;
|
||||||
|
|
||||||
class LdapSessionGuard extends ExternalBaseSessionGuard
|
class LdapSessionGuard extends ExternalBaseSessionGuard
|
||||||
{
|
{
|
||||||
|
|
||||||
protected $ldapService;
|
protected $ldapService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -36,8 +35,10 @@ class LdapSessionGuard extends ExternalBaseSessionGuard
|
||||||
* Validate a user's credentials.
|
* Validate a user's credentials.
|
||||||
*
|
*
|
||||||
* @param array $credentials
|
* @param array $credentials
|
||||||
* @return bool
|
*
|
||||||
* @throws LdapException
|
* @throws LdapException
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function validate(array $credentials = [])
|
public function validate(array $credentials = [])
|
||||||
{
|
{
|
||||||
|
@ -45,7 +46,7 @@ class LdapSessionGuard extends ExternalBaseSessionGuard
|
||||||
|
|
||||||
if (isset($userDetails['uid'])) {
|
if (isset($userDetails['uid'])) {
|
||||||
$this->lastAttempted = $this->provider->retrieveByCredentials([
|
$this->lastAttempted = $this->provider->retrieveByCredentials([
|
||||||
'external_auth_id' => $userDetails['uid']
|
'external_auth_id' => $userDetails['uid'],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,10 +57,12 @@ class LdapSessionGuard extends ExternalBaseSessionGuard
|
||||||
* Attempt to authenticate a user using the given credentials.
|
* Attempt to authenticate a user using the given credentials.
|
||||||
*
|
*
|
||||||
* @param array $credentials
|
* @param array $credentials
|
||||||
* @param bool $remember
|
* @param bool $remember
|
||||||
* @return bool
|
*
|
||||||
* @throws LoginAttemptException
|
* @throws LoginAttemptException
|
||||||
* @throws LdapException
|
* @throws LdapException
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function attempt(array $credentials = [], $remember = false)
|
public function attempt(array $credentials = [], $remember = false)
|
||||||
{
|
{
|
||||||
|
@ -69,7 +72,7 @@ class LdapSessionGuard extends ExternalBaseSessionGuard
|
||||||
$user = null;
|
$user = null;
|
||||||
if (isset($userDetails['uid'])) {
|
if (isset($userDetails['uid'])) {
|
||||||
$this->lastAttempted = $user = $this->provider->retrieveByCredentials([
|
$this->lastAttempted = $user = $this->provider->retrieveByCredentials([
|
||||||
'external_auth_id' => $userDetails['uid']
|
'external_auth_id' => $userDetails['uid'],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,11 +99,13 @@ class LdapSessionGuard extends ExternalBaseSessionGuard
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->login($user, $remember);
|
$this->login($user, $remember);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new user from the given ldap credentials and login credentials
|
* Create a new user from the given ldap credentials and login credentials.
|
||||||
|
*
|
||||||
* @throws LoginAttemptEmailNeededException
|
* @throws LoginAttemptEmailNeededException
|
||||||
* @throws LoginAttemptException
|
* @throws LoginAttemptException
|
||||||
* @throws UserRegistrationException
|
* @throws UserRegistrationException
|
||||||
|
@ -114,14 +119,15 @@ class LdapSessionGuard extends ExternalBaseSessionGuard
|
||||||
}
|
}
|
||||||
|
|
||||||
$details = [
|
$details = [
|
||||||
'name' => $ldapUserDetails['name'],
|
'name' => $ldapUserDetails['name'],
|
||||||
'email' => $ldapUserDetails['email'] ?: $credentials['email'],
|
'email' => $ldapUserDetails['email'] ?: $credentials['email'],
|
||||||
'external_auth_id' => $ldapUserDetails['uid'],
|
'external_auth_id' => $ldapUserDetails['uid'],
|
||||||
'password' => Str::random(32),
|
'password' => Str::random(32),
|
||||||
];
|
];
|
||||||
|
|
||||||
$user = $this->registrationService->registerUser($details, null, false);
|
$user = $this->registrationService->registerUser($details, null, false);
|
||||||
$this->ldapService->saveAndAttachAvatar($user, $ldapUserDetails);
|
$this->ldapService->saveAndAttachAvatar($user, $ldapUserDetails);
|
||||||
|
|
||||||
return $user;
|
return $user;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
namespace BookStack\Auth\Access\Guards;
|
namespace BookStack\Auth\Access\Guards;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saml2 Session Guard
|
* Saml2 Session Guard.
|
||||||
*
|
*
|
||||||
* The saml2 login process is async in nature meaning it does not fit very well
|
* The saml2 login process is async in nature meaning it does not fit very well
|
||||||
* into the default laravel 'Guard' auth flow. Instead most of the logic is done
|
* into the default laravel 'Guard' auth flow. Instead most of the logic is done
|
||||||
|
@ -16,6 +16,7 @@ class Saml2SessionGuard extends ExternalBaseSessionGuard
|
||||||
* Validate a user's credentials.
|
* Validate a user's credentials.
|
||||||
*
|
*
|
||||||
* @param array $credentials
|
* @param array $credentials
|
||||||
|
*
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function validate(array $credentials = [])
|
public function validate(array $credentials = [])
|
||||||
|
@ -27,7 +28,8 @@ class Saml2SessionGuard extends ExternalBaseSessionGuard
|
||||||
* Attempt to authenticate a user using the given credentials.
|
* Attempt to authenticate a user using the given credentials.
|
||||||
*
|
*
|
||||||
* @param array $credentials
|
* @param array $credentials
|
||||||
* @param bool $remember
|
* @param bool $remember
|
||||||
|
*
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function attempt(array $credentials = [], $remember = false)
|
public function attempt(array $credentials = [], $remember = false)
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
<?php namespace BookStack\Auth\Access;
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Auth\Access;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class Ldap
|
* Class Ldap
|
||||||
|
@ -7,11 +9,12 @@
|
||||||
*/
|
*/
|
||||||
class Ldap
|
class Ldap
|
||||||
{
|
{
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connect to a LDAP server.
|
* Connect to a LDAP server.
|
||||||
|
*
|
||||||
* @param string $hostName
|
* @param string $hostName
|
||||||
* @param int $port
|
* @param int $port
|
||||||
|
*
|
||||||
* @return resource
|
* @return resource
|
||||||
*/
|
*/
|
||||||
public function connect($hostName, $port)
|
public function connect($hostName, $port)
|
||||||
|
@ -21,9 +24,11 @@ class Ldap
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the value of a LDAP option for the given connection.
|
* Set the value of a LDAP option for the given connection.
|
||||||
|
*
|
||||||
* @param resource $ldapConnection
|
* @param resource $ldapConnection
|
||||||
* @param int $option
|
* @param int $option
|
||||||
* @param mixed $value
|
* @param mixed $value
|
||||||
|
*
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function setOption($ldapConnection, $option, $value)
|
public function setOption($ldapConnection, $option, $value)
|
||||||
|
@ -41,8 +46,10 @@ class Ldap
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the version number for the given ldap connection.
|
* Set the version number for the given ldap connection.
|
||||||
|
*
|
||||||
* @param $ldapConnection
|
* @param $ldapConnection
|
||||||
* @param $version
|
* @param $version
|
||||||
|
*
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function setVersion($ldapConnection, $version)
|
public function setVersion($ldapConnection, $version)
|
||||||
|
@ -52,10 +59,12 @@ class Ldap
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search LDAP tree using the provided filter.
|
* Search LDAP tree using the provided filter.
|
||||||
|
*
|
||||||
* @param resource $ldapConnection
|
* @param resource $ldapConnection
|
||||||
* @param string $baseDn
|
* @param string $baseDn
|
||||||
* @param string $filter
|
* @param string $filter
|
||||||
* @param array|null $attributes
|
* @param array|null $attributes
|
||||||
|
*
|
||||||
* @return resource
|
* @return resource
|
||||||
*/
|
*/
|
||||||
public function search($ldapConnection, $baseDn, $filter, array $attributes = null)
|
public function search($ldapConnection, $baseDn, $filter, array $attributes = null)
|
||||||
|
@ -65,8 +74,10 @@ class Ldap
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get entries from an ldap search result.
|
* Get entries from an ldap search result.
|
||||||
|
*
|
||||||
* @param resource $ldapConnection
|
* @param resource $ldapConnection
|
||||||
* @param resource $ldapSearchResult
|
* @param resource $ldapSearchResult
|
||||||
|
*
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function getEntries($ldapConnection, $ldapSearchResult)
|
public function getEntries($ldapConnection, $ldapSearchResult)
|
||||||
|
@ -76,23 +87,28 @@ class Ldap
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search and get entries immediately.
|
* Search and get entries immediately.
|
||||||
|
*
|
||||||
* @param resource $ldapConnection
|
* @param resource $ldapConnection
|
||||||
* @param string $baseDn
|
* @param string $baseDn
|
||||||
* @param string $filter
|
* @param string $filter
|
||||||
* @param array|null $attributes
|
* @param array|null $attributes
|
||||||
|
*
|
||||||
* @return resource
|
* @return resource
|
||||||
*/
|
*/
|
||||||
public function searchAndGetEntries($ldapConnection, $baseDn, $filter, array $attributes = null)
|
public function searchAndGetEntries($ldapConnection, $baseDn, $filter, array $attributes = null)
|
||||||
{
|
{
|
||||||
$search = $this->search($ldapConnection, $baseDn, $filter, $attributes);
|
$search = $this->search($ldapConnection, $baseDn, $filter, $attributes);
|
||||||
|
|
||||||
return $this->getEntries($ldapConnection, $search);
|
return $this->getEntries($ldapConnection, $search);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bind to LDAP directory.
|
* Bind to LDAP directory.
|
||||||
|
*
|
||||||
* @param resource $ldapConnection
|
* @param resource $ldapConnection
|
||||||
* @param string $bindRdn
|
* @param string $bindRdn
|
||||||
* @param string $bindPassword
|
* @param string $bindPassword
|
||||||
|
*
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function bind($ldapConnection, $bindRdn = null, $bindPassword = null)
|
public function bind($ldapConnection, $bindRdn = null, $bindPassword = null)
|
||||||
|
@ -102,8 +118,10 @@ class Ldap
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Explode a LDAP dn string into an array of components.
|
* Explode a LDAP dn string into an array of components.
|
||||||
|
*
|
||||||
* @param string $dn
|
* @param string $dn
|
||||||
* @param int $withAttrib
|
* @param int $withAttrib
|
||||||
|
*
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function explodeDn(string $dn, int $withAttrib)
|
public function explodeDn(string $dn, int $withAttrib)
|
||||||
|
@ -113,12 +131,14 @@ class Ldap
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Escape a string for use in an LDAP filter.
|
* Escape a string for use in an LDAP filter.
|
||||||
|
*
|
||||||
* @param string $value
|
* @param string $value
|
||||||
* @param string $ignore
|
* @param string $ignore
|
||||||
* @param int $flags
|
* @param int $flags
|
||||||
|
*
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
public function escape(string $value, string $ignore = "", int $flags = 0)
|
public function escape(string $value, string $ignore = '', int $flags = 0)
|
||||||
{
|
{
|
||||||
return ldap_escape($value, $ignore, $flags);
|
return ldap_escape($value, $ignore, $flags);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
<?php namespace BookStack\Auth\Access;
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Auth\Access;
|
||||||
|
|
||||||
use BookStack\Auth\User;
|
use BookStack\Auth\User;
|
||||||
use BookStack\Exceptions\JsonDebugException;
|
use BookStack\Exceptions\JsonDebugException;
|
||||||
|
@ -13,7 +15,6 @@ use Illuminate\Support\Facades\Log;
|
||||||
*/
|
*/
|
||||||
class LdapService extends ExternalAuthService
|
class LdapService extends ExternalAuthService
|
||||||
{
|
{
|
||||||
|
|
||||||
protected $ldap;
|
protected $ldap;
|
||||||
protected $ldapConnection;
|
protected $ldapConnection;
|
||||||
protected $userAvatars;
|
protected $userAvatars;
|
||||||
|
@ -33,6 +34,7 @@ class LdapService extends ExternalAuthService
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if groups should be synced.
|
* Check if groups should be synced.
|
||||||
|
*
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function shouldSyncGroups()
|
public function shouldSyncGroups()
|
||||||
|
@ -42,6 +44,7 @@ class LdapService extends ExternalAuthService
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search for attributes for a specific user on the ldap.
|
* Search for attributes for a specific user on the ldap.
|
||||||
|
*
|
||||||
* @throws LdapException
|
* @throws LdapException
|
||||||
*/
|
*/
|
||||||
private function getUserWithAttributes(string $userName, array $attributes): ?array
|
private function getUserWithAttributes(string $userName, array $attributes): ?array
|
||||||
|
@ -73,6 +76,7 @@ class LdapService extends ExternalAuthService
|
||||||
/**
|
/**
|
||||||
* Get the details of a user from LDAP using the given username.
|
* Get the details of a user from LDAP using the given username.
|
||||||
* User found via configurable user filter.
|
* User found via configurable user filter.
|
||||||
|
*
|
||||||
* @throws LdapException
|
* @throws LdapException
|
||||||
*/
|
*/
|
||||||
public function getUserDetails(string $userName): ?array
|
public function getUserDetails(string $userName): ?array
|
||||||
|
@ -92,16 +96,16 @@ class LdapService extends ExternalAuthService
|
||||||
|
|
||||||
$userCn = $this->getUserResponseProperty($user, 'cn', null);
|
$userCn = $this->getUserResponseProperty($user, 'cn', null);
|
||||||
$formatted = [
|
$formatted = [
|
||||||
'uid' => $this->getUserResponseProperty($user, $idAttr, $user['dn']),
|
'uid' => $this->getUserResponseProperty($user, $idAttr, $user['dn']),
|
||||||
'name' => $this->getUserResponseProperty($user, $displayNameAttr, $userCn),
|
'name' => $this->getUserResponseProperty($user, $displayNameAttr, $userCn),
|
||||||
'dn' => $user['dn'],
|
'dn' => $user['dn'],
|
||||||
'email' => $this->getUserResponseProperty($user, $emailAttr, null),
|
'email' => $this->getUserResponseProperty($user, $emailAttr, null),
|
||||||
'avatar'=> $thumbnailAttr ? $this->getUserResponseProperty($user, $thumbnailAttr, null) : null,
|
'avatar'=> $thumbnailAttr ? $this->getUserResponseProperty($user, $thumbnailAttr, null) : null,
|
||||||
];
|
];
|
||||||
|
|
||||||
if ($this->config['dump_user_details']) {
|
if ($this->config['dump_user_details']) {
|
||||||
throw new JsonDebugException([
|
throw new JsonDebugException([
|
||||||
'details_from_ldap' => $user,
|
'details_from_ldap' => $user,
|
||||||
'details_bookstack_parsed' => $formatted,
|
'details_bookstack_parsed' => $formatted,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
@ -137,6 +141,7 @@ class LdapService extends ExternalAuthService
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the given credentials are valid for the given user.
|
* Check if the given credentials are valid for the given user.
|
||||||
|
*
|
||||||
* @throws LdapException
|
* @throws LdapException
|
||||||
*/
|
*/
|
||||||
public function validateUserCredentials(?array $ldapUserDetails, string $password): bool
|
public function validateUserCredentials(?array $ldapUserDetails, string $password): bool
|
||||||
|
@ -146,6 +151,7 @@ class LdapService extends ExternalAuthService
|
||||||
}
|
}
|
||||||
|
|
||||||
$ldapConnection = $this->getConnection();
|
$ldapConnection = $this->getConnection();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$ldapBind = $this->ldap->bind($ldapConnection, $ldapUserDetails['dn'], $password);
|
$ldapBind = $this->ldap->bind($ldapConnection, $ldapUserDetails['dn'], $password);
|
||||||
} catch (ErrorException $e) {
|
} catch (ErrorException $e) {
|
||||||
|
@ -158,7 +164,9 @@ class LdapService extends ExternalAuthService
|
||||||
/**
|
/**
|
||||||
* Bind the system user to the LDAP connection using the given credentials
|
* Bind the system user to the LDAP connection using the given credentials
|
||||||
* otherwise anonymous access is attempted.
|
* otherwise anonymous access is attempted.
|
||||||
|
*
|
||||||
* @param $connection
|
* @param $connection
|
||||||
|
*
|
||||||
* @throws LdapException
|
* @throws LdapException
|
||||||
*/
|
*/
|
||||||
protected function bindSystemUser($connection)
|
protected function bindSystemUser($connection)
|
||||||
|
@ -181,8 +189,10 @@ class LdapService extends ExternalAuthService
|
||||||
/**
|
/**
|
||||||
* Get the connection to the LDAP server.
|
* Get the connection to the LDAP server.
|
||||||
* Creates a new connection if one does not exist.
|
* Creates a new connection if one does not exist.
|
||||||
* @return resource
|
*
|
||||||
* @throws LdapException
|
* @throws LdapException
|
||||||
|
*
|
||||||
|
* @return resource
|
||||||
*/
|
*/
|
||||||
protected function getConnection()
|
protected function getConnection()
|
||||||
{
|
{
|
||||||
|
@ -222,6 +232,7 @@ class LdapService extends ExternalAuthService
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->ldapConnection = $ldapConnection;
|
$this->ldapConnection = $ldapConnection;
|
||||||
|
|
||||||
return $this->ldapConnection;
|
return $this->ldapConnection;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -241,6 +252,7 @@ class LdapService extends ExternalAuthService
|
||||||
// Otherwise, extract the port out
|
// Otherwise, extract the port out
|
||||||
$hostName = $serverNameParts[0];
|
$hostName = $serverNameParts[0];
|
||||||
$ldapPort = (count($serverNameParts) > 1) ? intval($serverNameParts[1]) : 389;
|
$ldapPort = (count($serverNameParts) > 1) ? intval($serverNameParts[1]) : 389;
|
||||||
|
|
||||||
return ['host' => $hostName, 'port' => $ldapPort];
|
return ['host' => $hostName, 'port' => $ldapPort];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -254,11 +266,13 @@ class LdapService extends ExternalAuthService
|
||||||
$newKey = '${' . $key . '}';
|
$newKey = '${' . $key . '}';
|
||||||
$newAttrs[$newKey] = $this->ldap->escape($attrText);
|
$newAttrs[$newKey] = $this->ldap->escape($attrText);
|
||||||
}
|
}
|
||||||
|
|
||||||
return strtr($filterString, $newAttrs);
|
return strtr($filterString, $newAttrs);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the groups a user is a part of on ldap.
|
* Get the groups a user is a part of on ldap.
|
||||||
|
*
|
||||||
* @throws LdapException
|
* @throws LdapException
|
||||||
*/
|
*/
|
||||||
public function getUserGroups(string $userName): array
|
public function getUserGroups(string $userName): array
|
||||||
|
@ -272,11 +286,13 @@ class LdapService extends ExternalAuthService
|
||||||
|
|
||||||
$userGroups = $this->groupFilter($user);
|
$userGroups = $this->groupFilter($user);
|
||||||
$userGroups = $this->getGroupsRecursive($userGroups, []);
|
$userGroups = $this->getGroupsRecursive($userGroups, []);
|
||||||
|
|
||||||
return $userGroups;
|
return $userGroups;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the parent groups of an array of groups.
|
* Get the parent groups of an array of groups.
|
||||||
|
*
|
||||||
* @throws LdapException
|
* @throws LdapException
|
||||||
*/
|
*/
|
||||||
private function getGroupsRecursive(array $groupsArray, array $checked): array
|
private function getGroupsRecursive(array $groupsArray, array $checked): array
|
||||||
|
@ -303,6 +319,7 @@ class LdapService extends ExternalAuthService
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the parent groups of a single group.
|
* Get the parent groups of a single group.
|
||||||
|
*
|
||||||
* @throws LdapException
|
* @throws LdapException
|
||||||
*/
|
*/
|
||||||
private function getGroupGroups(string $groupName): array
|
private function getGroupGroups(string $groupName): array
|
||||||
|
@ -336,7 +353,7 @@ class LdapService extends ExternalAuthService
|
||||||
$count = 0;
|
$count = 0;
|
||||||
|
|
||||||
if (isset($userGroupSearchResponse[$groupsAttr]['count'])) {
|
if (isset($userGroupSearchResponse[$groupsAttr]['count'])) {
|
||||||
$count = (int)$userGroupSearchResponse[$groupsAttr]['count'];
|
$count = (int) $userGroupSearchResponse[$groupsAttr]['count'];
|
||||||
}
|
}
|
||||||
|
|
||||||
for ($i = 0; $i < $count; $i++) {
|
for ($i = 0; $i < $count; $i++) {
|
||||||
|
@ -351,6 +368,7 @@ class LdapService extends ExternalAuthService
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sync the LDAP groups to the user roles for the current user.
|
* Sync the LDAP groups to the user roles for the current user.
|
||||||
|
*
|
||||||
* @throws LdapException
|
* @throws LdapException
|
||||||
*/
|
*/
|
||||||
public function syncGroups(User $user, string $username)
|
public function syncGroups(User $user, string $username)
|
||||||
|
|
|
@ -0,0 +1,164 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Auth\Access;
|
||||||
|
|
||||||
|
use BookStack\Actions\ActivityType;
|
||||||
|
use BookStack\Auth\Access\Mfa\MfaSession;
|
||||||
|
use BookStack\Auth\User;
|
||||||
|
use BookStack\Exceptions\StoppedAuthenticationException;
|
||||||
|
use BookStack\Facades\Activity;
|
||||||
|
use BookStack\Facades\Theme;
|
||||||
|
use BookStack\Theming\ThemeEvents;
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class LoginService
|
||||||
|
{
|
||||||
|
protected const LAST_LOGIN_ATTEMPTED_SESSION_KEY = 'auth-login-last-attempted';
|
||||||
|
|
||||||
|
protected $mfaSession;
|
||||||
|
protected $emailConfirmationService;
|
||||||
|
|
||||||
|
public function __construct(MfaSession $mfaSession, EmailConfirmationService $emailConfirmationService)
|
||||||
|
{
|
||||||
|
$this->mfaSession = $mfaSession;
|
||||||
|
$this->emailConfirmationService = $emailConfirmationService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log the given user into the system.
|
||||||
|
* Will start a login of the given user but will prevent if there's
|
||||||
|
* a reason to (MFA or Unconfirmed Email).
|
||||||
|
* Returns a boolean to indicate the current login result.
|
||||||
|
*
|
||||||
|
* @throws StoppedAuthenticationException
|
||||||
|
*/
|
||||||
|
public function login(User $user, string $method, bool $remember = false): void
|
||||||
|
{
|
||||||
|
if ($this->awaitingEmailConfirmation($user) || $this->needsMfaVerification($user)) {
|
||||||
|
$this->setLastLoginAttemptedForUser($user, $method, $remember);
|
||||||
|
|
||||||
|
throw new StoppedAuthenticationException($user, $this);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->clearLastLoginAttempted();
|
||||||
|
auth()->login($user, $remember);
|
||||||
|
Activity::add(ActivityType::AUTH_LOGIN, "{$method}; {$user->logDescriptor()}");
|
||||||
|
Theme::dispatch(ThemeEvents::AUTH_LOGIN, $method, $user);
|
||||||
|
|
||||||
|
// Authenticate on all session guards if a likely admin
|
||||||
|
if ($user->can('users-manage') && $user->can('user-roles-manage')) {
|
||||||
|
$guards = ['standard', 'ldap', 'saml2'];
|
||||||
|
foreach ($guards as $guard) {
|
||||||
|
auth($guard)->login($user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reattempt a system login after a previous stopped attempt.
|
||||||
|
*
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function reattemptLoginFor(User $user)
|
||||||
|
{
|
||||||
|
if ($user->id !== ($this->getLastLoginAttemptUser()->id ?? null)) {
|
||||||
|
throw new Exception('Login reattempt user does align with current session state');
|
||||||
|
}
|
||||||
|
|
||||||
|
$lastLoginDetails = $this->getLastLoginAttemptDetails();
|
||||||
|
$this->login($user, $lastLoginDetails['method'], $lastLoginDetails['remember'] ?? false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the last user that was attempted to be logged in.
|
||||||
|
* Only exists if the last login attempt had correct credentials
|
||||||
|
* but had been prevented by a secondary factor.
|
||||||
|
*/
|
||||||
|
public function getLastLoginAttemptUser(): ?User
|
||||||
|
{
|
||||||
|
$id = $this->getLastLoginAttemptDetails()['user_id'];
|
||||||
|
|
||||||
|
return User::query()->where('id', '=', $id)->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the details of the last login attempt.
|
||||||
|
* Checks upon a ttl of about 1 hour since that last attempted login.
|
||||||
|
*
|
||||||
|
* @return array{user_id: ?string, method: ?string, remember: bool}
|
||||||
|
*/
|
||||||
|
protected function getLastLoginAttemptDetails(): array
|
||||||
|
{
|
||||||
|
$value = session()->get(self::LAST_LOGIN_ATTEMPTED_SESSION_KEY);
|
||||||
|
if (!$value) {
|
||||||
|
return ['user_id' => null, 'method' => null];
|
||||||
|
}
|
||||||
|
|
||||||
|
[$id, $method, $remember, $time] = explode(':', $value);
|
||||||
|
$hourAgo = time() - (60 * 60);
|
||||||
|
if ($time < $hourAgo) {
|
||||||
|
$this->clearLastLoginAttempted();
|
||||||
|
|
||||||
|
return ['user_id' => null, 'method' => null];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['user_id' => $id, 'method' => $method, 'remember' => boolval($remember)];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the last login attempted user.
|
||||||
|
* Must be only used when credentials are correct and a login could be
|
||||||
|
* achieved but a secondary factor has stopped the login.
|
||||||
|
*/
|
||||||
|
protected function setLastLoginAttemptedForUser(User $user, string $method, bool $remember)
|
||||||
|
{
|
||||||
|
session()->put(
|
||||||
|
self::LAST_LOGIN_ATTEMPTED_SESSION_KEY,
|
||||||
|
implode(':', [$user->id, $method, $remember, time()])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the last login attempted session value.
|
||||||
|
*/
|
||||||
|
protected function clearLastLoginAttempted(): void
|
||||||
|
{
|
||||||
|
session()->remove(self::LAST_LOGIN_ATTEMPTED_SESSION_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if MFA verification is needed.
|
||||||
|
*/
|
||||||
|
public function needsMfaVerification(User $user): bool
|
||||||
|
{
|
||||||
|
return !$this->mfaSession->isVerifiedForUser($user) && $this->mfaSession->isRequiredForUser($user);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the given user is awaiting email confirmation.
|
||||||
|
*/
|
||||||
|
public function awaitingEmailConfirmation(User $user): bool
|
||||||
|
{
|
||||||
|
return $this->emailConfirmationService->confirmationRequired() && !$user->email_confirmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt the login of a user using the given credentials.
|
||||||
|
* Meant to mirror Laravel's default guard 'attempt' method
|
||||||
|
* but in a manner that always routes through our login system.
|
||||||
|
* May interrupt the flow if extra authentication requirements are imposed.
|
||||||
|
*
|
||||||
|
* @throws StoppedAuthenticationException
|
||||||
|
*/
|
||||||
|
public function attempt(array $credentials, string $method, bool $remember = false): bool
|
||||||
|
{
|
||||||
|
$result = auth()->attempt($credentials, $remember);
|
||||||
|
if ($result) {
|
||||||
|
$user = auth()->user();
|
||||||
|
auth()->logout();
|
||||||
|
$this->login($user, $method, $remember);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Auth\Access\Mfa;
|
||||||
|
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class BackupCodeService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Generate a new set of 16 backup codes.
|
||||||
|
*/
|
||||||
|
public function generateNewSet(): array
|
||||||
|
{
|
||||||
|
$codes = [];
|
||||||
|
while (count($codes) < 16) {
|
||||||
|
$code = Str::random(5) . '-' . Str::random(5);
|
||||||
|
if (!in_array($code, $codes)) {
|
||||||
|
$codes[] = strtolower($code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $codes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the given code matches one of the available options.
|
||||||
|
*/
|
||||||
|
public function inputCodeExistsInSet(string $code, string $codeSet): bool
|
||||||
|
{
|
||||||
|
$cleanCode = $this->cleanInputCode($code);
|
||||||
|
$codes = json_decode($codeSet);
|
||||||
|
|
||||||
|
return in_array($cleanCode, $codes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the given input code from the given available options.
|
||||||
|
* Will return a JSON string containing the codes.
|
||||||
|
*/
|
||||||
|
public function removeInputCodeFromSet(string $code, string $codeSet): string
|
||||||
|
{
|
||||||
|
$cleanCode = $this->cleanInputCode($code);
|
||||||
|
$codes = json_decode($codeSet);
|
||||||
|
$pos = array_search($cleanCode, $codes, true);
|
||||||
|
array_splice($codes, $pos, 1);
|
||||||
|
|
||||||
|
return json_encode($codes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count the number of codes in the given set.
|
||||||
|
*/
|
||||||
|
public function countCodesInSet(string $codeSet): int
|
||||||
|
{
|
||||||
|
return count(json_decode($codeSet));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function cleanInputCode(string $code): string
|
||||||
|
{
|
||||||
|
return strtolower(str_replace(' ', '-', trim($code)));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Auth\Access\Mfa;
|
||||||
|
|
||||||
|
use BookStack\Auth\User;
|
||||||
|
|
||||||
|
class MfaSession
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Check if MFA is required for the given user.
|
||||||
|
*/
|
||||||
|
public function isRequiredForUser(User $user): bool
|
||||||
|
{
|
||||||
|
// TODO - Test both these cases
|
||||||
|
return $user->mfaValues()->exists() || $this->userRoleEnforcesMfa($user);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the given user is pending MFA setup.
|
||||||
|
* (MFA required but not yet configured).
|
||||||
|
*/
|
||||||
|
public function isPendingMfaSetup(User $user): bool
|
||||||
|
{
|
||||||
|
return $this->isRequiredForUser($user) && !$user->mfaValues()->exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a role of the given user enforces MFA.
|
||||||
|
*/
|
||||||
|
protected function userRoleEnforcesMfa(User $user): bool
|
||||||
|
{
|
||||||
|
return $user->roles()
|
||||||
|
->where('mfa_enforced', '=', true)
|
||||||
|
->exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the current MFA session has already been verified for the given user.
|
||||||
|
*/
|
||||||
|
public function isVerifiedForUser(User $user): bool
|
||||||
|
{
|
||||||
|
return session()->get($this->getMfaVerifiedSessionKey($user)) === 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark the current session as MFA-verified.
|
||||||
|
*/
|
||||||
|
public function markVerifiedForUser(User $user): void
|
||||||
|
{
|
||||||
|
session()->put($this->getMfaVerifiedSessionKey($user), 'true');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the session key in which the MFA verification status is stored.
|
||||||
|
*/
|
||||||
|
protected function getMfaVerifiedSessionKey(User $user): string
|
||||||
|
{
|
||||||
|
return 'mfa-verification-passed:' . $user->id;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Auth\Access\Mfa;
|
||||||
|
|
||||||
|
use BookStack\Auth\User;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property int $id
|
||||||
|
* @property int $user_id
|
||||||
|
* @property string $method
|
||||||
|
* @property string $value
|
||||||
|
* @property Carbon $created_at
|
||||||
|
* @property Carbon $updated_at
|
||||||
|
*/
|
||||||
|
class MfaValue extends Model
|
||||||
|
{
|
||||||
|
protected static $unguarded = true;
|
||||||
|
|
||||||
|
const METHOD_TOTP = 'totp';
|
||||||
|
const METHOD_BACKUP_CODES = 'backup_codes';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all the MFA methods available.
|
||||||
|
*/
|
||||||
|
public static function allMethods(): array
|
||||||
|
{
|
||||||
|
return [self::METHOD_TOTP, self::METHOD_BACKUP_CODES];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upsert a new MFA value for the given user and method
|
||||||
|
* using the provided value.
|
||||||
|
*/
|
||||||
|
public static function upsertWithValue(User $user, string $method, string $value): void
|
||||||
|
{
|
||||||
|
/** @var MfaValue $mfaVal */
|
||||||
|
$mfaVal = static::query()->firstOrNew([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'method' => $method,
|
||||||
|
]);
|
||||||
|
$mfaVal->setValue($value);
|
||||||
|
$mfaVal->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Easily get the decrypted MFA value for the given user and method.
|
||||||
|
*/
|
||||||
|
public static function getValueForUser(User $user, string $method): ?string
|
||||||
|
{
|
||||||
|
/** @var MfaValue $mfaVal */
|
||||||
|
$mfaVal = static::query()
|
||||||
|
->where('user_id', '=', $user->id)
|
||||||
|
->where('method', '=', $method)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
return $mfaVal ? $mfaVal->getValue() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypt the value attribute upon access.
|
||||||
|
*/
|
||||||
|
protected function getValue(): string
|
||||||
|
{
|
||||||
|
return decrypt($this->value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypt the value attribute upon access.
|
||||||
|
*/
|
||||||
|
protected function setValue($value): void
|
||||||
|
{
|
||||||
|
$this->value = encrypt($value);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Auth\Access\Mfa;
|
||||||
|
|
||||||
|
use BaconQrCode\Renderer\Color\Rgb;
|
||||||
|
use BaconQrCode\Renderer\Image\SvgImageBackEnd;
|
||||||
|
use BaconQrCode\Renderer\ImageRenderer;
|
||||||
|
use BaconQrCode\Renderer\RendererStyle\Fill;
|
||||||
|
use BaconQrCode\Renderer\RendererStyle\RendererStyle;
|
||||||
|
use BaconQrCode\Writer;
|
||||||
|
use PragmaRX\Google2FA\Google2FA;
|
||||||
|
use PragmaRX\Google2FA\Support\Constants;
|
||||||
|
|
||||||
|
class TotpService
|
||||||
|
{
|
||||||
|
protected $google2fa;
|
||||||
|
|
||||||
|
public function __construct(Google2FA $google2fa)
|
||||||
|
{
|
||||||
|
$this->google2fa = $google2fa;
|
||||||
|
// Use SHA1 as a default, Personal testing of other options in 2021 found
|
||||||
|
// many apps lack support for other algorithms yet still will scan
|
||||||
|
// the code causing a confusing UX.
|
||||||
|
$this->google2fa->setAlgorithm(Constants::SHA1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a new totp secret key.
|
||||||
|
*/
|
||||||
|
public function generateSecret(): string
|
||||||
|
{
|
||||||
|
/** @noinspection PhpUnhandledExceptionInspection */
|
||||||
|
return $this->google2fa->generateSecretKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a TOTP URL from secret key.
|
||||||
|
*/
|
||||||
|
public function generateUrl(string $secret): string
|
||||||
|
{
|
||||||
|
return $this->google2fa->getQRCodeUrl(
|
||||||
|
setting('app-name'),
|
||||||
|
user()->email,
|
||||||
|
$secret
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a QR code to display a TOTP URL.
|
||||||
|
*/
|
||||||
|
public function generateQrCodeSvg(string $url): string
|
||||||
|
{
|
||||||
|
$color = Fill::uniformColor(new Rgb(255, 255, 255), new Rgb(32, 110, 167));
|
||||||
|
|
||||||
|
return (new Writer(
|
||||||
|
new ImageRenderer(
|
||||||
|
new RendererStyle(192, 0, null, null, $color),
|
||||||
|
new SvgImageBackEnd()
|
||||||
|
)
|
||||||
|
))->writeString($url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify that the user provided code is valid for the secret.
|
||||||
|
* The secret must be known, not user-provided.
|
||||||
|
*/
|
||||||
|
public function verifyCode(string $code, string $secret): bool
|
||||||
|
{
|
||||||
|
/** @noinspection PhpUnhandledExceptionInspection */
|
||||||
|
return $this->google2fa->verifyKey($secret, $code);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Auth\Access\Mfa;
|
||||||
|
|
||||||
|
use Illuminate\Contracts\Validation\Rule;
|
||||||
|
|
||||||
|
class TotpValidationRule implements Rule
|
||||||
|
{
|
||||||
|
protected $secret;
|
||||||
|
protected $totpService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new rule instance.
|
||||||
|
* Takes the TOTP secret that must be system provided, not user provided.
|
||||||
|
*/
|
||||||
|
public function __construct(string $secret)
|
||||||
|
{
|
||||||
|
$this->secret = $secret;
|
||||||
|
$this->totpService = app()->make(TotpService::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the validation rule passes.
|
||||||
|
*/
|
||||||
|
public function passes($attribute, $value)
|
||||||
|
{
|
||||||
|
return $this->totpService->verifyCode($value, $this->secret);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation error message.
|
||||||
|
*/
|
||||||
|
public function message()
|
||||||
|
{
|
||||||
|
return trans('validation.totp');
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,6 @@
|
||||||
<?php namespace BookStack\Auth\Access;
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Auth\Access;
|
||||||
|
|
||||||
use BookStack\Actions\ActivityType;
|
use BookStack\Actions\ActivityType;
|
||||||
use BookStack\Auth\SocialAccount;
|
use BookStack\Auth\SocialAccount;
|
||||||
|
@ -12,7 +14,6 @@ use Exception;
|
||||||
|
|
||||||
class RegistrationService
|
class RegistrationService
|
||||||
{
|
{
|
||||||
|
|
||||||
protected $userRepo;
|
protected $userRepo;
|
||||||
protected $emailConfirmationService;
|
protected $emailConfirmationService;
|
||||||
|
|
||||||
|
@ -27,6 +28,7 @@ class RegistrationService
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check whether or not registrations are allowed in the app settings.
|
* Check whether or not registrations are allowed in the app settings.
|
||||||
|
*
|
||||||
* @throws UserRegistrationException
|
* @throws UserRegistrationException
|
||||||
*/
|
*/
|
||||||
public function ensureRegistrationAllowed()
|
public function ensureRegistrationAllowed()
|
||||||
|
@ -44,11 +46,13 @@ class RegistrationService
|
||||||
{
|
{
|
||||||
$authMethod = config('auth.method');
|
$authMethod = config('auth.method');
|
||||||
$authMethodsWithRegistration = ['standard'];
|
$authMethodsWithRegistration = ['standard'];
|
||||||
|
|
||||||
return in_array($authMethod, $authMethodsWithRegistration) && setting('registration-enabled');
|
return in_array($authMethod, $authMethodsWithRegistration) && setting('registration-enabled');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The registrations flow for all users.
|
* The registrations flow for all users.
|
||||||
|
*
|
||||||
* @throws UserRegistrationException
|
* @throws UserRegistrationException
|
||||||
*/
|
*/
|
||||||
public function registerUser(array $userData, ?SocialAccount $socialAccount = null, bool $emailConfirmed = false): User
|
public function registerUser(array $userData, ?SocialAccount $socialAccount = null, bool $emailConfirmed = false): User
|
||||||
|
@ -84,6 +88,7 @@ class RegistrationService
|
||||||
session()->flash('sent-email-confirmation', true);
|
session()->flash('sent-email-confirmation', true);
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
$message = trans('auth.email_confirm_send_error');
|
$message = trans('auth.email_confirm_send_error');
|
||||||
|
|
||||||
throw new UserRegistrationException($message, '/register/confirm');
|
throw new UserRegistrationException($message, '/register/confirm');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -94,6 +99,7 @@ class RegistrationService
|
||||||
/**
|
/**
|
||||||
* Ensure that the given email meets any active email domain registration restrictions.
|
* Ensure that the given email meets any active email domain registration restrictions.
|
||||||
* Throws if restrictions are active and the email does not match an allowed domain.
|
* Throws if restrictions are active and the email does not match an allowed domain.
|
||||||
|
*
|
||||||
* @throws UserRegistrationException
|
* @throws UserRegistrationException
|
||||||
*/
|
*/
|
||||||
protected function ensureEmailDomainAllowed(string $userEmail): void
|
protected function ensureEmailDomainAllowed(string $userEmail): void
|
||||||
|
@ -105,9 +111,10 @@ class RegistrationService
|
||||||
}
|
}
|
||||||
|
|
||||||
$restrictedEmailDomains = explode(',', str_replace(' ', '', $registrationRestrict));
|
$restrictedEmailDomains = explode(',', str_replace(' ', '', $registrationRestrict));
|
||||||
$userEmailDomain = $domain = mb_substr(mb_strrchr($userEmail, "@"), 1);
|
$userEmailDomain = $domain = mb_substr(mb_strrchr($userEmail, '@'), 1);
|
||||||
if (!in_array($userEmailDomain, $restrictedEmailDomains)) {
|
if (!in_array($userEmailDomain, $restrictedEmailDomains)) {
|
||||||
$redirect = $this->registrationAllowed() ? '/register' : '/login';
|
$redirect = $this->registrationAllowed() ? '/register' : '/login';
|
||||||
|
|
||||||
throw new UserRegistrationException(trans('auth.registration_email_domain_invalid'), $redirect);
|
throw new UserRegistrationException(trans('auth.registration_email_domain_invalid'), $redirect);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
<?php namespace BookStack\Auth\Access;
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Auth\Access;
|
||||||
|
|
||||||
use BookStack\Actions\ActivityType;
|
|
||||||
use BookStack\Auth\User;
|
use BookStack\Auth\User;
|
||||||
use BookStack\Exceptions\JsonDebugException;
|
use BookStack\Exceptions\JsonDebugException;
|
||||||
use BookStack\Exceptions\SamlException;
|
use BookStack\Exceptions\SamlException;
|
||||||
|
use BookStack\Exceptions\StoppedAuthenticationException;
|
||||||
use BookStack\Exceptions\UserRegistrationException;
|
use BookStack\Exceptions\UserRegistrationException;
|
||||||
use BookStack\Facades\Activity;
|
|
||||||
use BookStack\Facades\Theme;
|
|
||||||
use BookStack\Theming\ThemeEvents;
|
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use OneLogin\Saml2\Auth;
|
use OneLogin\Saml2\Auth;
|
||||||
|
@ -23,34 +22,37 @@ class Saml2Service extends ExternalAuthService
|
||||||
{
|
{
|
||||||
protected $config;
|
protected $config;
|
||||||
protected $registrationService;
|
protected $registrationService;
|
||||||
protected $user;
|
protected $loginService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saml2Service constructor.
|
* Saml2Service constructor.
|
||||||
*/
|
*/
|
||||||
public function __construct(RegistrationService $registrationService, User $user)
|
public function __construct(RegistrationService $registrationService, LoginService $loginService)
|
||||||
{
|
{
|
||||||
$this->config = config('saml2');
|
$this->config = config('saml2');
|
||||||
$this->registrationService = $registrationService;
|
$this->registrationService = $registrationService;
|
||||||
$this->user = $user;
|
$this->loginService = $loginService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initiate a login flow.
|
* Initiate a login flow.
|
||||||
|
*
|
||||||
* @throws Error
|
* @throws Error
|
||||||
*/
|
*/
|
||||||
public function login(): array
|
public function login(): array
|
||||||
{
|
{
|
||||||
$toolKit = $this->getToolkit();
|
$toolKit = $this->getToolkit();
|
||||||
$returnRoute = url('/saml2/acs');
|
$returnRoute = url('/saml2/acs');
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'url' => $toolKit->login($returnRoute, [], false, false, true),
|
'url' => $toolKit->login($returnRoute, [], false, false, true),
|
||||||
'id' => $toolKit->getLastRequestID(),
|
'id' => $toolKit->getLastRequestID(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initiate a logout flow.
|
* Initiate a logout flow.
|
||||||
|
*
|
||||||
* @throws Error
|
* @throws Error
|
||||||
*/
|
*/
|
||||||
public function logout(): array
|
public function logout(): array
|
||||||
|
@ -78,6 +80,7 @@ class Saml2Service extends ExternalAuthService
|
||||||
* Process the ACS response from the idp and return the
|
* Process the ACS response from the idp and return the
|
||||||
* matching, or new if registration active, user matched to the idp.
|
* matching, or new if registration active, user matched to the idp.
|
||||||
* Returns null if not authenticated.
|
* Returns null if not authenticated.
|
||||||
|
*
|
||||||
* @throws Error
|
* @throws Error
|
||||||
* @throws SamlException
|
* @throws SamlException
|
||||||
* @throws ValidationError
|
* @throws ValidationError
|
||||||
|
@ -92,7 +95,7 @@ class Saml2Service extends ExternalAuthService
|
||||||
|
|
||||||
if (!empty($errors)) {
|
if (!empty($errors)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Invalid ACS Response: '.implode(', ', $errors)
|
'Invalid ACS Response: ' . implode(', ', $errors)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,6 +111,7 @@ class Saml2Service extends ExternalAuthService
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process a response for the single logout service.
|
* Process a response for the single logout service.
|
||||||
|
*
|
||||||
* @throws Error
|
* @throws Error
|
||||||
*/
|
*/
|
||||||
public function processSlsResponse(?string $requestId): ?string
|
public function processSlsResponse(?string $requestId): ?string
|
||||||
|
@ -119,11 +123,12 @@ class Saml2Service extends ExternalAuthService
|
||||||
|
|
||||||
if (!empty($errors)) {
|
if (!empty($errors)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Invalid SLS Response: '.implode(', ', $errors)
|
'Invalid SLS Response: ' . implode(', ', $errors)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->actionLogout();
|
$this->actionLogout();
|
||||||
|
|
||||||
return $redirect;
|
return $redirect;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,6 +143,7 @@ class Saml2Service extends ExternalAuthService
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the metadata for this service provider.
|
* Get the metadata for this service provider.
|
||||||
|
*
|
||||||
* @throws Error
|
* @throws Error
|
||||||
*/
|
*/
|
||||||
public function metadata(): string
|
public function metadata(): string
|
||||||
|
@ -149,7 +155,7 @@ class Saml2Service extends ExternalAuthService
|
||||||
|
|
||||||
if (!empty($errors)) {
|
if (!empty($errors)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Invalid SP metadata: '.implode(', ', $errors),
|
'Invalid SP metadata: ' . implode(', ', $errors),
|
||||||
Error::METADATA_SP_INVALID
|
Error::METADATA_SP_INVALID
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -159,6 +165,7 @@ class Saml2Service extends ExternalAuthService
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load the underlying Onelogin SAML2 toolkit.
|
* Load the underlying Onelogin SAML2 toolkit.
|
||||||
|
*
|
||||||
* @throws Error
|
* @throws Error
|
||||||
* @throws Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
|
@ -178,6 +185,7 @@ class Saml2Service extends ExternalAuthService
|
||||||
|
|
||||||
$spSettings = $this->loadOneloginServiceProviderDetails();
|
$spSettings = $this->loadOneloginServiceProviderDetails();
|
||||||
$settings = array_replace_recursive($settings, $spSettings, $metaDataSettings, $overrides);
|
$settings = array_replace_recursive($settings, $spSettings, $metaDataSettings, $overrides);
|
||||||
|
|
||||||
return new Auth($settings);
|
return new Auth($settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -187,18 +195,18 @@ class Saml2Service extends ExternalAuthService
|
||||||
protected function loadOneloginServiceProviderDetails(): array
|
protected function loadOneloginServiceProviderDetails(): array
|
||||||
{
|
{
|
||||||
$spDetails = [
|
$spDetails = [
|
||||||
'entityId' => url('/saml2/metadata'),
|
'entityId' => url('/saml2/metadata'),
|
||||||
'assertionConsumerService' => [
|
'assertionConsumerService' => [
|
||||||
'url' => url('/saml2/acs'),
|
'url' => url('/saml2/acs'),
|
||||||
],
|
],
|
||||||
'singleLogoutService' => [
|
'singleLogoutService' => [
|
||||||
'url' => url('/saml2/sls')
|
'url' => url('/saml2/sls'),
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'baseurl' => url('/saml2'),
|
'baseurl' => url('/saml2'),
|
||||||
'sp' => $spDetails
|
'sp' => $spDetails,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -211,7 +219,7 @@ class Saml2Service extends ExternalAuthService
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate the display name
|
* Calculate the display name.
|
||||||
*/
|
*/
|
||||||
protected function getUserDisplayName(array $samlAttributes, string $defaultValue): string
|
protected function getUserDisplayName(array $samlAttributes, string $defaultValue): string
|
||||||
{
|
{
|
||||||
|
@ -261,9 +269,9 @@ class Saml2Service extends ExternalAuthService
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'external_id' => $externalId,
|
'external_id' => $externalId,
|
||||||
'name' => $this->getUserDisplayName($samlAttributes, $externalId),
|
'name' => $this->getUserDisplayName($samlAttributes, $externalId),
|
||||||
'email' => $email,
|
'email' => $email,
|
||||||
'saml_id' => $samlID,
|
'saml_id' => $samlID,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -297,6 +305,7 @@ class Saml2Service extends ExternalAuthService
|
||||||
$data = $data[0];
|
$data = $data[0];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -315,19 +324,20 @@ class Saml2Service extends ExternalAuthService
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the user from the database for the specified details.
|
* Get the user from the database for the specified details.
|
||||||
|
*
|
||||||
* @throws UserRegistrationException
|
* @throws UserRegistrationException
|
||||||
*/
|
*/
|
||||||
protected function getOrRegisterUser(array $userDetails): ?User
|
protected function getOrRegisterUser(array $userDetails): ?User
|
||||||
{
|
{
|
||||||
$user = $this->user->newQuery()
|
$user = User::query()
|
||||||
->where('external_auth_id', '=', $userDetails['external_id'])
|
->where('external_auth_id', '=', $userDetails['external_id'])
|
||||||
->first();
|
->first();
|
||||||
|
|
||||||
if (is_null($user)) {
|
if (is_null($user)) {
|
||||||
$userData = [
|
$userData = [
|
||||||
'name' => $userDetails['name'],
|
'name' => $userDetails['name'],
|
||||||
'email' => $userDetails['email'],
|
'email' => $userDetails['email'],
|
||||||
'password' => Str::random(32),
|
'password' => Str::random(32),
|
||||||
'external_auth_id' => $userDetails['external_id'],
|
'external_auth_id' => $userDetails['external_id'],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -340,9 +350,11 @@ class Saml2Service extends ExternalAuthService
|
||||||
/**
|
/**
|
||||||
* Process the SAML response for a user. Login the user when
|
* Process the SAML response for a user. Login the user when
|
||||||
* they exist, optionally registering them automatically.
|
* they exist, optionally registering them automatically.
|
||||||
|
*
|
||||||
* @throws SamlException
|
* @throws SamlException
|
||||||
* @throws JsonDebugException
|
* @throws JsonDebugException
|
||||||
* @throws UserRegistrationException
|
* @throws UserRegistrationException
|
||||||
|
* @throws StoppedAuthenticationException
|
||||||
*/
|
*/
|
||||||
public function processLoginCallback(string $samlID, array $samlAttributes): User
|
public function processLoginCallback(string $samlID, array $samlAttributes): User
|
||||||
{
|
{
|
||||||
|
@ -351,8 +363,8 @@ class Saml2Service extends ExternalAuthService
|
||||||
|
|
||||||
if ($this->config['dump_user_details']) {
|
if ($this->config['dump_user_details']) {
|
||||||
throw new JsonDebugException([
|
throw new JsonDebugException([
|
||||||
'id_from_idp' => $samlID,
|
'id_from_idp' => $samlID,
|
||||||
'attrs_from_idp' => $samlAttributes,
|
'attrs_from_idp' => $samlAttributes,
|
||||||
'attrs_after_parsing' => $userDetails,
|
'attrs_after_parsing' => $userDetails,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
@ -375,9 +387,8 @@ class Saml2Service extends ExternalAuthService
|
||||||
$this->syncWithGroups($user, $groups);
|
$this->syncWithGroups($user, $groups);
|
||||||
}
|
}
|
||||||
|
|
||||||
auth()->login($user);
|
$this->loginService->login($user, 'saml2');
|
||||||
Activity::add(ActivityType::AUTH_LOGIN, "saml2; {$user->logDescriptor()}");
|
|
||||||
Theme::dispatch(ThemeEvents::AUTH_LOGIN, 'saml2', $user);
|
|
||||||
return $user;
|
return $user;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,12 @@
|
||||||
<?php namespace BookStack\Auth\Access;
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Auth\Access;
|
||||||
|
|
||||||
use BookStack\Actions\ActivityType;
|
|
||||||
use BookStack\Auth\SocialAccount;
|
use BookStack\Auth\SocialAccount;
|
||||||
use BookStack\Auth\User;
|
use BookStack\Auth\User;
|
||||||
use BookStack\Exceptions\SocialDriverNotConfigured;
|
use BookStack\Exceptions\SocialDriverNotConfigured;
|
||||||
use BookStack\Exceptions\SocialSignInAccountNotUsed;
|
use BookStack\Exceptions\SocialSignInAccountNotUsed;
|
||||||
use BookStack\Exceptions\UserRegistrationException;
|
use BookStack\Exceptions\UserRegistrationException;
|
||||||
use BookStack\Facades\Activity;
|
|
||||||
use BookStack\Facades\Theme;
|
|
||||||
use BookStack\Theming\ThemeEvents;
|
|
||||||
use Illuminate\Support\Facades\Event;
|
use Illuminate\Support\Facades\Event;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Laravel\Socialite\Contracts\Factory as Socialite;
|
use Laravel\Socialite\Contracts\Factory as Socialite;
|
||||||
|
@ -21,12 +19,19 @@ class SocialAuthService
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* The core socialite library used.
|
* The core socialite library used.
|
||||||
|
*
|
||||||
* @var Socialite
|
* @var Socialite
|
||||||
*/
|
*/
|
||||||
protected $socialite;
|
protected $socialite;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var LoginService
|
||||||
|
*/
|
||||||
|
protected $loginService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The default built-in social drivers we support.
|
* The default built-in social drivers we support.
|
||||||
|
*
|
||||||
* @var string[]
|
* @var string[]
|
||||||
*/
|
*/
|
||||||
protected $validSocialDrivers = [
|
protected $validSocialDrivers = [
|
||||||
|
@ -39,7 +44,7 @@ class SocialAuthService
|
||||||
'okta',
|
'okta',
|
||||||
'gitlab',
|
'gitlab',
|
||||||
'twitch',
|
'twitch',
|
||||||
'discord'
|
'discord',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -47,6 +52,7 @@ class SocialAuthService
|
||||||
* for an initial redirect action.
|
* for an initial redirect action.
|
||||||
* Array is keyed by social driver name.
|
* Array is keyed by social driver name.
|
||||||
* Callbacks are passed an instance of the driver.
|
* Callbacks are passed an instance of the driver.
|
||||||
|
*
|
||||||
* @var array<string, callable>
|
* @var array<string, callable>
|
||||||
*/
|
*/
|
||||||
protected $configureForRedirectCallbacks = [];
|
protected $configureForRedirectCallbacks = [];
|
||||||
|
@ -54,33 +60,39 @@ class SocialAuthService
|
||||||
/**
|
/**
|
||||||
* SocialAuthService constructor.
|
* SocialAuthService constructor.
|
||||||
*/
|
*/
|
||||||
public function __construct(Socialite $socialite)
|
public function __construct(Socialite $socialite, LoginService $loginService)
|
||||||
{
|
{
|
||||||
$this->socialite = $socialite;
|
$this->socialite = $socialite;
|
||||||
|
$this->loginService = $loginService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start the social login path.
|
* Start the social login path.
|
||||||
|
*
|
||||||
* @throws SocialDriverNotConfigured
|
* @throws SocialDriverNotConfigured
|
||||||
*/
|
*/
|
||||||
public function startLogIn(string $socialDriver): RedirectResponse
|
public function startLogIn(string $socialDriver): RedirectResponse
|
||||||
{
|
{
|
||||||
$driver = $this->validateDriver($socialDriver);
|
$driver = $this->validateDriver($socialDriver);
|
||||||
|
|
||||||
return $this->getDriverForRedirect($driver)->redirect();
|
return $this->getDriverForRedirect($driver)->redirect();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start the social registration process
|
* Start the social registration process.
|
||||||
|
*
|
||||||
* @throws SocialDriverNotConfigured
|
* @throws SocialDriverNotConfigured
|
||||||
*/
|
*/
|
||||||
public function startRegister(string $socialDriver): RedirectResponse
|
public function startRegister(string $socialDriver): RedirectResponse
|
||||||
{
|
{
|
||||||
$driver = $this->validateDriver($socialDriver);
|
$driver = $this->validateDriver($socialDriver);
|
||||||
|
|
||||||
return $this->getDriverForRedirect($driver)->redirect();
|
return $this->getDriverForRedirect($driver)->redirect();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle the social registration process on callback.
|
* Handle the social registration process on callback.
|
||||||
|
*
|
||||||
* @throws UserRegistrationException
|
* @throws UserRegistrationException
|
||||||
*/
|
*/
|
||||||
public function handleRegistrationCallback(string $socialDriver, SocialUser $socialUser): SocialUser
|
public function handleRegistrationCallback(string $socialDriver, SocialUser $socialUser): SocialUser
|
||||||
|
@ -92,6 +104,7 @@ class SocialAuthService
|
||||||
|
|
||||||
if (User::query()->where('email', '=', $socialUser->getEmail())->exists()) {
|
if (User::query()->where('email', '=', $socialUser->getEmail())->exists()) {
|
||||||
$email = $socialUser->getEmail();
|
$email = $socialUser->getEmail();
|
||||||
|
|
||||||
throw new UserRegistrationException(trans('errors.error_user_exists_different_creds', ['email' => $email]), '/login');
|
throw new UserRegistrationException(trans('errors.error_user_exists_different_creds', ['email' => $email]), '/login');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,16 +113,19 @@ class SocialAuthService
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the social user details via the social driver.
|
* Get the social user details via the social driver.
|
||||||
|
*
|
||||||
* @throws SocialDriverNotConfigured
|
* @throws SocialDriverNotConfigured
|
||||||
*/
|
*/
|
||||||
public function getSocialUser(string $socialDriver): SocialUser
|
public function getSocialUser(string $socialDriver): SocialUser
|
||||||
{
|
{
|
||||||
$driver = $this->validateDriver($socialDriver);
|
$driver = $this->validateDriver($socialDriver);
|
||||||
|
|
||||||
return $this->socialite->driver($driver)->user();
|
return $this->socialite->driver($driver)->user();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle the login process on a oAuth callback.
|
* Handle the login process on a oAuth callback.
|
||||||
|
*
|
||||||
* @throws SocialSignInAccountNotUsed
|
* @throws SocialSignInAccountNotUsed
|
||||||
*/
|
*/
|
||||||
public function handleLoginCallback(string $socialDriver, SocialUser $socialUser)
|
public function handleLoginCallback(string $socialDriver, SocialUser $socialUser)
|
||||||
|
@ -125,9 +141,8 @@ class SocialAuthService
|
||||||
// When a user is not logged in and a matching SocialAccount exists,
|
// When a user is not logged in and a matching SocialAccount exists,
|
||||||
// Simply log the user into the application.
|
// Simply log the user into the application.
|
||||||
if (!$isLoggedIn && $socialAccount !== null) {
|
if (!$isLoggedIn && $socialAccount !== null) {
|
||||||
auth()->login($socialAccount->user);
|
$this->loginService->login($socialAccount->user, $socialAccount);
|
||||||
Activity::add(ActivityType::AUTH_LOGIN, $socialAccount);
|
|
||||||
Theme::dispatch(ThemeEvents::AUTH_LOGIN, $socialDriver, $socialAccount->user);
|
|
||||||
return redirect()->intended('/');
|
return redirect()->intended('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,18 +152,21 @@ class SocialAuthService
|
||||||
$account = $this->newSocialAccount($socialDriver, $socialUser);
|
$account = $this->newSocialAccount($socialDriver, $socialUser);
|
||||||
$currentUser->socialAccounts()->save($account);
|
$currentUser->socialAccounts()->save($account);
|
||||||
session()->flash('success', trans('settings.users_social_connected', ['socialAccount' => $titleCaseDriver]));
|
session()->flash('success', trans('settings.users_social_connected', ['socialAccount' => $titleCaseDriver]));
|
||||||
|
|
||||||
return redirect($currentUser->getEditUrl());
|
return redirect($currentUser->getEditUrl());
|
||||||
}
|
}
|
||||||
|
|
||||||
// When a user is logged in and the social account exists and is already linked to the current user.
|
// When a user is logged in and the social account exists and is already linked to the current user.
|
||||||
if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id === $currentUser->id) {
|
if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id === $currentUser->id) {
|
||||||
session()->flash('error', trans('errors.social_account_existing', ['socialAccount' => $titleCaseDriver]));
|
session()->flash('error', trans('errors.social_account_existing', ['socialAccount' => $titleCaseDriver]));
|
||||||
|
|
||||||
return redirect($currentUser->getEditUrl());
|
return redirect($currentUser->getEditUrl());
|
||||||
}
|
}
|
||||||
|
|
||||||
// When a user is logged in, A social account exists but the users do not match.
|
// When a user is logged in, A social account exists but the users do not match.
|
||||||
if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id != $currentUser->id) {
|
if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id != $currentUser->id) {
|
||||||
session()->flash('error', trans('errors.social_account_already_used_existing', ['socialAccount' => $titleCaseDriver]));
|
session()->flash('error', trans('errors.social_account_already_used_existing', ['socialAccount' => $titleCaseDriver]));
|
||||||
|
|
||||||
return redirect($currentUser->getEditUrl());
|
return redirect($currentUser->getEditUrl());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -163,6 +181,7 @@ class SocialAuthService
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure the social driver is correct and supported.
|
* Ensure the social driver is correct and supported.
|
||||||
|
*
|
||||||
* @throws SocialDriverNotConfigured
|
* @throws SocialDriverNotConfigured
|
||||||
*/
|
*/
|
||||||
protected function validateDriver(string $socialDriver): string
|
protected function validateDriver(string $socialDriver): string
|
||||||
|
@ -188,6 +207,7 @@ class SocialAuthService
|
||||||
$lowerName = strtolower($driver);
|
$lowerName = strtolower($driver);
|
||||||
$configPrefix = 'services.' . $lowerName . '.';
|
$configPrefix = 'services.' . $lowerName . '.';
|
||||||
$config = [config($configPrefix . 'client_id'), config($configPrefix . 'client_secret'), config('services.callback_url')];
|
$config = [config($configPrefix . 'client_id'), config($configPrefix . 'client_secret'), config('services.callback_url')];
|
||||||
|
|
||||||
return !in_array(false, $config) && !in_array(null, $config);
|
return !in_array(false, $config) && !in_array(null, $config);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -237,9 +257,9 @@ class SocialAuthService
|
||||||
public function newSocialAccount(string $socialDriver, SocialUser $socialUser): SocialAccount
|
public function newSocialAccount(string $socialDriver, SocialUser $socialUser): SocialAccount
|
||||||
{
|
{
|
||||||
return new SocialAccount([
|
return new SocialAccount([
|
||||||
'driver' => $socialDriver,
|
'driver' => $socialDriver,
|
||||||
'driver_id' => $socialUser->getId(),
|
'driver_id' => $socialUser->getId(),
|
||||||
'avatar' => $socialUser->getAvatar()
|
'avatar' => $socialUser->getAvatar(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -252,7 +272,7 @@ class SocialAuthService
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provide redirect options per service for the Laravel Socialite driver
|
* Provide redirect options per service for the Laravel Socialite driver.
|
||||||
*/
|
*/
|
||||||
protected function getDriverForRedirect(string $driverName): Provider
|
protected function getDriverForRedirect(string $driverName): Provider
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
<?php namespace BookStack\Auth\Access;
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Auth\Access;
|
||||||
|
|
||||||
use BookStack\Auth\User;
|
use BookStack\Auth\User;
|
||||||
use BookStack\Notifications\UserInvite;
|
use BookStack\Notifications\UserInvite;
|
||||||
|
@ -11,6 +13,7 @@ class UserInviteService extends UserTokenService
|
||||||
/**
|
/**
|
||||||
* Send an invitation to a user to sign into BookStack
|
* Send an invitation to a user to sign into BookStack
|
||||||
* Removes existing invitation tokens.
|
* Removes existing invitation tokens.
|
||||||
|
*
|
||||||
* @param User $user
|
* @param User $user
|
||||||
*/
|
*/
|
||||||
public function sendInvitation(User $user)
|
public function sendInvitation(User $user)
|
||||||
|
|
|
@ -1,59 +1,56 @@
|
||||||
<?php namespace BookStack\Auth\Access;
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Auth\Access;
|
||||||
|
|
||||||
use BookStack\Auth\User;
|
use BookStack\Auth\User;
|
||||||
use BookStack\Exceptions\UserTokenExpiredException;
|
use BookStack\Exceptions\UserTokenExpiredException;
|
||||||
use BookStack\Exceptions\UserTokenNotFoundException;
|
use BookStack\Exceptions\UserTokenNotFoundException;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Illuminate\Database\Connection as Database;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use stdClass;
|
use stdClass;
|
||||||
|
|
||||||
class UserTokenService
|
class UserTokenService
|
||||||
{
|
{
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Name of table where user tokens are stored.
|
* Name of table where user tokens are stored.
|
||||||
|
*
|
||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
protected $tokenTable = 'user_tokens';
|
protected $tokenTable = 'user_tokens';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Token expiry time in hours.
|
* Token expiry time in hours.
|
||||||
|
*
|
||||||
* @var int
|
* @var int
|
||||||
*/
|
*/
|
||||||
protected $expiryTime = 24;
|
protected $expiryTime = 24;
|
||||||
|
|
||||||
protected $db;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* UserTokenService constructor.
|
|
||||||
* @param Database $db
|
|
||||||
*/
|
|
||||||
public function __construct(Database $db)
|
|
||||||
{
|
|
||||||
$this->db = $db;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete all email confirmations that belong to a user.
|
* Delete all email confirmations that belong to a user.
|
||||||
|
*
|
||||||
* @param User $user
|
* @param User $user
|
||||||
|
*
|
||||||
* @return mixed
|
* @return mixed
|
||||||
*/
|
*/
|
||||||
public function deleteByUser(User $user)
|
public function deleteByUser(User $user)
|
||||||
{
|
{
|
||||||
return $this->db->table($this->tokenTable)
|
return DB::table($this->tokenTable)
|
||||||
->where('user_id', '=', $user->id)
|
->where('user_id', '=', $user->id)
|
||||||
->delete();
|
->delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the user id from a token, while check 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
|
* @param string $token
|
||||||
* @return int
|
*
|
||||||
* @throws UserTokenNotFoundException
|
* @throws UserTokenNotFoundException
|
||||||
* @throws UserTokenExpiredException
|
* @throws UserTokenExpiredException
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
*/
|
*/
|
||||||
public function checkTokenAndGetUserId(string $token) : int
|
public function checkTokenAndGetUserId(string $token): int
|
||||||
{
|
{
|
||||||
$entry = $this->getEntryByToken($token);
|
$entry = $this->getEntryByToken($token);
|
||||||
|
|
||||||
|
@ -70,63 +67,74 @@ class UserTokenService
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a unique token within the email confirmation database.
|
* Creates a unique token within the email confirmation database.
|
||||||
|
*
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
protected function generateToken() : string
|
protected function generateToken(): string
|
||||||
{
|
{
|
||||||
$token = Str::random(24);
|
$token = Str::random(24);
|
||||||
while ($this->tokenExists($token)) {
|
while ($this->tokenExists($token)) {
|
||||||
$token = Str::random(25);
|
$token = Str::random(25);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $token;
|
return $token;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate and store a token for the given user.
|
* Generate and store a token for the given user.
|
||||||
|
*
|
||||||
* @param User $user
|
* @param User $user
|
||||||
|
*
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
protected function createTokenForUser(User $user) : string
|
protected function createTokenForUser(User $user): string
|
||||||
{
|
{
|
||||||
$token = $this->generateToken();
|
$token = $this->generateToken();
|
||||||
$this->db->table($this->tokenTable)->insert([
|
DB::table($this->tokenTable)->insert([
|
||||||
'user_id' => $user->id,
|
'user_id' => $user->id,
|
||||||
'token' => $token,
|
'token' => $token,
|
||||||
'created_at' => Carbon::now(),
|
'created_at' => Carbon::now(),
|
||||||
'updated_at' => Carbon::now()
|
'updated_at' => Carbon::now(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return $token;
|
return $token;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the given token exists.
|
* Check if the given token exists.
|
||||||
|
*
|
||||||
* @param string $token
|
* @param string $token
|
||||||
|
*
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
protected function tokenExists(string $token) : bool
|
protected function tokenExists(string $token): bool
|
||||||
{
|
{
|
||||||
return $this->db->table($this->tokenTable)
|
return DB::table($this->tokenTable)
|
||||||
->where('token', '=', $token)->exists();
|
->where('token', '=', $token)->exists();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a token entry for the given token.
|
* Get a token entry for the given token.
|
||||||
|
*
|
||||||
* @param string $token
|
* @param string $token
|
||||||
|
*
|
||||||
* @return object|null
|
* @return object|null
|
||||||
*/
|
*/
|
||||||
protected function getEntryByToken(string $token)
|
protected function getEntryByToken(string $token)
|
||||||
{
|
{
|
||||||
return $this->db->table($this->tokenTable)
|
return DB::table($this->tokenTable)
|
||||||
->where('token', '=', $token)
|
->where('token', '=', $token)
|
||||||
->first();
|
->first();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the given token entry has expired.
|
* Check if the given token entry has expired.
|
||||||
|
*
|
||||||
* @param stdClass $tokenEntry
|
* @param stdClass $tokenEntry
|
||||||
|
*
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
protected function entryExpired(stdClass $tokenEntry) : bool
|
protected function entryExpired(stdClass $tokenEntry): bool
|
||||||
{
|
{
|
||||||
return Carbon::now()->subHours($this->expiryTime)
|
return Carbon::now()->subHours($this->expiryTime)
|
||||||
->gt(new Carbon($tokenEntry->created_at));
|
->gt(new Carbon($tokenEntry->created_at));
|
||||||
|
|
|
@ -1,15 +1,17 @@
|
||||||
<?php namespace BookStack\Auth\Permissions;
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Auth\Permissions;
|
||||||
|
|
||||||
use BookStack\Model;
|
use BookStack\Model;
|
||||||
|
|
||||||
class EntityPermission extends Model
|
class EntityPermission extends Model
|
||||||
{
|
{
|
||||||
|
|
||||||
protected $fillable = ['role_id', 'action'];
|
protected $fillable = ['role_id', 'action'];
|
||||||
public $timestamps = false;
|
public $timestamps = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all this restriction's attached entity.
|
* Get all this restriction's attached entity.
|
||||||
|
*
|
||||||
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
|
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
|
||||||
*/
|
*/
|
||||||
public function restrictable()
|
public function restrictable()
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
<?php namespace BookStack\Auth\Permissions;
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Auth\Permissions;
|
||||||
|
|
||||||
use BookStack\Auth\Role;
|
use BookStack\Auth\Role;
|
||||||
use BookStack\Entities\Models\Entity;
|
use BookStack\Entities\Models\Entity;
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
<?php namespace BookStack\Auth\Permissions;
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Auth\Permissions;
|
||||||
|
|
||||||
use BookStack\Auth\Role;
|
use BookStack\Auth\Role;
|
||||||
use BookStack\Auth\User;
|
use BookStack\Auth\User;
|
||||||
|
@ -48,7 +50,7 @@ class PermissionService
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the database connection
|
* Set the database connection.
|
||||||
*/
|
*/
|
||||||
public function setConnection(Connection $connection)
|
public function setConnection(Connection $connection)
|
||||||
{
|
{
|
||||||
|
@ -56,7 +58,8 @@ class PermissionService
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prepare the local entity cache and ensure it's empty
|
* Prepare the local entity cache and ensure it's empty.
|
||||||
|
*
|
||||||
* @param Entity[] $entities
|
* @param Entity[] $entities
|
||||||
*/
|
*/
|
||||||
protected function readyEntityCache(array $entities = [])
|
protected function readyEntityCache(array $entities = [])
|
||||||
|
@ -73,7 +76,7 @@ class PermissionService
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a book via ID, Checks local cache
|
* Get a book via ID, Checks local cache.
|
||||||
*/
|
*/
|
||||||
protected function getBook(int $bookId): ?Book
|
protected function getBook(int $bookId): ?Book
|
||||||
{
|
{
|
||||||
|
@ -85,7 +88,7 @@ class PermissionService
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a chapter via ID, Checks local cache
|
* Get a chapter via ID, Checks local cache.
|
||||||
*/
|
*/
|
||||||
protected function getChapter(int $chapterId): ?Chapter
|
protected function getChapter(int $chapterId): ?Chapter
|
||||||
{
|
{
|
||||||
|
@ -151,12 +154,13 @@ class PermissionService
|
||||||
},
|
},
|
||||||
'pages' => function ($query) {
|
'pages' => function ($query) {
|
||||||
$query->withTrashed()->select(['id', 'restricted', 'owned_by', 'book_id', 'chapter_id']);
|
$query->withTrashed()->select(['id', 'restricted', 'owned_by', 'book_id', 'chapter_id']);
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build joint permissions for the given shelf and role combinations.
|
* Build joint permissions for the given shelf and role combinations.
|
||||||
|
*
|
||||||
* @throws Throwable
|
* @throws Throwable
|
||||||
*/
|
*/
|
||||||
protected function buildJointPermissionsForShelves(EloquentCollection $shelves, array $roles, bool $deleteOld = false)
|
protected function buildJointPermissionsForShelves(EloquentCollection $shelves, array $roles, bool $deleteOld = false)
|
||||||
|
@ -169,6 +173,7 @@ class PermissionService
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build joint permissions for the given book and role combinations.
|
* Build joint permissions for the given book and role combinations.
|
||||||
|
*
|
||||||
* @throws Throwable
|
* @throws Throwable
|
||||||
*/
|
*/
|
||||||
protected function buildJointPermissionsForBooks(EloquentCollection $books, array $roles, bool $deleteOld = false)
|
protected function buildJointPermissionsForBooks(EloquentCollection $books, array $roles, bool $deleteOld = false)
|
||||||
|
@ -193,6 +198,7 @@ class PermissionService
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rebuild the entity jointPermissions for a particular entity.
|
* Rebuild the entity jointPermissions for a particular entity.
|
||||||
|
*
|
||||||
* @throws Throwable
|
* @throws Throwable
|
||||||
*/
|
*/
|
||||||
public function buildJointPermissionsForEntity(Entity $entity)
|
public function buildJointPermissionsForEntity(Entity $entity)
|
||||||
|
@ -201,6 +207,7 @@ class PermissionService
|
||||||
if ($entity instanceof Book) {
|
if ($entity instanceof Book) {
|
||||||
$books = $this->bookFetchQuery()->where('id', '=', $entity->id)->get();
|
$books = $this->bookFetchQuery()->where('id', '=', $entity->id)->get();
|
||||||
$this->buildJointPermissionsForBooks($books, Role::query()->get()->all(), true);
|
$this->buildJointPermissionsForBooks($books, Role::query()->get()->all(), true);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -224,6 +231,7 @@ class PermissionService
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rebuild the entity jointPermissions for a collection of entities.
|
* Rebuild the entity jointPermissions for a collection of entities.
|
||||||
|
*
|
||||||
* @throws Throwable
|
* @throws Throwable
|
||||||
*/
|
*/
|
||||||
public function buildJointPermissionsForEntities(array $entities)
|
public function buildJointPermissionsForEntities(array $entities)
|
||||||
|
@ -263,6 +271,7 @@ class PermissionService
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete all of the entity jointPermissions for a list of entities.
|
* Delete all of the entity jointPermissions for a list of entities.
|
||||||
|
*
|
||||||
* @param Role[] $roles
|
* @param Role[] $roles
|
||||||
*/
|
*/
|
||||||
protected function deleteManyJointPermissionsForRoles($roles)
|
protected function deleteManyJointPermissionsForRoles($roles)
|
||||||
|
@ -275,7 +284,9 @@ class PermissionService
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete the entity jointPermissions for a particular entity.
|
* Delete the entity jointPermissions for a particular entity.
|
||||||
|
*
|
||||||
* @param Entity $entity
|
* @param Entity $entity
|
||||||
|
*
|
||||||
* @throws Throwable
|
* @throws Throwable
|
||||||
*/
|
*/
|
||||||
public function deleteJointPermissionsForEntity(Entity $entity)
|
public function deleteJointPermissionsForEntity(Entity $entity)
|
||||||
|
@ -285,7 +296,9 @@ class PermissionService
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete all of the entity jointPermissions for a list of entities.
|
* Delete all of the entity jointPermissions for a list of entities.
|
||||||
|
*
|
||||||
* @param Entity[] $entities
|
* @param Entity[] $entities
|
||||||
|
*
|
||||||
* @throws Throwable
|
* @throws Throwable
|
||||||
*/
|
*/
|
||||||
protected function deleteManyJointPermissionsForEntities(array $entities)
|
protected function deleteManyJointPermissionsForEntities(array $entities)
|
||||||
|
@ -295,7 +308,6 @@ class PermissionService
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->db->transaction(function () use ($entities) {
|
$this->db->transaction(function () use ($entities) {
|
||||||
|
|
||||||
foreach (array_chunk($entities, 1000) as $entityChunk) {
|
foreach (array_chunk($entities, 1000) as $entityChunk) {
|
||||||
$query = $this->db->table('joint_permissions');
|
$query = $this->db->table('joint_permissions');
|
||||||
foreach ($entityChunk as $entity) {
|
foreach ($entityChunk as $entity) {
|
||||||
|
@ -311,8 +323,10 @@ class PermissionService
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create & Save entity jointPermissions for many entities and roles.
|
* Create & Save entity jointPermissions for many entities and roles.
|
||||||
|
*
|
||||||
* @param Entity[] $entities
|
* @param Entity[] $entities
|
||||||
* @param Role[] $roles
|
* @param Role[] $roles
|
||||||
|
*
|
||||||
* @throws Throwable
|
* @throws Throwable
|
||||||
*/
|
*/
|
||||||
protected function createManyJointPermissions(array $entities, array $roles)
|
protected function createManyJointPermissions(array $entities, array $roles)
|
||||||
|
@ -363,7 +377,6 @@ class PermissionService
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the actions related to an entity.
|
* Get the actions related to an entity.
|
||||||
*/
|
*/
|
||||||
|
@ -376,6 +389,7 @@ class PermissionService
|
||||||
if ($entity instanceof Book) {
|
if ($entity instanceof Book) {
|
||||||
$baseActions[] = 'chapter-create';
|
$baseActions[] = 'chapter-create';
|
||||||
}
|
}
|
||||||
|
|
||||||
return $baseActions;
|
return $baseActions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -397,6 +411,7 @@ class PermissionService
|
||||||
|
|
||||||
if ($entity->restricted) {
|
if ($entity->restricted) {
|
||||||
$hasAccess = $this->mapHasActiveRestriction($permissionMap, $entity, $role, $restrictionAction);
|
$hasAccess = $this->mapHasActiveRestriction($permissionMap, $entity, $role, $restrictionAction);
|
||||||
|
|
||||||
return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
|
return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -433,6 +448,7 @@ class PermissionService
|
||||||
protected function mapHasActiveRestriction(array $entityMap, Entity $entity, Role $role, string $action): bool
|
protected function mapHasActiveRestriction(array $entityMap, Entity $entity, Role $role, string $action): bool
|
||||||
{
|
{
|
||||||
$key = $entity->getMorphClass() . ':' . $entity->getRawAttribute('id') . ':' . $role->getRawAttribute('id') . ':' . $action;
|
$key = $entity->getMorphClass() . ':' . $entity->getRawAttribute('id') . ':' . $role->getRawAttribute('id') . ':' . $action;
|
||||||
|
|
||||||
return $entityMap[$key] ?? false;
|
return $entityMap[$key] ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -443,18 +459,19 @@ class PermissionService
|
||||||
protected function createJointPermissionDataArray(Entity $entity, Role $role, string $action, bool $permissionAll, bool $permissionOwn): array
|
protected function createJointPermissionDataArray(Entity $entity, Role $role, string $action, bool $permissionAll, bool $permissionOwn): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'role_id' => $role->getRawAttribute('id'),
|
'role_id' => $role->getRawAttribute('id'),
|
||||||
'entity_id' => $entity->getRawAttribute('id'),
|
'entity_id' => $entity->getRawAttribute('id'),
|
||||||
'entity_type' => $entity->getMorphClass(),
|
'entity_type' => $entity->getMorphClass(),
|
||||||
'action' => $action,
|
'action' => $action,
|
||||||
'has_permission' => $permissionAll,
|
'has_permission' => $permissionAll,
|
||||||
'has_permission_own' => $permissionOwn,
|
'has_permission_own' => $permissionOwn,
|
||||||
'owned_by' => $entity->getRawAttribute('owned_by'),
|
'owned_by' => $entity->getRawAttribute('owned_by'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if an entity has a restriction set upon it.
|
* Checks if an entity has a restriction set upon it.
|
||||||
|
*
|
||||||
* @param HasCreatorAndUpdater|HasOwner $ownable
|
* @param HasCreatorAndUpdater|HasOwner $ownable
|
||||||
*/
|
*/
|
||||||
public function checkOwnableUserAccess(Model $ownable, string $permission): bool
|
public function checkOwnableUserAccess(Model $ownable, string $permission): bool
|
||||||
|
@ -473,7 +490,8 @@ class PermissionService
|
||||||
$ownPermission = $user && $user->can($permission . '-own');
|
$ownPermission = $user && $user->can($permission . '-own');
|
||||||
$ownerField = ($ownable instanceof Entity) ? 'owned_by' : 'created_by';
|
$ownerField = ($ownable instanceof Entity) ? 'owned_by' : 'created_by';
|
||||||
$isOwner = $user && $user->id === $ownable->$ownerField;
|
$isOwner = $user && $user->id === $ownable->$ownerField;
|
||||||
return ($allPermission || ($isOwner && $ownPermission));
|
|
||||||
|
return $allPermission || ($isOwner && $ownPermission);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle abnormal create jointPermissions
|
// Handle abnormal create jointPermissions
|
||||||
|
@ -483,6 +501,7 @@ class PermissionService
|
||||||
|
|
||||||
$hasAccess = $this->entityRestrictionQuery($baseQuery, $action)->count() > 0;
|
$hasAccess = $this->entityRestrictionQuery($baseQuery, $action)->count() > 0;
|
||||||
$this->clean();
|
$this->clean();
|
||||||
|
|
||||||
return $hasAccess;
|
return $hasAccess;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -509,6 +528,7 @@ class PermissionService
|
||||||
|
|
||||||
$hasPermission = $permissionQuery->count() > 0;
|
$hasPermission = $permissionQuery->count() > 0;
|
||||||
$this->clean();
|
$this->clean();
|
||||||
|
|
||||||
return $hasPermission;
|
return $hasPermission;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -529,6 +549,7 @@ class PermissionService
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->clean();
|
$this->clean();
|
||||||
|
|
||||||
return $q;
|
return $q;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -539,6 +560,7 @@ class PermissionService
|
||||||
public function restrictEntityQuery(Builder $query, string $ability = 'view'): Builder
|
public function restrictEntityQuery(Builder $query, string $ability = 'view'): Builder
|
||||||
{
|
{
|
||||||
$this->clean();
|
$this->clean();
|
||||||
|
|
||||||
return $query->where(function (Builder $parentQuery) use ($ability) {
|
return $query->where(function (Builder $parentQuery) use ($ability) {
|
||||||
$parentQuery->whereHas('jointPermissions', function (Builder $permissionQuery) use ($ability) {
|
$parentQuery->whereHas('jointPermissions', function (Builder $permissionQuery) use ($ability) {
|
||||||
$permissionQuery->whereIn('role_id', $this->getCurrentUserRoles())
|
$permissionQuery->whereIn('role_id', $this->getCurrentUserRoles())
|
||||||
|
@ -580,6 +602,7 @@ class PermissionService
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filter items that have entities set as a polymorphic relation.
|
* Filter items that have entities set as a polymorphic relation.
|
||||||
|
*
|
||||||
* @param Builder|\Illuminate\Database\Query\Builder $query
|
* @param Builder|\Illuminate\Database\Query\Builder $query
|
||||||
*/
|
*/
|
||||||
public function filterRestrictedEntityRelations($query, string $tableName, string $entityIdColumn, string $entityTypeColumn, string $action = 'view')
|
public function filterRestrictedEntityRelations($query, string $tableName, string $entityIdColumn, string $entityTypeColumn, string $action = 'view')
|
||||||
|
@ -600,6 +623,7 @@ class PermissionService
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->clean();
|
$this->clean();
|
||||||
|
|
||||||
return $q;
|
return $q;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -628,12 +652,14 @@ class PermissionService
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->clean();
|
$this->clean();
|
||||||
|
|
||||||
return $q;
|
return $q;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add the query for checking the given user id has permission
|
* Add the query for checking the given user id has permission
|
||||||
* within the join_permissions table.
|
* within the join_permissions table.
|
||||||
|
*
|
||||||
* @param QueryBuilder|Builder $query
|
* @param QueryBuilder|Builder $query
|
||||||
*/
|
*/
|
||||||
protected function addJointHasPermissionCheck($query, int $userIdToCheck)
|
protected function addJointHasPermissionCheck($query, int $userIdToCheck)
|
||||||
|
@ -645,7 +671,7 @@ class PermissionService
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current user
|
* Get the current user.
|
||||||
*/
|
*/
|
||||||
private function currentUser(): User
|
private function currentUser(): User
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
<?php namespace BookStack\Auth\Permissions;
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Auth\Permissions;
|
||||||
|
|
||||||
use BookStack\Actions\ActivityType;
|
use BookStack\Actions\ActivityType;
|
||||||
use BookStack\Auth\Role;
|
use BookStack\Auth\Role;
|
||||||
|
@ -9,7 +11,6 @@ use Illuminate\Database\Eloquent\Collection;
|
||||||
|
|
||||||
class PermissionsRepo
|
class PermissionsRepo
|
||||||
{
|
{
|
||||||
|
|
||||||
protected $permission;
|
protected $permission;
|
||||||
protected $role;
|
protected $role;
|
||||||
protected $permissionService;
|
protected $permissionService;
|
||||||
|
@ -56,12 +57,14 @@ class PermissionsRepo
|
||||||
public function saveNewRole(array $roleData): Role
|
public function saveNewRole(array $roleData): Role
|
||||||
{
|
{
|
||||||
$role = $this->role->newInstance($roleData);
|
$role = $this->role->newInstance($roleData);
|
||||||
|
$role->mfa_enforced = ($roleData['mfa_enforced'] ?? 'false') === 'true';
|
||||||
$role->save();
|
$role->save();
|
||||||
|
|
||||||
$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
|
$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
|
||||||
$this->assignRolePermissions($role, $permissions);
|
$this->assignRolePermissions($role, $permissions);
|
||||||
$this->permissionService->buildJointPermissionForRole($role);
|
$this->permissionService->buildJointPermissionForRole($role);
|
||||||
Activity::add(ActivityType::ROLE_CREATE, $role);
|
Activity::add(ActivityType::ROLE_CREATE, $role);
|
||||||
|
|
||||||
return $role;
|
return $role;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,6 +91,7 @@ class PermissionsRepo
|
||||||
$this->assignRolePermissions($role, $permissions);
|
$this->assignRolePermissions($role, $permissions);
|
||||||
|
|
||||||
$role->fill($roleData);
|
$role->fill($roleData);
|
||||||
|
$role->mfa_enforced = ($roleData['mfa_enforced'] ?? 'false') === 'true';
|
||||||
$role->save();
|
$role->save();
|
||||||
$this->permissionService->buildJointPermissionForRole($role);
|
$this->permissionService->buildJointPermissionForRole($role);
|
||||||
Activity::add(ActivityType::ROLE_UPDATE, $role);
|
Activity::add(ActivityType::ROLE_UPDATE, $role);
|
||||||
|
@ -116,6 +120,7 @@ class PermissionsRepo
|
||||||
* Check it's not an admin role or set as default before deleting.
|
* Check it's not an admin role or set as default before deleting.
|
||||||
* If an migration Role ID is specified the users assign to the current role
|
* If an migration Role ID is specified the users assign to the current role
|
||||||
* will be added to the role of the specified id.
|
* will be added to the role of the specified id.
|
||||||
|
*
|
||||||
* @throws PermissionsException
|
* @throws PermissionsException
|
||||||
* @throws Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
|
@ -127,7 +132,7 @@ class PermissionsRepo
|
||||||
// Prevent deleting admin role or default registration role.
|
// Prevent deleting admin role or default registration role.
|
||||||
if ($role->system_name && in_array($role->system_name, $this->systemRoles)) {
|
if ($role->system_name && in_array($role->system_name, $this->systemRoles)) {
|
||||||
throw new PermissionsException(trans('errors.role_system_cannot_be_deleted'));
|
throw new PermissionsException(trans('errors.role_system_cannot_be_deleted'));
|
||||||
} else if ($role->id === intval(setting('registration-role'))) {
|
} elseif ($role->id === intval(setting('registration-role'))) {
|
||||||
throw new PermissionsException(trans('errors.role_registration_default_cannot_delete'));
|
throw new PermissionsException(trans('errors.role_registration_default_cannot_delete'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
<?php namespace BookStack\Auth\Permissions;
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Auth\Permissions;
|
||||||
|
|
||||||
use BookStack\Auth\Role;
|
use BookStack\Auth\Role;
|
||||||
use BookStack\Model;
|
use BookStack\Model;
|
||||||
|
@ -18,7 +20,9 @@ class RolePermission extends Model
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the permission object by name.
|
* Get the permission object by name.
|
||||||
|
*
|
||||||
* @param $name
|
* @param $name
|
||||||
|
*
|
||||||
* @return mixed
|
* @return mixed
|
||||||
*/
|
*/
|
||||||
public static function getByName($name)
|
public static function getByName($name)
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
<?php namespace BookStack\Auth;
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Auth;
|
||||||
|
|
||||||
use BookStack\Auth\Permissions\JointPermission;
|
use BookStack\Auth\Permissions\JointPermission;
|
||||||
use BookStack\Auth\Permissions\RolePermission;
|
use BookStack\Auth\Permissions\RolePermission;
|
||||||
|
@ -9,16 +11,17 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class Role
|
* Class Role.
|
||||||
* @property int $id
|
*
|
||||||
|
* @property int $id
|
||||||
* @property string $display_name
|
* @property string $display_name
|
||||||
* @property string $description
|
* @property string $description
|
||||||
* @property string $external_auth_id
|
* @property string $external_auth_id
|
||||||
* @property string $system_name
|
* @property string $system_name
|
||||||
|
* @property bool $mfa_enforced
|
||||||
*/
|
*/
|
||||||
class Role extends Model implements Loggable
|
class Role extends Model implements Loggable
|
||||||
{
|
{
|
||||||
|
|
||||||
protected $fillable = ['display_name', 'description', 'external_auth_id'];
|
protected $fillable = ['display_name', 'description', 'external_auth_id'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -56,6 +59,7 @@ class Role extends Model implements Loggable
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,18 @@
|
||||||
<?php namespace BookStack\Auth;
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Auth;
|
||||||
|
|
||||||
use BookStack\Interfaces\Loggable;
|
use BookStack\Interfaces\Loggable;
|
||||||
use BookStack\Model;
|
use BookStack\Model;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class SocialAccount
|
* Class SocialAccount.
|
||||||
|
*
|
||||||
* @property string $driver
|
* @property string $driver
|
||||||
* @property User $user
|
* @property User $user
|
||||||
*/
|
*/
|
||||||
class SocialAccount extends Model implements Loggable
|
class SocialAccount extends Model implements Loggable
|
||||||
{
|
{
|
||||||
|
|
||||||
protected $fillable = ['user_id', 'driver', 'driver_id', 'timestamps'];
|
protected $fillable = ['user_id', 'driver', 'driver_id', 'timestamps'];
|
||||||
|
|
||||||
public function user()
|
public function user()
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
<?php namespace BookStack\Auth;
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Auth;
|
||||||
|
|
||||||
use BookStack\Actions\Favourite;
|
use BookStack\Actions\Favourite;
|
||||||
use BookStack\Api\ApiToken;
|
use BookStack\Api\ApiToken;
|
||||||
|
use BookStack\Auth\Access\Mfa\MfaValue;
|
||||||
use BookStack\Entities\Tools\SlugGenerator;
|
use BookStack\Entities\Tools\SlugGenerator;
|
||||||
use BookStack\Interfaces\Loggable;
|
use BookStack\Interfaces\Loggable;
|
||||||
use BookStack\Interfaces\Sluggable;
|
use BookStack\Interfaces\Sluggable;
|
||||||
|
@ -22,32 +25,38 @@ use Illuminate\Notifications\Notifiable;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class User
|
* Class User.
|
||||||
* @property string $id
|
*
|
||||||
* @property string $name
|
* @property string $id
|
||||||
* @property string $slug
|
* @property string $name
|
||||||
* @property string $email
|
* @property string $slug
|
||||||
* @property string $password
|
* @property string $email
|
||||||
* @property Carbon $created_at
|
* @property string $password
|
||||||
* @property Carbon $updated_at
|
* @property Carbon $created_at
|
||||||
* @property bool $email_confirmed
|
* @property Carbon $updated_at
|
||||||
* @property int $image_id
|
* @property bool $email_confirmed
|
||||||
* @property string $external_auth_id
|
* @property int $image_id
|
||||||
* @property string $system_name
|
* @property string $external_auth_id
|
||||||
|
* @property string $system_name
|
||||||
* @property Collection $roles
|
* @property Collection $roles
|
||||||
|
* @property Collection $mfaValues
|
||||||
*/
|
*/
|
||||||
class User extends Model implements AuthenticatableContract, CanResetPasswordContract, Loggable, Sluggable
|
class User extends Model implements AuthenticatableContract, CanResetPasswordContract, Loggable, Sluggable
|
||||||
{
|
{
|
||||||
use Authenticatable, CanResetPassword, Notifiable;
|
use Authenticatable;
|
||||||
|
use CanResetPassword;
|
||||||
|
use Notifiable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The database table used by the model.
|
* The database table used by the model.
|
||||||
|
*
|
||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
protected $table = 'users';
|
protected $table = 'users';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The attributes that are mass assignable.
|
* The attributes that are mass assignable.
|
||||||
|
*
|
||||||
* @var array
|
* @var array
|
||||||
*/
|
*/
|
||||||
protected $fillable = ['name', 'email'];
|
protected $fillable = ['name', 'email'];
|
||||||
|
@ -56,6 +65,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The attributes excluded from the model's JSON form.
|
* The attributes excluded from the model's JSON form.
|
||||||
|
*
|
||||||
* @var array
|
* @var array
|
||||||
*/
|
*/
|
||||||
protected $hidden = [
|
protected $hidden = [
|
||||||
|
@ -65,12 +75,14 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This holds the user's permissions when loaded.
|
* This holds the user's permissions when loaded.
|
||||||
|
*
|
||||||
* @var ?Collection
|
* @var ?Collection
|
||||||
*/
|
*/
|
||||||
protected $permissions;
|
protected $permissions;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This holds the default user when loaded.
|
* This holds the default user when loaded.
|
||||||
|
*
|
||||||
* @var null|User
|
* @var null|User
|
||||||
*/
|
*/
|
||||||
protected static $defaultUser = null;
|
protected static $defaultUser = null;
|
||||||
|
@ -85,6 +97,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||||
}
|
}
|
||||||
|
|
||||||
static::$defaultUser = static::query()->where('system_name', '=', 'public')->first();
|
static::$defaultUser = static::query()->where('system_name', '=', 'public')->first();
|
||||||
|
|
||||||
return static::$defaultUser;
|
return static::$defaultUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,13 +111,15 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The roles that belong to the user.
|
* The roles that belong to the user.
|
||||||
|
*
|
||||||
* @return BelongsToMany
|
* @return BelongsToMany
|
||||||
*/
|
*/
|
||||||
public function roles()
|
public function roles()
|
||||||
{
|
{
|
||||||
if ($this->id === 0) {
|
if ($this->id === 0) {
|
||||||
return ;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->belongsToMany(Role::class);
|
return $this->belongsToMany(Role::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -194,7 +209,9 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||||
/**
|
/**
|
||||||
* Check if the user has a social account,
|
* Check if the user has a social account,
|
||||||
* If a driver is passed it checks for that single account type.
|
* If a driver is passed it checks for that single account type.
|
||||||
|
*
|
||||||
* @param bool|string $socialDriver
|
* @param bool|string $socialDriver
|
||||||
|
*
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function hasSocialAccount($socialDriver = false)
|
public function hasSocialAccount($socialDriver = false)
|
||||||
|
@ -207,7 +224,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a URL to the user's avatar
|
* Returns a URL to the user's avatar.
|
||||||
*/
|
*/
|
||||||
public function getAvatar(int $size = 50): string
|
public function getAvatar(int $size = 50): string
|
||||||
{
|
{
|
||||||
|
@ -222,6 +239,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||||
} catch (Exception $err) {
|
} catch (Exception $err) {
|
||||||
$avatar = $default;
|
$avatar = $default;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $avatar;
|
return $avatar;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -249,6 +267,14 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||||
return $this->hasMany(Favourite::class);
|
return $this->hasMany(Favourite::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the MFA values belonging to this use.
|
||||||
|
*/
|
||||||
|
public function mfaValues(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(MfaValue::class);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the last activity time for this user.
|
* Get the last activity time for this user.
|
||||||
*/
|
*/
|
||||||
|
@ -268,6 +294,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||||
public function getEditUrl(string $path = ''): string
|
public function getEditUrl(string $path = ''): string
|
||||||
{
|
{
|
||||||
$uri = '/settings/users/' . $this->id . '/' . trim($path, '/');
|
$uri = '/settings/users/' . $this->id . '/' . trim($path, '/');
|
||||||
|
|
||||||
return url(rtrim($uri, '/'));
|
return url(rtrim($uri, '/'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -298,7 +325,9 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send the password reset notification.
|
* Send the password reset notification.
|
||||||
* @param string $token
|
*
|
||||||
|
* @param string $token
|
||||||
|
*
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function sendPasswordResetNotification($token)
|
public function sendPasswordResetNotification($token)
|
||||||
|
@ -320,6 +349,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||||
public function refreshSlug(): string
|
public function refreshSlug(): string
|
||||||
{
|
{
|
||||||
$this->slug = app(SlugGenerator::class)->generate($this);
|
$this->slug = app(SlugGenerator::class)->generate($this);
|
||||||
|
|
||||||
return $this->slug;
|
return $this->slug;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
<?php namespace BookStack\Auth;
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Auth;
|
||||||
|
|
||||||
use Activity;
|
use Activity;
|
||||||
use BookStack\Entities\EntityProvider;
|
use BookStack\Entities\EntityProvider;
|
||||||
|
@ -69,6 +71,7 @@ class UserRepo
|
||||||
$query = User::query()->select(['*'])
|
$query = User::query()->select(['*'])
|
||||||
->withLastActivityAt()
|
->withLastActivityAt()
|
||||||
->with(['roles', 'avatar'])
|
->with(['roles', 'avatar'])
|
||||||
|
->withCount('mfaValues')
|
||||||
->orderBy($sort, $sortData['order']);
|
->orderBy($sort, $sortData['order']);
|
||||||
|
|
||||||
if ($sortData['search']) {
|
if ($sortData['search']) {
|
||||||
|
@ -82,7 +85,7 @@ class UserRepo
|
||||||
return $query->paginate($count);
|
return $query->paginate($count);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new user and attaches a role to them.
|
* Creates a new user and attaches a role to them.
|
||||||
*/
|
*/
|
||||||
public function registerNew(array $data, bool $emailConfirmed = false): User
|
public function registerNew(array $data, bool $emailConfirmed = false): User
|
||||||
|
@ -96,6 +99,7 @@ class UserRepo
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assign a user to a system-level role.
|
* Assign a user to a system-level role.
|
||||||
|
*
|
||||||
* @throws NotFoundException
|
* @throws NotFoundException
|
||||||
*/
|
*/
|
||||||
public function attachSystemRole(User $user, string $systemRoleName)
|
public function attachSystemRole(User $user, string $systemRoleName)
|
||||||
|
@ -126,6 +130,7 @@ class UserRepo
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the assigned user roles via an array of role IDs.
|
* Set the assigned user roles via an array of role IDs.
|
||||||
|
*
|
||||||
* @throws UserUpdateException
|
* @throws UserUpdateException
|
||||||
*/
|
*/
|
||||||
public function setUserRoles(User $user, array $roles)
|
public function setUserRoles(User $user, array $roles)
|
||||||
|
@ -141,7 +146,7 @@ class UserRepo
|
||||||
* Check if the given user is the last admin and their new roles no longer
|
* Check if the given user is the last admin and their new roles no longer
|
||||||
* contains the admin role.
|
* contains the admin role.
|
||||||
*/
|
*/
|
||||||
protected function demotingLastAdmin(User $user, array $newRoles) : bool
|
protected function demotingLastAdmin(User $user, array $newRoles): bool
|
||||||
{
|
{
|
||||||
if ($this->isOnlyAdmin($user)) {
|
if ($this->isOnlyAdmin($user)) {
|
||||||
$adminRole = Role::getSystemRole('admin');
|
$adminRole = Role::getSystemRole('admin');
|
||||||
|
@ -159,10 +164,10 @@ class UserRepo
|
||||||
public function create(array $data, bool $emailConfirmed = false): User
|
public function create(array $data, bool $emailConfirmed = false): User
|
||||||
{
|
{
|
||||||
$details = [
|
$details = [
|
||||||
'name' => $data['name'],
|
'name' => $data['name'],
|
||||||
'email' => $data['email'],
|
'email' => $data['email'],
|
||||||
'password' => bcrypt($data['password']),
|
'password' => bcrypt($data['password']),
|
||||||
'email_confirmed' => $emailConfirmed,
|
'email_confirmed' => $emailConfirmed,
|
||||||
'external_auth_id' => $data['external_auth_id'] ?? '',
|
'external_auth_id' => $data['external_auth_id'] ?? '',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -176,6 +181,7 @@ class UserRepo
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove the given user from storage, Delete all related content.
|
* Remove the given user from storage, Delete all related content.
|
||||||
|
*
|
||||||
* @throws Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
public function destroy(User $user, ?int $newOwnerId = null)
|
public function destroy(User $user, ?int $newOwnerId = null)
|
||||||
|
@ -183,6 +189,7 @@ class UserRepo
|
||||||
$user->socialAccounts()->delete();
|
$user->socialAccounts()->delete();
|
||||||
$user->apiTokens()->delete();
|
$user->apiTokens()->delete();
|
||||||
$user->favourites()->delete();
|
$user->favourites()->delete();
|
||||||
|
$user->mfaValues()->delete();
|
||||||
$user->delete();
|
$user->delete();
|
||||||
|
|
||||||
// Delete user profile images
|
// Delete user profile images
|
||||||
|
@ -201,7 +208,7 @@ class UserRepo
|
||||||
*/
|
*/
|
||||||
protected function migrateOwnership(User $fromUser, User $toUser)
|
protected function migrateOwnership(User $fromUser, User $toUser)
|
||||||
{
|
{
|
||||||
$entities = (new EntityProvider)->all();
|
$entities = (new EntityProvider())->all();
|
||||||
foreach ($entities as $instance) {
|
foreach ($entities as $instance) {
|
||||||
$instance->newQuery()->where('owned_by', '=', $fromUser->id)
|
$instance->newQuery()->where('owned_by', '=', $fromUser->id)
|
||||||
->update(['owned_by' => $toUser->id]);
|
->update(['owned_by' => $toUser->id]);
|
||||||
|
@ -242,11 +249,12 @@ class UserRepo
|
||||||
public function getAssetCounts(User $user): array
|
public function getAssetCounts(User $user): array
|
||||||
{
|
{
|
||||||
$createdBy = ['created_by' => $user->id];
|
$createdBy = ['created_by' => $user->id];
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'pages' => Page::visible()->where($createdBy)->count(),
|
'pages' => Page::visible()->where($createdBy)->count(),
|
||||||
'chapters' => Chapter::visible()->where($createdBy)->count(),
|
'chapters' => Chapter::visible()->where($createdBy)->count(),
|
||||||
'books' => Book::visible()->where($createdBy)->count(),
|
'books' => Book::visible()->where($createdBy)->count(),
|
||||||
'shelves' => Bookshelf::visible()->where($createdBy)->count(),
|
'shelves' => Bookshelf::visible()->where($createdBy)->count(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,6 @@ return [
|
||||||
'max_item_count' => env('API_MAX_ITEM_COUNT', 500),
|
'max_item_count' => env('API_MAX_ITEM_COUNT', 500),
|
||||||
|
|
||||||
// The number of API requests that can be made per minute by a single user.
|
// The number of API requests that can be made per minute by a single user.
|
||||||
'requests_per_minute' => env('API_REQUESTS_PER_MIN', 180)
|
'requests_per_minute' => env('API_REQUESTS_PER_MIN', 180),
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
|
@ -36,6 +36,11 @@ return [
|
||||||
// Even when overridden the WYSIWYG editor may still escape script content.
|
// Even when overridden the WYSIWYG editor may still escape script content.
|
||||||
'allow_content_scripts' => env('ALLOW_CONTENT_SCRIPTS', false),
|
'allow_content_scripts' => env('ALLOW_CONTENT_SCRIPTS', false),
|
||||||
|
|
||||||
|
// Allow server-side fetches to be performed to potentially unknown
|
||||||
|
// and user-provided locations. Primarily used in exports when loading
|
||||||
|
// in externally referenced assets.
|
||||||
|
'allow_untrusted_server_fetching' => env('ALLOW_UNTRUSTED_SERVER_FETCHING', false),
|
||||||
|
|
||||||
// Override the default behaviour for allowing crawlers to crawl the instance.
|
// Override the default behaviour for allowing crawlers to crawl the instance.
|
||||||
// May be ignored if view has be overridden or modified.
|
// May be ignored if view has be overridden or modified.
|
||||||
// Defaults to null since, if not set, 'app-public' status used instead.
|
// Defaults to null since, if not set, 'app-public' status used instead.
|
||||||
|
@ -56,7 +61,7 @@ return [
|
||||||
'locale' => env('APP_LANG', 'en'),
|
'locale' => env('APP_LANG', 'en'),
|
||||||
|
|
||||||
// Locales available
|
// Locales available
|
||||||
'locales' => ['en', 'ar', 'bg', 'bs', 'ca', 'cs', 'da', 'de', 'de_informal', 'es', 'es_AR', 'fa', 'fr', 'he', 'hr', 'hu', 'id', 'it', 'ja', 'ko', 'lv', 'nl', 'nb', 'pt', 'pt_BR', 'sk', 'sl', 'sv', 'pl', 'ru', 'th', 'tr', 'uk', 'vi', 'zh_CN', 'zh_TW',],
|
'locales' => ['en', 'ar', 'bg', 'bs', 'ca', 'cs', 'da', 'de', 'de_informal', 'es', 'es_AR', 'fa', 'fr', 'he', 'hr', 'hu', 'id', 'it', 'ja', 'ko', 'lt', 'lv', 'nl', 'nb', 'pt', 'pt_BR', 'sk', 'sl', 'sv', 'pl', 'ru', 'th', 'tr', 'uk', 'vi', 'zh_CN', 'zh_TW'],
|
||||||
|
|
||||||
// Application Fallback Locale
|
// Application Fallback Locale
|
||||||
'fallback_locale' => 'en',
|
'fallback_locale' => 'en',
|
||||||
|
@ -140,52 +145,52 @@ return [
|
||||||
'aliases' => [
|
'aliases' => [
|
||||||
|
|
||||||
// Laravel
|
// Laravel
|
||||||
'App' => Illuminate\Support\Facades\App::class,
|
'App' => Illuminate\Support\Facades\App::class,
|
||||||
'Arr' => Illuminate\Support\Arr::class,
|
'Arr' => Illuminate\Support\Arr::class,
|
||||||
'Artisan' => Illuminate\Support\Facades\Artisan::class,
|
'Artisan' => Illuminate\Support\Facades\Artisan::class,
|
||||||
'Auth' => Illuminate\Support\Facades\Auth::class,
|
'Auth' => Illuminate\Support\Facades\Auth::class,
|
||||||
'Blade' => Illuminate\Support\Facades\Blade::class,
|
'Blade' => Illuminate\Support\Facades\Blade::class,
|
||||||
'Bus' => Illuminate\Support\Facades\Bus::class,
|
'Bus' => Illuminate\Support\Facades\Bus::class,
|
||||||
'Cache' => Illuminate\Support\Facades\Cache::class,
|
'Cache' => Illuminate\Support\Facades\Cache::class,
|
||||||
'Config' => Illuminate\Support\Facades\Config::class,
|
'Config' => Illuminate\Support\Facades\Config::class,
|
||||||
'Cookie' => Illuminate\Support\Facades\Cookie::class,
|
'Cookie' => Illuminate\Support\Facades\Cookie::class,
|
||||||
'Crypt' => Illuminate\Support\Facades\Crypt::class,
|
'Crypt' => Illuminate\Support\Facades\Crypt::class,
|
||||||
'DB' => Illuminate\Support\Facades\DB::class,
|
'DB' => Illuminate\Support\Facades\DB::class,
|
||||||
'Eloquent' => Illuminate\Database\Eloquent\Model::class,
|
'Eloquent' => Illuminate\Database\Eloquent\Model::class,
|
||||||
'Event' => Illuminate\Support\Facades\Event::class,
|
'Event' => Illuminate\Support\Facades\Event::class,
|
||||||
'File' => Illuminate\Support\Facades\File::class,
|
'File' => Illuminate\Support\Facades\File::class,
|
||||||
'Hash' => Illuminate\Support\Facades\Hash::class,
|
'Hash' => Illuminate\Support\Facades\Hash::class,
|
||||||
'Input' => Illuminate\Support\Facades\Input::class,
|
'Input' => Illuminate\Support\Facades\Input::class,
|
||||||
'Inspiring' => Illuminate\Foundation\Inspiring::class,
|
'Inspiring' => Illuminate\Foundation\Inspiring::class,
|
||||||
'Lang' => Illuminate\Support\Facades\Lang::class,
|
'Lang' => Illuminate\Support\Facades\Lang::class,
|
||||||
'Log' => Illuminate\Support\Facades\Log::class,
|
'Log' => Illuminate\Support\Facades\Log::class,
|
||||||
'Mail' => Illuminate\Support\Facades\Mail::class,
|
'Mail' => Illuminate\Support\Facades\Mail::class,
|
||||||
'Notification' => Illuminate\Support\Facades\Notification::class,
|
'Notification' => Illuminate\Support\Facades\Notification::class,
|
||||||
'Password' => Illuminate\Support\Facades\Password::class,
|
'Password' => Illuminate\Support\Facades\Password::class,
|
||||||
'Queue' => Illuminate\Support\Facades\Queue::class,
|
'Queue' => Illuminate\Support\Facades\Queue::class,
|
||||||
'Redirect' => Illuminate\Support\Facades\Redirect::class,
|
'Redirect' => Illuminate\Support\Facades\Redirect::class,
|
||||||
'Redis' => Illuminate\Support\Facades\Redis::class,
|
'Redis' => Illuminate\Support\Facades\Redis::class,
|
||||||
'Request' => Illuminate\Support\Facades\Request::class,
|
'Request' => Illuminate\Support\Facades\Request::class,
|
||||||
'Response' => Illuminate\Support\Facades\Response::class,
|
'Response' => Illuminate\Support\Facades\Response::class,
|
||||||
'Route' => Illuminate\Support\Facades\Route::class,
|
'Route' => Illuminate\Support\Facades\Route::class,
|
||||||
'Schema' => Illuminate\Support\Facades\Schema::class,
|
'Schema' => Illuminate\Support\Facades\Schema::class,
|
||||||
'Session' => Illuminate\Support\Facades\Session::class,
|
'Session' => Illuminate\Support\Facades\Session::class,
|
||||||
'Storage' => Illuminate\Support\Facades\Storage::class,
|
'Storage' => Illuminate\Support\Facades\Storage::class,
|
||||||
'Str' => Illuminate\Support\Str::class,
|
'Str' => Illuminate\Support\Str::class,
|
||||||
'URL' => Illuminate\Support\Facades\URL::class,
|
'URL' => Illuminate\Support\Facades\URL::class,
|
||||||
'Validator' => Illuminate\Support\Facades\Validator::class,
|
'Validator' => Illuminate\Support\Facades\Validator::class,
|
||||||
'View' => Illuminate\Support\Facades\View::class,
|
'View' => Illuminate\Support\Facades\View::class,
|
||||||
'Socialite' => Laravel\Socialite\Facades\Socialite::class,
|
'Socialite' => Laravel\Socialite\Facades\Socialite::class,
|
||||||
|
|
||||||
// Third Party
|
// Third Party
|
||||||
'ImageTool' => Intervention\Image\Facades\Image::class,
|
'ImageTool' => Intervention\Image\Facades\Image::class,
|
||||||
'DomPDF' => Barryvdh\DomPDF\Facade::class,
|
'DomPDF' => Barryvdh\DomPDF\Facade::class,
|
||||||
'SnappyPDF' => Barryvdh\Snappy\Facades\SnappyPdf::class,
|
'SnappyPDF' => Barryvdh\Snappy\Facades\SnappyPdf::class,
|
||||||
|
|
||||||
// Custom BookStack
|
// Custom BookStack
|
||||||
'Activity' => BookStack\Facades\Activity::class,
|
'Activity' => BookStack\Facades\Activity::class,
|
||||||
'Permissions' => BookStack\Facades\Permissions::class,
|
'Permissions' => BookStack\Facades\Permissions::class,
|
||||||
'Theme' => BookStack\Facades\Theme::class,
|
'Theme' => BookStack\Facades\Theme::class,
|
||||||
],
|
],
|
||||||
|
|
||||||
// Proxy configuration
|
// Proxy configuration
|
||||||
|
|
|
@ -18,7 +18,7 @@ return [
|
||||||
// This option controls the default authentication "guard" and password
|
// This option controls the default authentication "guard" and password
|
||||||
// reset options for your application.
|
// reset options for your application.
|
||||||
'defaults' => [
|
'defaults' => [
|
||||||
'guard' => env('AUTH_METHOD', 'standard'),
|
'guard' => env('AUTH_METHOD', 'standard'),
|
||||||
'passwords' => 'users',
|
'passwords' => 'users',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
@ -29,15 +29,15 @@ return [
|
||||||
// Supported drivers: "session", "api-token", "ldap-session"
|
// Supported drivers: "session", "api-token", "ldap-session"
|
||||||
'guards' => [
|
'guards' => [
|
||||||
'standard' => [
|
'standard' => [
|
||||||
'driver' => 'session',
|
'driver' => 'session',
|
||||||
'provider' => 'users',
|
'provider' => 'users',
|
||||||
],
|
],
|
||||||
'ldap' => [
|
'ldap' => [
|
||||||
'driver' => 'ldap-session',
|
'driver' => 'ldap-session',
|
||||||
'provider' => 'external',
|
'provider' => 'external',
|
||||||
],
|
],
|
||||||
'saml2' => [
|
'saml2' => [
|
||||||
'driver' => 'saml2-session',
|
'driver' => 'saml2-session',
|
||||||
'provider' => 'external',
|
'provider' => 'external',
|
||||||
],
|
],
|
||||||
'api' => [
|
'api' => [
|
||||||
|
@ -52,11 +52,11 @@ return [
|
||||||
'providers' => [
|
'providers' => [
|
||||||
'users' => [
|
'users' => [
|
||||||
'driver' => 'eloquent',
|
'driver' => 'eloquent',
|
||||||
'model' => \BookStack\Auth\User::class,
|
'model' => \BookStack\Auth\User::class,
|
||||||
],
|
],
|
||||||
'external' => [
|
'external' => [
|
||||||
'driver' => 'external-users',
|
'driver' => 'external-users',
|
||||||
'model' => \BookStack\Auth\User::class,
|
'model' => \BookStack\Auth\User::class,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
|
@ -67,9 +67,9 @@ return [
|
||||||
'passwords' => [
|
'passwords' => [
|
||||||
'users' => [
|
'users' => [
|
||||||
'provider' => 'users',
|
'provider' => 'users',
|
||||||
'email' => 'emails.password',
|
'email' => 'emails.password',
|
||||||
'table' => 'password_resets',
|
'table' => 'password_resets',
|
||||||
'expire' => 60,
|
'expire' => 60,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
|
@ -23,18 +23,18 @@ return [
|
||||||
'connections' => [
|
'connections' => [
|
||||||
|
|
||||||
'pusher' => [
|
'pusher' => [
|
||||||
'driver' => 'pusher',
|
'driver' => 'pusher',
|
||||||
'key' => env('PUSHER_APP_KEY'),
|
'key' => env('PUSHER_APP_KEY'),
|
||||||
'secret' => env('PUSHER_APP_SECRET'),
|
'secret' => env('PUSHER_APP_SECRET'),
|
||||||
'app_id' => env('PUSHER_APP_ID'),
|
'app_id' => env('PUSHER_APP_ID'),
|
||||||
'options' => [
|
'options' => [
|
||||||
'cluster' => env('PUSHER_APP_CLUSTER'),
|
'cluster' => env('PUSHER_APP_CLUSTER'),
|
||||||
'useTLS' => true,
|
'useTLS' => true,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
'redis' => [
|
'redis' => [
|
||||||
'driver' => 'redis',
|
'driver' => 'redis',
|
||||||
'connection' => 'default',
|
'connection' => 'default',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
@ -46,7 +46,6 @@ return [
|
||||||
'driver' => 'null',
|
'driver' => 'null',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
||||||
],
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
|
@ -42,8 +42,8 @@ return [
|
||||||
],
|
],
|
||||||
|
|
||||||
'database' => [
|
'database' => [
|
||||||
'driver' => 'database',
|
'driver' => 'database',
|
||||||
'table' => 'cache',
|
'table' => 'cache',
|
||||||
'connection' => null,
|
'connection' => null,
|
||||||
],
|
],
|
||||||
|
|
||||||
|
@ -58,7 +58,7 @@ return [
|
||||||
],
|
],
|
||||||
|
|
||||||
'redis' => [
|
'redis' => [
|
||||||
'driver' => 'redis',
|
'driver' => 'redis',
|
||||||
'connection' => 'default',
|
'connection' => 'default',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
|
@ -59,38 +59,38 @@ return [
|
||||||
'connections' => [
|
'connections' => [
|
||||||
|
|
||||||
'mysql' => [
|
'mysql' => [
|
||||||
'driver' => 'mysql',
|
'driver' => 'mysql',
|
||||||
'url' => env('DATABASE_URL'),
|
'url' => env('DATABASE_URL'),
|
||||||
'host' => $mysql_host,
|
'host' => $mysql_host,
|
||||||
'database' => env('DB_DATABASE', 'forge'),
|
'database' => env('DB_DATABASE', 'forge'),
|
||||||
'username' => env('DB_USERNAME', 'forge'),
|
'username' => env('DB_USERNAME', 'forge'),
|
||||||
'password' => env('DB_PASSWORD', ''),
|
'password' => env('DB_PASSWORD', ''),
|
||||||
'unix_socket' => env('DB_SOCKET', ''),
|
'unix_socket' => env('DB_SOCKET', ''),
|
||||||
'port' => $mysql_port,
|
'port' => $mysql_port,
|
||||||
'charset' => 'utf8mb4',
|
'charset' => 'utf8mb4',
|
||||||
'collation' => 'utf8mb4_unicode_ci',
|
'collation' => 'utf8mb4_unicode_ci',
|
||||||
'prefix' => '',
|
'prefix' => '',
|
||||||
'prefix_indexes' => true,
|
'prefix_indexes' => true,
|
||||||
'strict' => false,
|
'strict' => false,
|
||||||
'engine' => null,
|
'engine' => null,
|
||||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||||
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
|
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
|
||||||
]) : [],
|
]) : [],
|
||||||
],
|
],
|
||||||
|
|
||||||
'mysql_testing' => [
|
'mysql_testing' => [
|
||||||
'driver' => 'mysql',
|
'driver' => 'mysql',
|
||||||
'url' => env('TEST_DATABASE_URL'),
|
'url' => env('TEST_DATABASE_URL'),
|
||||||
'host' => '127.0.0.1',
|
'host' => '127.0.0.1',
|
||||||
'database' => 'bookstack-test',
|
'database' => 'bookstack-test',
|
||||||
'username' => env('MYSQL_USER', 'bookstack-test'),
|
'username' => env('MYSQL_USER', 'bookstack-test'),
|
||||||
'password' => env('MYSQL_PASSWORD', 'bookstack-test'),
|
'password' => env('MYSQL_PASSWORD', 'bookstack-test'),
|
||||||
'port' => $mysql_port,
|
'port' => $mysql_port,
|
||||||
'charset' => 'utf8mb4',
|
'charset' => 'utf8mb4',
|
||||||
'collation' => 'utf8mb4_unicode_ci',
|
'collation' => 'utf8mb4_unicode_ci',
|
||||||
'prefix' => '',
|
'prefix' => '',
|
||||||
'prefix_indexes' => true,
|
'prefix_indexes' => true,
|
||||||
'strict' => false,
|
'strict' => false,
|
||||||
],
|
],
|
||||||
|
|
||||||
],
|
],
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Debugbar Configuration Options
|
* Debugbar Configuration Options.
|
||||||
*
|
*
|
||||||
* Changes to these config files are not supported by BookStack and may break upon updates.
|
* Changes to these config files are not supported by BookStack and may break upon updates.
|
||||||
* Configuration should be altered via the `.env` file or environment variables.
|
* Configuration should be altered via the `.env` file or environment variables.
|
||||||
|
@ -10,53 +10,52 @@
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
// Debugbar is enabled by default, when debug is set to true in app.php.
|
// Debugbar is enabled by default, when debug is set to true in app.php.
|
||||||
// You can override the value by setting enable to true or false instead of null.
|
// You can override the value by setting enable to true or false instead of null.
|
||||||
//
|
//
|
||||||
// You can provide an array of URI's that must be ignored (eg. 'api/*')
|
// You can provide an array of URI's that must be ignored (eg. 'api/*')
|
||||||
'enabled' => env('DEBUGBAR_ENABLED', false),
|
'enabled' => env('DEBUGBAR_ENABLED', false),
|
||||||
'except' => [
|
'except' => [
|
||||||
'telescope*'
|
'telescope*',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
// DebugBar stores data for session/ajax requests.
|
||||||
// DebugBar stores data for session/ajax requests.
|
// You can disable this, so the debugbar stores data in headers/session,
|
||||||
// You can disable this, so the debugbar stores data in headers/session,
|
// but this can cause problems with large data collectors.
|
||||||
// but this can cause problems with large data collectors.
|
// By default, file storage (in the storage folder) is used. Redis and PDO
|
||||||
// By default, file storage (in the storage folder) is used. Redis and PDO
|
// can also be used. For PDO, run the package migrations first.
|
||||||
// can also be used. For PDO, run the package migrations first.
|
|
||||||
'storage' => [
|
'storage' => [
|
||||||
'enabled' => true,
|
'enabled' => true,
|
||||||
'driver' => 'file', // redis, file, pdo, custom
|
'driver' => 'file', // redis, file, pdo, custom
|
||||||
'path' => storage_path('debugbar'), // For file driver
|
'path' => storage_path('debugbar'), // For file driver
|
||||||
'connection' => null, // Leave null for default connection (Redis/PDO)
|
'connection' => null, // Leave null for default connection (Redis/PDO)
|
||||||
'provider' => '' // Instance of StorageInterface for custom driver
|
'provider' => '', // Instance of StorageInterface for custom driver
|
||||||
],
|
],
|
||||||
|
|
||||||
// Vendor files are included by default, but can be set to false.
|
// Vendor files are included by default, but can be set to false.
|
||||||
// This can also be set to 'js' or 'css', to only include javascript or css vendor files.
|
// This can also be set to 'js' or 'css', to only include javascript or css vendor files.
|
||||||
// Vendor files are for css: font-awesome (including fonts) and highlight.js (css files)
|
// Vendor files are for css: font-awesome (including fonts) and highlight.js (css files)
|
||||||
// and for js: jquery and and highlight.js
|
// and for js: jquery and and highlight.js
|
||||||
// So if you want syntax highlighting, set it to true.
|
// So if you want syntax highlighting, set it to true.
|
||||||
// jQuery is set to not conflict with existing jQuery scripts.
|
// jQuery is set to not conflict with existing jQuery scripts.
|
||||||
'include_vendors' => true,
|
'include_vendors' => true,
|
||||||
|
|
||||||
// The Debugbar can capture Ajax requests and display them. If you don't want this (ie. because of errors),
|
// The Debugbar can capture Ajax requests and display them. If you don't want this (ie. because of errors),
|
||||||
// you can use this option to disable sending the data through the headers.
|
// you can use this option to disable sending the data through the headers.
|
||||||
// Optionally, you can also send ServerTiming headers on ajax requests for the Chrome DevTools.
|
// Optionally, you can also send ServerTiming headers on ajax requests for the Chrome DevTools.
|
||||||
|
|
||||||
'capture_ajax' => true,
|
'capture_ajax' => true,
|
||||||
'add_ajax_timing' => false,
|
'add_ajax_timing' => false,
|
||||||
|
|
||||||
// When enabled, the Debugbar shows deprecated warnings for Symfony components
|
// When enabled, the Debugbar shows deprecated warnings for Symfony components
|
||||||
// in the Messages tab.
|
// in the Messages tab.
|
||||||
'error_handler' => false,
|
'error_handler' => false,
|
||||||
|
|
||||||
// The Debugbar can emulate the Clockwork headers, so you can use the Chrome
|
// The Debugbar can emulate the Clockwork headers, so you can use the Chrome
|
||||||
// Extension, without the server-side code. It uses Debugbar collectors instead.
|
// Extension, without the server-side code. It uses Debugbar collectors instead.
|
||||||
'clockwork' => false,
|
'clockwork' => false,
|
||||||
|
|
||||||
// Enable/disable DataCollectors
|
// Enable/disable DataCollectors
|
||||||
'collectors' => [
|
'collectors' => [
|
||||||
'phpinfo' => true, // Php version
|
'phpinfo' => true, // Php version
|
||||||
'messages' => true, // Messages
|
'messages' => true, // Messages
|
||||||
|
@ -82,7 +81,7 @@ return [
|
||||||
'models' => true, // Display models
|
'models' => true, // Display models
|
||||||
],
|
],
|
||||||
|
|
||||||
// Configure some DataCollectors
|
// Configure some DataCollectors
|
||||||
'options' => [
|
'options' => [
|
||||||
'auth' => [
|
'auth' => [
|
||||||
'show_name' => true, // Also show the users name/email in the debugbar
|
'show_name' => true, // Also show the users name/email in the debugbar
|
||||||
|
@ -91,43 +90,43 @@ return [
|
||||||
'with_params' => true, // Render SQL with the parameters substituted
|
'with_params' => true, // Render SQL with the parameters substituted
|
||||||
'backtrace' => true, // Use a backtrace to find the origin of the query in your files.
|
'backtrace' => true, // Use a backtrace to find the origin of the query in your files.
|
||||||
'timeline' => false, // Add the queries to the timeline
|
'timeline' => false, // Add the queries to the timeline
|
||||||
'explain' => [ // Show EXPLAIN output on queries
|
'explain' => [ // Show EXPLAIN output on queries
|
||||||
'enabled' => false,
|
'enabled' => false,
|
||||||
'types' => ['SELECT'], // ['SELECT', 'INSERT', 'UPDATE', 'DELETE']; for MySQL 5.6.3+
|
'types' => ['SELECT'], // ['SELECT', 'INSERT', 'UPDATE', 'DELETE']; for MySQL 5.6.3+
|
||||||
],
|
],
|
||||||
'hints' => true, // Show hints for common mistakes
|
'hints' => true, // Show hints for common mistakes
|
||||||
],
|
],
|
||||||
'mail' => [
|
'mail' => [
|
||||||
'full_log' => false
|
'full_log' => false,
|
||||||
],
|
],
|
||||||
'views' => [
|
'views' => [
|
||||||
'data' => false, //Note: Can slow down the application, because the data can be quite large..
|
'data' => false, //Note: Can slow down the application, because the data can be quite large..
|
||||||
],
|
],
|
||||||
'route' => [
|
'route' => [
|
||||||
'label' => true // show complete route on bar
|
'label' => true, // show complete route on bar
|
||||||
],
|
],
|
||||||
'logs' => [
|
'logs' => [
|
||||||
'file' => null
|
'file' => null,
|
||||||
],
|
],
|
||||||
'cache' => [
|
'cache' => [
|
||||||
'values' => true // collect cache values
|
'values' => true, // collect cache values
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
// Inject Debugbar into the response
|
// Inject Debugbar into the response
|
||||||
// Usually, the debugbar is added just before </body>, by listening to the
|
// Usually, the debugbar is added just before </body>, by listening to the
|
||||||
// Response after the App is done. If you disable this, you have to add them
|
// Response after the App is done. If you disable this, you have to add them
|
||||||
// in your template yourself. See http://phpdebugbar.com/docs/rendering.html
|
// in your template yourself. See http://phpdebugbar.com/docs/rendering.html
|
||||||
'inject' => true,
|
'inject' => true,
|
||||||
|
|
||||||
// DebugBar route prefix
|
// DebugBar route prefix
|
||||||
// Sometimes you want to set route prefix to be used by DebugBar to load
|
// Sometimes you want to set route prefix to be used by DebugBar to load
|
||||||
// its resources from. Usually the need comes from misconfigured web server or
|
// its resources from. Usually the need comes from misconfigured web server or
|
||||||
// from trying to overcome bugs like this: http://trac.nginx.org/nginx/ticket/97
|
// from trying to overcome bugs like this: http://trac.nginx.org/nginx/ticket/97
|
||||||
'route_prefix' => '_debugbar',
|
'route_prefix' => '_debugbar',
|
||||||
|
|
||||||
// DebugBar route domain
|
// DebugBar route domain
|
||||||
// By default DebugBar route served from the same domain that request served.
|
// By default DebugBar route served from the same domain that request served.
|
||||||
// To override default domain, specify it as a non-empty value.
|
// To override default domain, specify it as a non-empty value.
|
||||||
'route_domain' => env('APP_URL', '') === 'http://bookstack.dev' ? '' : env('APP_URL', ''),
|
'route_domain' => env('APP_URL', '') === 'http://bookstack.dev' ? '' : env('APP_URL', ''),
|
||||||
];
|
];
|
||||||
|
|
|
@ -10,12 +10,11 @@
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
|
|
||||||
'show_warnings' => false, // Throw an Exception on warnings from dompdf
|
'show_warnings' => false, // Throw an Exception on warnings from dompdf
|
||||||
'orientation' => 'portrait',
|
'orientation' => 'portrait',
|
||||||
'defines' => [
|
'defines' => [
|
||||||
/**
|
/**
|
||||||
* The location of the DOMPDF font directory
|
* The location of the DOMPDF font directory.
|
||||||
*
|
*
|
||||||
* The location of the directory where DOMPDF will store fonts and font metrics
|
* The location of the directory where DOMPDF will store fonts and font metrics
|
||||||
* Note: This directory must exist and be writable by the webserver process.
|
* Note: This directory must exist and be writable by the webserver process.
|
||||||
|
@ -38,17 +37,17 @@ return [
|
||||||
* Times-Roman, Times-Bold, Times-BoldItalic, Times-Italic,
|
* Times-Roman, Times-Bold, Times-BoldItalic, Times-Italic,
|
||||||
* Symbol, ZapfDingbats.
|
* Symbol, ZapfDingbats.
|
||||||
*/
|
*/
|
||||||
"DOMPDF_FONT_DIR" => storage_path('fonts/'), // advised by dompdf (https://github.com/dompdf/dompdf/pull/782)
|
'font_dir' => storage_path('fonts/'), // advised by dompdf (https://github.com/dompdf/dompdf/pull/782)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The location of the DOMPDF font cache directory
|
* The location of the DOMPDF font cache directory.
|
||||||
*
|
*
|
||||||
* This directory contains the cached font metrics for the fonts used by DOMPDF.
|
* This directory contains the cached font metrics for the fonts used by DOMPDF.
|
||||||
* This directory can be the same as DOMPDF_FONT_DIR
|
* This directory can be the same as DOMPDF_FONT_DIR
|
||||||
*
|
*
|
||||||
* Note: This directory must exist and be writable by the webserver process.
|
* Note: This directory must exist and be writable by the webserver process.
|
||||||
*/
|
*/
|
||||||
"DOMPDF_FONT_CACHE" => storage_path('fonts/'),
|
'font_cache' => storage_path('fonts/'),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The location of a temporary directory.
|
* The location of a temporary directory.
|
||||||
|
@ -57,10 +56,10 @@ return [
|
||||||
* The temporary directory is required to download remote images and when
|
* The temporary directory is required to download remote images and when
|
||||||
* using the PFDLib back end.
|
* using the PFDLib back end.
|
||||||
*/
|
*/
|
||||||
"DOMPDF_TEMP_DIR" => sys_get_temp_dir(),
|
'temp_dir' => sys_get_temp_dir(),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ==== IMPORTANT ====
|
* ==== IMPORTANT ====.
|
||||||
*
|
*
|
||||||
* dompdf's "chroot": Prevents dompdf from accessing system files or other
|
* dompdf's "chroot": Prevents dompdf from accessing system files or other
|
||||||
* files on the webserver. All local files opened by dompdf must be in a
|
* files on the webserver. All local files opened by dompdf must be in a
|
||||||
|
@ -71,7 +70,7 @@ return [
|
||||||
* direct class use like:
|
* direct class use like:
|
||||||
* $dompdf = new DOMPDF(); $dompdf->load_html($htmldata); $dompdf->render(); $pdfdata = $dompdf->output();
|
* $dompdf = new DOMPDF(); $dompdf->load_html($htmldata); $dompdf->render(); $pdfdata = $dompdf->output();
|
||||||
*/
|
*/
|
||||||
"DOMPDF_CHROOT" => realpath(base_path()),
|
'chroot' => realpath(base_path()),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether to use Unicode fonts or not.
|
* Whether to use Unicode fonts or not.
|
||||||
|
@ -82,20 +81,19 @@ return [
|
||||||
* When enabled, dompdf can support all Unicode glyphs. Any glyphs used in a
|
* When enabled, dompdf can support all Unicode glyphs. Any glyphs used in a
|
||||||
* document must be present in your fonts, however.
|
* document must be present in your fonts, however.
|
||||||
*/
|
*/
|
||||||
"DOMPDF_UNICODE_ENABLED" => true,
|
'unicode_enabled' => true,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether to enable font subsetting or not.
|
* Whether to enable font subsetting or not.
|
||||||
*/
|
*/
|
||||||
"DOMPDF_ENABLE_FONTSUBSETTING" => false,
|
'enable_fontsubsetting' => false,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The PDF rendering backend to use
|
* The PDF rendering backend to use.
|
||||||
*
|
*
|
||||||
* Valid settings are 'PDFLib', 'CPDF' (the bundled R&OS PDF class), 'GD' and
|
* Valid settings are 'PDFLib', 'CPDF' (the bundled R&OS PDF class), 'GD' and
|
||||||
* 'auto'. 'auto' will look for PDFLib and use it if found, or if not it will
|
* 'auto'. 'auto' will look for PDFLib and use it if found, or if not it will
|
||||||
* fall back on CPDF. 'GD' renders PDFs to graphic files. {@link
|
* fall back on CPDF. 'GD' renders PDFs to graphic files. {@link * Canvas_Factory} ultimately determines which rendering class to instantiate
|
||||||
* Canvas_Factory} ultimately determines which rendering class to instantiate
|
|
||||||
* based on this setting.
|
* based on this setting.
|
||||||
*
|
*
|
||||||
* Both PDFLib & CPDF rendering backends provide sufficient rendering
|
* Both PDFLib & CPDF rendering backends provide sufficient rendering
|
||||||
|
@ -117,10 +115,10 @@ return [
|
||||||
* @link http://www.ros.co.nz/pdf
|
* @link http://www.ros.co.nz/pdf
|
||||||
* @link http://www.php.net/image
|
* @link http://www.php.net/image
|
||||||
*/
|
*/
|
||||||
"DOMPDF_PDF_BACKEND" => "CPDF",
|
'pdf_backend' => 'CPDF',
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PDFlib license key
|
* PDFlib license key.
|
||||||
*
|
*
|
||||||
* If you are using a licensed, commercial version of PDFlib, specify
|
* If you are using a licensed, commercial version of PDFlib, specify
|
||||||
* your license key here. If you are using PDFlib-Lite or are evaluating
|
* your license key here. If you are using PDFlib-Lite or are evaluating
|
||||||
|
@ -143,7 +141,7 @@ return [
|
||||||
* the desired content might be different (e.g. screen or projection view of html file).
|
* the desired content might be different (e.g. screen or projection view of html file).
|
||||||
* Therefore allow specification of content here.
|
* Therefore allow specification of content here.
|
||||||
*/
|
*/
|
||||||
"DOMPDF_DEFAULT_MEDIA_TYPE" => "print",
|
'default_media_type' => 'print',
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The default paper size.
|
* The default paper size.
|
||||||
|
@ -152,18 +150,19 @@ return [
|
||||||
*
|
*
|
||||||
* @see CPDF_Adapter::PAPER_SIZES for valid sizes ('letter', 'legal', 'A4', etc.)
|
* @see CPDF_Adapter::PAPER_SIZES for valid sizes ('letter', 'legal', 'A4', etc.)
|
||||||
*/
|
*/
|
||||||
"DOMPDF_DEFAULT_PAPER_SIZE" => "a4",
|
'default_paper_size' => 'a4',
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The default font family
|
* The default font family.
|
||||||
*
|
*
|
||||||
* Used if no suitable fonts can be found. This must exist in the font folder.
|
* Used if no suitable fonts can be found. This must exist in the font folder.
|
||||||
|
*
|
||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
"DOMPDF_DEFAULT_FONT" => "dejavu sans",
|
'default_font' => 'dejavu sans',
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Image DPI setting
|
* Image DPI setting.
|
||||||
*
|
*
|
||||||
* This setting determines the default DPI setting for images and fonts. The
|
* This setting determines the default DPI setting for images and fonts. The
|
||||||
* DPI may be overridden for inline images by explictly setting the
|
* DPI may be overridden for inline images by explictly setting the
|
||||||
|
@ -195,10 +194,10 @@ return [
|
||||||
*
|
*
|
||||||
* @var int
|
* @var int
|
||||||
*/
|
*/
|
||||||
"DOMPDF_DPI" => 96,
|
'dpi' => 96,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enable inline PHP
|
* Enable inline PHP.
|
||||||
*
|
*
|
||||||
* If this setting is set to true then DOMPDF will automatically evaluate
|
* If this setting is set to true then DOMPDF will automatically evaluate
|
||||||
* inline PHP contained within <script type="text/php"> ... </script> tags.
|
* inline PHP contained within <script type="text/php"> ... </script> tags.
|
||||||
|
@ -209,20 +208,20 @@ return [
|
||||||
*
|
*
|
||||||
* @var bool
|
* @var bool
|
||||||
*/
|
*/
|
||||||
"DOMPDF_ENABLE_PHP" => false,
|
'enable_php' => false,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enable inline Javascript
|
* Enable inline Javascript.
|
||||||
*
|
*
|
||||||
* If this setting is set to true then DOMPDF will automatically insert
|
* If this setting is set to true then DOMPDF will automatically insert
|
||||||
* JavaScript code contained within <script type="text/javascript"> ... </script> tags.
|
* JavaScript code contained within <script type="text/javascript"> ... </script> tags.
|
||||||
*
|
*
|
||||||
* @var bool
|
* @var bool
|
||||||
*/
|
*/
|
||||||
"DOMPDF_ENABLE_JAVASCRIPT" => false,
|
'enable_javascript' => false,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enable remote file access
|
* Enable remote file access.
|
||||||
*
|
*
|
||||||
* If this setting is set to true, DOMPDF will access remote sites for
|
* If this setting is set to true, DOMPDF will access remote sites for
|
||||||
* images and CSS files as required.
|
* images and CSS files as required.
|
||||||
|
@ -238,29 +237,27 @@ return [
|
||||||
*
|
*
|
||||||
* @var bool
|
* @var bool
|
||||||
*/
|
*/
|
||||||
"DOMPDF_ENABLE_REMOTE" => true,
|
'enable_remote' => env('ALLOW_UNTRUSTED_SERVER_FETCHING', false),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A ratio applied to the fonts height to be more like browsers' line height
|
* A ratio applied to the fonts height to be more like browsers' line height.
|
||||||
*/
|
*/
|
||||||
"DOMPDF_FONT_HEIGHT_RATIO" => 1.1,
|
'font_height_ratio' => 1.1,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enable CSS float
|
* Enable CSS float.
|
||||||
*
|
*
|
||||||
* Allows people to disabled CSS float support
|
* Allows people to disabled CSS float support
|
||||||
|
*
|
||||||
* @var bool
|
* @var bool
|
||||||
*/
|
*/
|
||||||
"DOMPDF_ENABLE_CSS_FLOAT" => true,
|
'enable_css_float' => true,
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use the more-than-experimental HTML5 Lib parser
|
* Use the more-than-experimental HTML5 Lib parser.
|
||||||
*/
|
*/
|
||||||
"DOMPDF_ENABLE_HTML5PARSER" => true,
|
'enable_html5parser' => true,
|
||||||
|
|
||||||
|
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
|
@ -34,7 +34,7 @@ return [
|
||||||
|
|
||||||
'local' => [
|
'local' => [
|
||||||
'driver' => 'local',
|
'driver' => 'local',
|
||||||
'root' => public_path(),
|
'root' => public_path(),
|
||||||
],
|
],
|
||||||
|
|
||||||
'local_secure' => [
|
'local_secure' => [
|
||||||
|
@ -43,12 +43,12 @@ return [
|
||||||
],
|
],
|
||||||
|
|
||||||
's3' => [
|
's3' => [
|
||||||
'driver' => 's3',
|
'driver' => 's3',
|
||||||
'key' => env('STORAGE_S3_KEY', 'your-key'),
|
'key' => env('STORAGE_S3_KEY', 'your-key'),
|
||||||
'secret' => env('STORAGE_S3_SECRET', 'your-secret'),
|
'secret' => env('STORAGE_S3_SECRET', 'your-secret'),
|
||||||
'region' => env('STORAGE_S3_REGION', 'your-region'),
|
'region' => env('STORAGE_S3_REGION', 'your-region'),
|
||||||
'bucket' => env('STORAGE_S3_BUCKET', 'your-bucket'),
|
'bucket' => env('STORAGE_S3_BUCKET', 'your-bucket'),
|
||||||
'endpoint' => env('STORAGE_S3_ENDPOINT', null),
|
'endpoint' => env('STORAGE_S3_ENDPOINT', null),
|
||||||
'use_path_style_endpoint' => env('STORAGE_S3_ENDPOINT', null) !== null,
|
'use_path_style_endpoint' => env('STORAGE_S3_ENDPOINT', null) !== null,
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
|
@ -29,9 +29,9 @@ return [
|
||||||
// passwords are hashed using the Argon algorithm. These will allow you
|
// passwords are hashed using the Argon algorithm. These will allow you
|
||||||
// to control the amount of time it takes to hash the given password.
|
// to control the amount of time it takes to hash the given password.
|
||||||
'argon' => [
|
'argon' => [
|
||||||
'memory' => 1024,
|
'memory' => 1024,
|
||||||
'threads' => 2,
|
'threads' => 2,
|
||||||
'time' => 2,
|
'time' => 2,
|
||||||
],
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
|
@ -30,66 +30,66 @@ return [
|
||||||
// "custom", "stack"
|
// "custom", "stack"
|
||||||
'channels' => [
|
'channels' => [
|
||||||
'stack' => [
|
'stack' => [
|
||||||
'driver' => 'stack',
|
'driver' => 'stack',
|
||||||
'channels' => ['daily'],
|
'channels' => ['daily'],
|
||||||
'ignore_exceptions' => false,
|
'ignore_exceptions' => false,
|
||||||
],
|
],
|
||||||
|
|
||||||
'single' => [
|
'single' => [
|
||||||
'driver' => 'single',
|
'driver' => 'single',
|
||||||
'path' => storage_path('logs/laravel.log'),
|
'path' => storage_path('logs/laravel.log'),
|
||||||
'level' => 'debug',
|
'level' => 'debug',
|
||||||
'days' => 14,
|
'days' => 14,
|
||||||
],
|
],
|
||||||
|
|
||||||
'daily' => [
|
'daily' => [
|
||||||
'driver' => 'daily',
|
'driver' => 'daily',
|
||||||
'path' => storage_path('logs/laravel.log'),
|
'path' => storage_path('logs/laravel.log'),
|
||||||
'level' => 'debug',
|
'level' => 'debug',
|
||||||
'days' => 7,
|
'days' => 7,
|
||||||
],
|
],
|
||||||
|
|
||||||
'slack' => [
|
'slack' => [
|
||||||
'driver' => 'slack',
|
'driver' => 'slack',
|
||||||
'url' => env('LOG_SLACK_WEBHOOK_URL'),
|
'url' => env('LOG_SLACK_WEBHOOK_URL'),
|
||||||
'username' => 'Laravel Log',
|
'username' => 'Laravel Log',
|
||||||
'emoji' => ':boom:',
|
'emoji' => ':boom:',
|
||||||
'level' => 'critical',
|
'level' => 'critical',
|
||||||
],
|
],
|
||||||
|
|
||||||
'stderr' => [
|
'stderr' => [
|
||||||
'driver' => 'monolog',
|
'driver' => 'monolog',
|
||||||
'handler' => StreamHandler::class,
|
'handler' => StreamHandler::class,
|
||||||
'with' => [
|
'with' => [
|
||||||
'stream' => 'php://stderr',
|
'stream' => 'php://stderr',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
'syslog' => [
|
'syslog' => [
|
||||||
'driver' => 'syslog',
|
'driver' => 'syslog',
|
||||||
'level' => 'debug',
|
'level' => 'debug',
|
||||||
],
|
],
|
||||||
|
|
||||||
'errorlog' => [
|
'errorlog' => [
|
||||||
'driver' => 'errorlog',
|
'driver' => 'errorlog',
|
||||||
'level' => 'debug',
|
'level' => 'debug',
|
||||||
],
|
],
|
||||||
|
|
||||||
// Custom errorlog implementation that logs out a plain,
|
// Custom errorlog implementation that logs out a plain,
|
||||||
// non-formatted message intended for the webserver log.
|
// non-formatted message intended for the webserver log.
|
||||||
'errorlog_plain_webserver' => [
|
'errorlog_plain_webserver' => [
|
||||||
'driver' => 'monolog',
|
'driver' => 'monolog',
|
||||||
'level' => 'debug',
|
'level' => 'debug',
|
||||||
'handler' => ErrorLogHandler::class,
|
'handler' => ErrorLogHandler::class,
|
||||||
'handler_with' => [4],
|
'handler_with' => [4],
|
||||||
'formatter' => LineFormatter::class,
|
'formatter' => LineFormatter::class,
|
||||||
'formatter_with' => [
|
'formatter_with' => [
|
||||||
'format' => "%message%",
|
'format' => '%message%',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
'null' => [
|
'null' => [
|
||||||
'driver' => 'monolog',
|
'driver' => 'monolog',
|
||||||
'handler' => NullHandler::class,
|
'handler' => NullHandler::class,
|
||||||
],
|
],
|
||||||
|
|
||||||
|
@ -101,7 +101,6 @@ return [
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
||||||
// Failed Login Message
|
// Failed Login Message
|
||||||
// Allows a configurable message to be logged when a login request fails.
|
// Allows a configurable message to be logged when a login request fails.
|
||||||
'failed_login' => [
|
'failed_login' => [
|
||||||
|
|
|
@ -23,7 +23,7 @@ return [
|
||||||
// Global "From" address & name
|
// Global "From" address & name
|
||||||
'from' => [
|
'from' => [
|
||||||
'address' => env('MAIL_FROM', 'mail@bookstackapp.com'),
|
'address' => env('MAIL_FROM', 'mail@bookstackapp.com'),
|
||||||
'name' => env('MAIL_FROM_NAME', 'BookStack')
|
'name' => env('MAIL_FROM_NAME', 'BookStack'),
|
||||||
],
|
],
|
||||||
|
|
||||||
// Email encryption protocol
|
// Email encryption protocol
|
||||||
|
|
|
@ -17,24 +17,23 @@ return [
|
||||||
// Queue connection configuration
|
// Queue connection configuration
|
||||||
'connections' => [
|
'connections' => [
|
||||||
|
|
||||||
|
|
||||||
'sync' => [
|
'sync' => [
|
||||||
'driver' => 'sync',
|
'driver' => 'sync',
|
||||||
],
|
],
|
||||||
|
|
||||||
'database' => [
|
'database' => [
|
||||||
'driver' => 'database',
|
'driver' => 'database',
|
||||||
'table' => 'jobs',
|
'table' => 'jobs',
|
||||||
'queue' => 'default',
|
'queue' => 'default',
|
||||||
'retry_after' => 90,
|
'retry_after' => 90,
|
||||||
],
|
],
|
||||||
|
|
||||||
'redis' => [
|
'redis' => [
|
||||||
'driver' => 'redis',
|
'driver' => 'redis',
|
||||||
'connection' => 'default',
|
'connection' => 'default',
|
||||||
'queue' => env('REDIS_QUEUE', 'default'),
|
'queue' => env('REDIS_QUEUE', 'default'),
|
||||||
'retry_after' => 90,
|
'retry_after' => 90,
|
||||||
'block_for' => null,
|
'block_for' => null,
|
||||||
],
|
],
|
||||||
|
|
||||||
],
|
],
|
||||||
|
|
|
@ -31,7 +31,6 @@ return [
|
||||||
// Overrides, in JSON format, to the configuration passed to underlying onelogin library.
|
// Overrides, in JSON format, to the configuration passed to underlying onelogin library.
|
||||||
'onelogin_overrides' => env('SAML2_ONELOGIN_OVERRIDES', null),
|
'onelogin_overrides' => env('SAML2_ONELOGIN_OVERRIDES', null),
|
||||||
|
|
||||||
|
|
||||||
'onelogin' => [
|
'onelogin' => [
|
||||||
// If 'strict' is True, then the PHP Toolkit will reject unsigned
|
// If 'strict' is True, then the PHP Toolkit will reject unsigned
|
||||||
// or unencrypted messages if it expects them signed or encrypted
|
// or unencrypted messages if it expects them signed or encrypted
|
||||||
|
@ -81,7 +80,7 @@ return [
|
||||||
'NameIDFormat' => 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
|
'NameIDFormat' => 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
|
||||||
// Usually x509cert and privateKey of the SP are provided by files placed at
|
// Usually x509cert and privateKey of the SP are provided by files placed at
|
||||||
// the certs folder. But we can also provide them with the following parameters
|
// the certs folder. But we can also provide them with the following parameters
|
||||||
'x509cert' => '',
|
'x509cert' => '',
|
||||||
'privateKey' => '',
|
'privateKey' => '',
|
||||||
],
|
],
|
||||||
// Identity Provider Data that we want connect with our SP
|
// Identity Provider Data that we want connect with our SP
|
||||||
|
|
|
@ -28,16 +28,16 @@ return [
|
||||||
'redirect' => env('APP_URL') . '/login/service/github/callback',
|
'redirect' => env('APP_URL') . '/login/service/github/callback',
|
||||||
'name' => 'GitHub',
|
'name' => 'GitHub',
|
||||||
'auto_register' => env('GITHUB_AUTO_REGISTER', false),
|
'auto_register' => env('GITHUB_AUTO_REGISTER', false),
|
||||||
'auto_confirm' => env('GITHUB_AUTO_CONFIRM_EMAIL', false),
|
'auto_confirm' => env('GITHUB_AUTO_CONFIRM_EMAIL', false),
|
||||||
],
|
],
|
||||||
|
|
||||||
'google' => [
|
'google' => [
|
||||||
'client_id' => env('GOOGLE_APP_ID', false),
|
'client_id' => env('GOOGLE_APP_ID', false),
|
||||||
'client_secret' => env('GOOGLE_APP_SECRET', false),
|
'client_secret' => env('GOOGLE_APP_SECRET', false),
|
||||||
'redirect' => env('APP_URL') . '/login/service/google/callback',
|
'redirect' => env('APP_URL') . '/login/service/google/callback',
|
||||||
'name' => 'Google',
|
'name' => 'Google',
|
||||||
'auto_register' => env('GOOGLE_AUTO_REGISTER', false),
|
'auto_register' => env('GOOGLE_AUTO_REGISTER', false),
|
||||||
'auto_confirm' => env('GOOGLE_AUTO_CONFIRM_EMAIL', false),
|
'auto_confirm' => env('GOOGLE_AUTO_CONFIRM_EMAIL', false),
|
||||||
'select_account' => env('GOOGLE_SELECT_ACCOUNT', false),
|
'select_account' => env('GOOGLE_SELECT_ACCOUNT', false),
|
||||||
],
|
],
|
||||||
|
|
||||||
|
@ -47,7 +47,7 @@ return [
|
||||||
'redirect' => env('APP_URL') . '/login/service/slack/callback',
|
'redirect' => env('APP_URL') . '/login/service/slack/callback',
|
||||||
'name' => 'Slack',
|
'name' => 'Slack',
|
||||||
'auto_register' => env('SLACK_AUTO_REGISTER', false),
|
'auto_register' => env('SLACK_AUTO_REGISTER', false),
|
||||||
'auto_confirm' => env('SLACK_AUTO_CONFIRM_EMAIL', false),
|
'auto_confirm' => env('SLACK_AUTO_CONFIRM_EMAIL', false),
|
||||||
],
|
],
|
||||||
|
|
||||||
'facebook' => [
|
'facebook' => [
|
||||||
|
@ -56,7 +56,7 @@ return [
|
||||||
'redirect' => env('APP_URL') . '/login/service/facebook/callback',
|
'redirect' => env('APP_URL') . '/login/service/facebook/callback',
|
||||||
'name' => 'Facebook',
|
'name' => 'Facebook',
|
||||||
'auto_register' => env('FACEBOOK_AUTO_REGISTER', false),
|
'auto_register' => env('FACEBOOK_AUTO_REGISTER', false),
|
||||||
'auto_confirm' => env('FACEBOOK_AUTO_CONFIRM_EMAIL', false),
|
'auto_confirm' => env('FACEBOOK_AUTO_CONFIRM_EMAIL', false),
|
||||||
],
|
],
|
||||||
|
|
||||||
'twitter' => [
|
'twitter' => [
|
||||||
|
@ -65,27 +65,27 @@ return [
|
||||||
'redirect' => env('APP_URL') . '/login/service/twitter/callback',
|
'redirect' => env('APP_URL') . '/login/service/twitter/callback',
|
||||||
'name' => 'Twitter',
|
'name' => 'Twitter',
|
||||||
'auto_register' => env('TWITTER_AUTO_REGISTER', false),
|
'auto_register' => env('TWITTER_AUTO_REGISTER', false),
|
||||||
'auto_confirm' => env('TWITTER_AUTO_CONFIRM_EMAIL', false),
|
'auto_confirm' => env('TWITTER_AUTO_CONFIRM_EMAIL', false),
|
||||||
],
|
],
|
||||||
|
|
||||||
'azure' => [
|
'azure' => [
|
||||||
'client_id' => env('AZURE_APP_ID', false),
|
'client_id' => env('AZURE_APP_ID', false),
|
||||||
'client_secret' => env('AZURE_APP_SECRET', false),
|
'client_secret' => env('AZURE_APP_SECRET', false),
|
||||||
'tenant' => env('AZURE_TENANT', false),
|
'tenant' => env('AZURE_TENANT', false),
|
||||||
'redirect' => env('APP_URL') . '/login/service/azure/callback',
|
'redirect' => env('APP_URL') . '/login/service/azure/callback',
|
||||||
'name' => 'Microsoft Azure',
|
'name' => 'Microsoft Azure',
|
||||||
'auto_register' => env('AZURE_AUTO_REGISTER', false),
|
'auto_register' => env('AZURE_AUTO_REGISTER', false),
|
||||||
'auto_confirm' => env('AZURE_AUTO_CONFIRM_EMAIL', false),
|
'auto_confirm' => env('AZURE_AUTO_CONFIRM_EMAIL', false),
|
||||||
],
|
],
|
||||||
|
|
||||||
'okta' => [
|
'okta' => [
|
||||||
'client_id' => env('OKTA_APP_ID'),
|
'client_id' => env('OKTA_APP_ID'),
|
||||||
'client_secret' => env('OKTA_APP_SECRET'),
|
'client_secret' => env('OKTA_APP_SECRET'),
|
||||||
'redirect' => env('APP_URL') . '/login/service/okta/callback',
|
'redirect' => env('APP_URL') . '/login/service/okta/callback',
|
||||||
'base_url' => env('OKTA_BASE_URL'),
|
'base_url' => env('OKTA_BASE_URL'),
|
||||||
'name' => 'Okta',
|
'name' => 'Okta',
|
||||||
'auto_register' => env('OKTA_AUTO_REGISTER', false),
|
'auto_register' => env('OKTA_AUTO_REGISTER', false),
|
||||||
'auto_confirm' => env('OKTA_AUTO_CONFIRM_EMAIL', false),
|
'auto_confirm' => env('OKTA_AUTO_CONFIRM_EMAIL', false),
|
||||||
],
|
],
|
||||||
|
|
||||||
'gitlab' => [
|
'gitlab' => [
|
||||||
|
@ -95,45 +95,45 @@ return [
|
||||||
'instance_uri' => env('GITLAB_BASE_URI'), // Needed only for self hosted instances
|
'instance_uri' => env('GITLAB_BASE_URI'), // Needed only for self hosted instances
|
||||||
'name' => 'GitLab',
|
'name' => 'GitLab',
|
||||||
'auto_register' => env('GITLAB_AUTO_REGISTER', false),
|
'auto_register' => env('GITLAB_AUTO_REGISTER', false),
|
||||||
'auto_confirm' => env('GITLAB_AUTO_CONFIRM_EMAIL', false),
|
'auto_confirm' => env('GITLAB_AUTO_CONFIRM_EMAIL', false),
|
||||||
],
|
],
|
||||||
|
|
||||||
'twitch' => [
|
'twitch' => [
|
||||||
'client_id' => env('TWITCH_APP_ID'),
|
'client_id' => env('TWITCH_APP_ID'),
|
||||||
'client_secret' => env('TWITCH_APP_SECRET'),
|
'client_secret' => env('TWITCH_APP_SECRET'),
|
||||||
'redirect' => env('APP_URL') . '/login/service/twitch/callback',
|
'redirect' => env('APP_URL') . '/login/service/twitch/callback',
|
||||||
'name' => 'Twitch',
|
'name' => 'Twitch',
|
||||||
'auto_register' => env('TWITCH_AUTO_REGISTER', false),
|
'auto_register' => env('TWITCH_AUTO_REGISTER', false),
|
||||||
'auto_confirm' => env('TWITCH_AUTO_CONFIRM_EMAIL', false),
|
'auto_confirm' => env('TWITCH_AUTO_CONFIRM_EMAIL', false),
|
||||||
],
|
],
|
||||||
|
|
||||||
'discord' => [
|
'discord' => [
|
||||||
'client_id' => env('DISCORD_APP_ID'),
|
'client_id' => env('DISCORD_APP_ID'),
|
||||||
'client_secret' => env('DISCORD_APP_SECRET'),
|
'client_secret' => env('DISCORD_APP_SECRET'),
|
||||||
'redirect' => env('APP_URL') . '/login/service/discord/callback',
|
'redirect' => env('APP_URL') . '/login/service/discord/callback',
|
||||||
'name' => 'Discord',
|
'name' => 'Discord',
|
||||||
'auto_register' => env('DISCORD_AUTO_REGISTER', false),
|
'auto_register' => env('DISCORD_AUTO_REGISTER', false),
|
||||||
'auto_confirm' => env('DISCORD_AUTO_CONFIRM_EMAIL', false),
|
'auto_confirm' => env('DISCORD_AUTO_CONFIRM_EMAIL', false),
|
||||||
],
|
],
|
||||||
|
|
||||||
'ldap' => [
|
'ldap' => [
|
||||||
'server' => env('LDAP_SERVER', false),
|
'server' => env('LDAP_SERVER', false),
|
||||||
'dump_user_details' => env('LDAP_DUMP_USER_DETAILS', false),
|
'dump_user_details' => env('LDAP_DUMP_USER_DETAILS', false),
|
||||||
'dn' => env('LDAP_DN', false),
|
'dn' => env('LDAP_DN', false),
|
||||||
'pass' => env('LDAP_PASS', false),
|
'pass' => env('LDAP_PASS', false),
|
||||||
'base_dn' => env('LDAP_BASE_DN', false),
|
'base_dn' => env('LDAP_BASE_DN', false),
|
||||||
'user_filter' => env('LDAP_USER_FILTER', '(&(uid=${user}))'),
|
'user_filter' => env('LDAP_USER_FILTER', '(&(uid=${user}))'),
|
||||||
'version' => env('LDAP_VERSION', false),
|
'version' => env('LDAP_VERSION', false),
|
||||||
'id_attribute' => env('LDAP_ID_ATTRIBUTE', 'uid'),
|
'id_attribute' => env('LDAP_ID_ATTRIBUTE', 'uid'),
|
||||||
'email_attribute' => env('LDAP_EMAIL_ATTRIBUTE', 'mail'),
|
'email_attribute' => env('LDAP_EMAIL_ATTRIBUTE', 'mail'),
|
||||||
'display_name_attribute' => env('LDAP_DISPLAY_NAME_ATTRIBUTE', 'cn'),
|
'display_name_attribute' => env('LDAP_DISPLAY_NAME_ATTRIBUTE', 'cn'),
|
||||||
'follow_referrals' => env('LDAP_FOLLOW_REFERRALS', false),
|
'follow_referrals' => env('LDAP_FOLLOW_REFERRALS', false),
|
||||||
'user_to_groups' => env('LDAP_USER_TO_GROUPS', false),
|
'user_to_groups' => env('LDAP_USER_TO_GROUPS', false),
|
||||||
'group_attribute' => env('LDAP_GROUP_ATTRIBUTE', 'memberOf'),
|
'group_attribute' => env('LDAP_GROUP_ATTRIBUTE', 'memberOf'),
|
||||||
'remove_from_groups' => env('LDAP_REMOVE_FROM_GROUPS', false),
|
'remove_from_groups' => env('LDAP_REMOVE_FROM_GROUPS', false),
|
||||||
'tls_insecure' => env('LDAP_TLS_INSECURE', false),
|
'tls_insecure' => env('LDAP_TLS_INSECURE', false),
|
||||||
'start_tls' => env('LDAP_START_TLS', false),
|
'start_tls' => env('LDAP_START_TLS', false),
|
||||||
'thumbnail_attribute' => env('LDAP_THUMBNAIL_ATTRIBUTE', null),
|
'thumbnail_attribute' => env('LDAP_THUMBNAIL_ATTRIBUTE', null),
|
||||||
],
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
use \Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Session configuration options.
|
* Session configuration options.
|
||||||
|
|
|
@ -26,10 +26,10 @@ return [
|
||||||
|
|
||||||
// User-level default settings
|
// User-level default settings
|
||||||
'user' => [
|
'user' => [
|
||||||
'dark-mode-enabled' => env('APP_DEFAULT_DARK_MODE', false),
|
'dark-mode-enabled' => env('APP_DEFAULT_DARK_MODE', false),
|
||||||
'bookshelves_view_type' => env('APP_VIEWS_BOOKSHELVES', 'grid'),
|
'bookshelves_view_type' => env('APP_VIEWS_BOOKSHELVES', 'grid'),
|
||||||
'bookshelf_view_type' =>env('APP_VIEWS_BOOKSHELF', 'grid'),
|
'bookshelf_view_type' => env('APP_VIEWS_BOOKSHELF', 'grid'),
|
||||||
'books_view_type' => env('APP_VIEWS_BOOKS', 'grid'),
|
'books_view_type' => env('APP_VIEWS_BOOKS', 'grid'),
|
||||||
],
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
|
@ -14,7 +14,7 @@ return [
|
||||||
'binary' => file_exists(base_path('wkhtmltopdf')) ? base_path('wkhtmltopdf') : env('WKHTMLTOPDF', false),
|
'binary' => file_exists(base_path('wkhtmltopdf')) ? base_path('wkhtmltopdf') : env('WKHTMLTOPDF', false),
|
||||||
'timeout' => false,
|
'timeout' => false,
|
||||||
'options' => [
|
'options' => [
|
||||||
'outline' => true
|
'outline' => true,
|
||||||
],
|
],
|
||||||
'env' => [],
|
'env' => [],
|
||||||
],
|
],
|
||||||
|
|
|
@ -25,11 +25,11 @@ class CleanupImages extends Command
|
||||||
*/
|
*/
|
||||||
protected $description = 'Cleanup images and drawings';
|
protected $description = 'Cleanup images and drawings';
|
||||||
|
|
||||||
|
|
||||||
protected $imageService;
|
protected $imageService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new command instance.
|
* Create a new command instance.
|
||||||
|
*
|
||||||
* @param \BookStack\Uploads\ImageService $imageService
|
* @param \BookStack\Uploads\ImageService $imageService
|
||||||
*/
|
*/
|
||||||
public function __construct(ImageService $imageService)
|
public function __construct(ImageService $imageService)
|
||||||
|
@ -63,6 +63,7 @@ class CleanupImages extends Command
|
||||||
$this->comment($deleteCount . ' images found that would have been deleted');
|
$this->comment($deleteCount . ' images found that would have been deleted');
|
||||||
$this->showDeletedImages($deleted);
|
$this->showDeletedImages($deleted);
|
||||||
$this->comment('Run with -f or --force to perform deletions');
|
$this->comment('Run with -f or --force to perform deletions');
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,6 @@ class ClearViews extends Command
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new command instance.
|
* Create a new command instance.
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
|
|
|
@ -54,13 +54,14 @@ class CopyShelfPermissions extends Command
|
||||||
|
|
||||||
if (!$cascadeAll && !$shelfSlug) {
|
if (!$cascadeAll && !$shelfSlug) {
|
||||||
$this->error('Either a --slug or --all option must be provided.');
|
$this->error('Either a --slug or --all option must be provided.');
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($cascadeAll) {
|
if ($cascadeAll) {
|
||||||
$continue = $this->confirm(
|
$continue = $this->confirm(
|
||||||
'Permission settings for all shelves will be cascaded. '.
|
'Permission settings for all shelves will be cascaded. ' .
|
||||||
'Books assigned to multiple shelves will receive only the permissions of it\'s last processed shelf. '.
|
'Books assigned to multiple shelves will receive only the permissions of it\'s last processed shelf. ' .
|
||||||
'Are you sure you want to proceed?'
|
'Are you sure you want to proceed?'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -38,8 +38,9 @@ class CreateAdmin extends Command
|
||||||
/**
|
/**
|
||||||
* Execute the console command.
|
* Execute the console command.
|
||||||
*
|
*
|
||||||
* @return mixed
|
|
||||||
* @throws \BookStack\Exceptions\NotFoundException
|
* @throws \BookStack\Exceptions\NotFoundException
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
*/
|
*/
|
||||||
public function handle()
|
public function handle()
|
||||||
{
|
{
|
||||||
|
@ -71,7 +72,6 @@ class CreateAdmin extends Command
|
||||||
return $this->error('Invalid password provided, Must be at least 5 characters');
|
return $this->error('Invalid password provided, Must be at least 5 characters');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
$user = $this->userRepo->create(['email' => $email, 'name' => $name, 'password' => $password]);
|
$user = $this->userRepo->create(['email' => $email, 'name' => $name, 'password' => $password]);
|
||||||
$this->userRepo->attachSystemRole($user, 'admin');
|
$this->userRepo->attachSystemRole($user, 'admin');
|
||||||
$this->userRepo->downloadAndAssignUserAvatar($user);
|
$this->userRepo->downloadAndAssignUserAvatar($user);
|
||||||
|
|
|
@ -8,7 +8,6 @@ use Illuminate\Console\Command;
|
||||||
|
|
||||||
class DeleteUsers extends Command
|
class DeleteUsers extends Command
|
||||||
{
|
{
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The name and signature of the console command.
|
* The name and signature of the console command.
|
||||||
*
|
*
|
||||||
|
@ -47,7 +46,7 @@ class DeleteUsers extends Command
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$this->userRepo->destroy($user);
|
$this->userRepo->destroy($user);
|
||||||
++$numDeleted;
|
$numDeleted++;
|
||||||
}
|
}
|
||||||
$this->info("Deleted $numDeleted of $totalUsers total users.");
|
$this->info("Deleted $numDeleted of $totalUsers total users.");
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Console\Commands;
|
||||||
|
|
||||||
|
use BookStack\Auth\User;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class ResetMfa extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'bookstack:reset-mfa
|
||||||
|
{--id= : Numeric ID of the user to reset MFA for}
|
||||||
|
{--email= : Email address of the user to reset MFA for}
|
||||||
|
';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Reset & Clear any configured MFA methods for the given user';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$id = $this->option('id');
|
||||||
|
$email = $this->option('email');
|
||||||
|
if (!$id && !$email) {
|
||||||
|
$this->error('Either a --id=<number> or --email=<email> option must be provided.');
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var User $user */
|
||||||
|
$field = $id ? 'id' : 'email';
|
||||||
|
$value = $id ?: $email;
|
||||||
|
$user = User::query()
|
||||||
|
->where($field, '=', $value)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
$this->error("A user where {$field}={$value} could not be found.");
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info("This will delete any configure multi-factor authentication methods for user: \n- ID: {$user->id}\n- Name: {$user->name}\n- Email: {$user->email}\n");
|
||||||
|
$this->info('If multi-factor authentication is required for this user they will be asked to reconfigure their methods on next login.');
|
||||||
|
$confirm = $this->confirm('Are you sure you want to proceed?');
|
||||||
|
if ($confirm) {
|
||||||
|
$user->mfaValues()->delete();
|
||||||
|
$this->info('User MFA methods have been reset.');
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,7 +4,6 @@ namespace BookStack\Console\Commands;
|
||||||
|
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
use Illuminate\Database\Connection;
|
use Illuminate\Database\Connection;
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
|
|
||||||
class UpdateUrl extends Command
|
class UpdateUrl extends Command
|
||||||
{
|
{
|
||||||
|
@ -49,7 +48,8 @@ class UpdateUrl extends Command
|
||||||
|
|
||||||
$urlPattern = '/https?:\/\/(.+)/';
|
$urlPattern = '/https?:\/\/(.+)/';
|
||||||
if (!preg_match($urlPattern, $oldUrl) || !preg_match($urlPattern, $newUrl)) {
|
if (!preg_match($urlPattern, $oldUrl) || !preg_match($urlPattern, $newUrl)) {
|
||||||
$this->error("The given urls are expected to be full urls starting with http:// or https://");
|
$this->error('The given urls are expected to be full urls starting with http:// or https://');
|
||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,11 +58,11 @@ class UpdateUrl extends Command
|
||||||
}
|
}
|
||||||
|
|
||||||
$columnsToUpdateByTable = [
|
$columnsToUpdateByTable = [
|
||||||
"attachments" => ["path"],
|
'attachments' => ['path'],
|
||||||
"pages" => ["html", "text", "markdown"],
|
'pages' => ['html', 'text', 'markdown'],
|
||||||
"images" => ["url"],
|
'images' => ['url'],
|
||||||
"settings" => ["value"],
|
'settings' => ['value'],
|
||||||
"comments" => ["html", "text"],
|
'comments' => ['html', 'text'],
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($columnsToUpdateByTable as $table => $columns) {
|
foreach ($columnsToUpdateByTable as $table => $columns) {
|
||||||
|
@ -73,7 +73,7 @@ class UpdateUrl extends Command
|
||||||
}
|
}
|
||||||
|
|
||||||
$jsonColumnsToUpdateByTable = [
|
$jsonColumnsToUpdateByTable = [
|
||||||
"settings" => ["value"],
|
'settings' => ['value'],
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($jsonColumnsToUpdateByTable as $table => $columns) {
|
foreach ($jsonColumnsToUpdateByTable as $table => $columns) {
|
||||||
|
@ -85,10 +85,11 @@ class UpdateUrl extends Command
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->info("URL update procedure complete.");
|
$this->info('URL update procedure complete.');
|
||||||
$this->info('============================================================================');
|
$this->info('============================================================================');
|
||||||
$this->info('Be sure to run "php artisan cache:clear" to clear any old URLs in the cache.');
|
$this->info('Be sure to run "php artisan cache:clear" to clear any old URLs in the cache.');
|
||||||
$this->info('============================================================================');
|
$this->info('============================================================================');
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,8 +101,9 @@ class UpdateUrl extends Command
|
||||||
{
|
{
|
||||||
$oldQuoted = $this->db->getPdo()->quote($oldUrl);
|
$oldQuoted = $this->db->getPdo()->quote($oldUrl);
|
||||||
$newQuoted = $this->db->getPdo()->quote($newUrl);
|
$newQuoted = $this->db->getPdo()->quote($newUrl);
|
||||||
|
|
||||||
return $this->db->table($table)->update([
|
return $this->db->table($table)->update([
|
||||||
$column => $this->db->raw("REPLACE({$column}, {$oldQuoted}, {$newQuoted})")
|
$column => $this->db->raw("REPLACE({$column}, {$oldQuoted}, {$newQuoted})"),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,8 +114,8 @@ class UpdateUrl extends Command
|
||||||
protected function checkUserOkayToProceed(string $oldUrl, string $newUrl): bool
|
protected function checkUserOkayToProceed(string $oldUrl, string $newUrl): bool
|
||||||
{
|
{
|
||||||
$dangerWarning = "This will search for \"{$oldUrl}\" in your database and replace it with \"{$newUrl}\".\n";
|
$dangerWarning = "This will search for \"{$oldUrl}\" in your database and replace it with \"{$newUrl}\".\n";
|
||||||
$dangerWarning .= "Are you sure you want to proceed?";
|
$dangerWarning .= 'Are you sure you want to proceed?';
|
||||||
$backupConfirmation = "This operation could cause issues if used incorrectly. Have you made a backup of your existing database?";
|
$backupConfirmation = 'This operation could cause issues if used incorrectly. Have you made a backup of your existing database?';
|
||||||
|
|
||||||
return $this->confirm($dangerWarning) && $this->confirm($backupConfirmation);
|
return $this->confirm($dangerWarning) && $this->confirm($backupConfirmation);
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,6 @@ class UpgradeDatabaseEncoding extends Command
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new command instance.
|
* Create a new command instance.
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
|
@ -44,12 +43,12 @@ class UpgradeDatabaseEncoding extends Command
|
||||||
|
|
||||||
$database = DB::getDatabaseName();
|
$database = DB::getDatabaseName();
|
||||||
$tables = DB::select('SHOW TABLES');
|
$tables = DB::select('SHOW TABLES');
|
||||||
$this->line('ALTER DATABASE `'.$database.'` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;');
|
$this->line('ALTER DATABASE `' . $database . '` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;');
|
||||||
$this->line('USE `'.$database.'`;');
|
$this->line('USE `' . $database . '`;');
|
||||||
$key = 'Tables_in_' . $database;
|
$key = 'Tables_in_' . $database;
|
||||||
foreach ($tables as $table) {
|
foreach ($tables as $table) {
|
||||||
$tableName = $table->$key;
|
$tableName = $table->$key;
|
||||||
$this->line('ALTER TABLE `'.$tableName.'` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;');
|
$this->line('ALTER TABLE `' . $tableName . '` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;');
|
||||||
}
|
}
|
||||||
|
|
||||||
DB::setDefaultConnection($connection);
|
DB::setDefaultConnection($connection);
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
<?php namespace BookStack\Console;
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Console;
|
||||||
|
|
||||||
use Illuminate\Console\Scheduling\Schedule;
|
use Illuminate\Console\Scheduling\Schedule;
|
||||||
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
||||||
|
@ -17,7 +19,8 @@ class Kernel extends ConsoleKernel
|
||||||
/**
|
/**
|
||||||
* Define the application's command schedule.
|
* Define the application's command schedule.
|
||||||
*
|
*
|
||||||
* @param \Illuminate\Console\Scheduling\Schedule $schedule
|
* @param \Illuminate\Console\Scheduling\Schedule $schedule
|
||||||
|
*
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
protected function schedule(Schedule $schedule)
|
protected function schedule(Schedule $schedule)
|
||||||
|
@ -32,6 +35,6 @@ class Kernel extends ConsoleKernel
|
||||||
*/
|
*/
|
||||||
protected function commands()
|
protected function commands()
|
||||||
{
|
{
|
||||||
$this->load(__DIR__.'/Commands');
|
$this->load(__DIR__ . '/Commands');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
<?php namespace BookStack\Entities;
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Entities;
|
||||||
|
|
||||||
use BookStack\Entities\Models\Book;
|
use BookStack\Entities\Models\Book;
|
||||||
use BookStack\Entities\Tools\ShelfContext;
|
use BookStack\Entities\Tools\ShelfContext;
|
||||||
|
@ -6,11 +8,11 @@ use Illuminate\View\View;
|
||||||
|
|
||||||
class BreadcrumbsViewComposer
|
class BreadcrumbsViewComposer
|
||||||
{
|
{
|
||||||
|
|
||||||
protected $entityContextManager;
|
protected $entityContextManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* BreadcrumbsViewComposer constructor.
|
* BreadcrumbsViewComposer constructor.
|
||||||
|
*
|
||||||
* @param ShelfContext $entityContextManager
|
* @param ShelfContext $entityContextManager
|
||||||
*/
|
*/
|
||||||
public function __construct(ShelfContext $entityContextManager)
|
public function __construct(ShelfContext $entityContextManager)
|
||||||
|
@ -20,6 +22,7 @@ class BreadcrumbsViewComposer
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Modify data when the view is composed.
|
* Modify data when the view is composed.
|
||||||
|
*
|
||||||
* @param View $view
|
* @param View $view
|
||||||
*/
|
*/
|
||||||
public function compose(View $view)
|
public function compose(View $view)
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
<?php namespace BookStack\Entities;
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Entities;
|
||||||
|
|
||||||
use BookStack\Entities\Models\Book;
|
use BookStack\Entities\Models\Book;
|
||||||
use BookStack\Entities\Models\Bookshelf;
|
use BookStack\Entities\Models\Bookshelf;
|
||||||
|
@ -8,7 +10,7 @@ use BookStack\Entities\Models\Page;
|
||||||
use BookStack\Entities\Models\PageRevision;
|
use BookStack\Entities\Models\PageRevision;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class EntityProvider
|
* Class EntityProvider.
|
||||||
*
|
*
|
||||||
* Provides access to the core entity models.
|
* Provides access to the core entity models.
|
||||||
* Wrapped up in this provider since they are often used together
|
* Wrapped up in this provider since they are often used together
|
||||||
|
@ -16,7 +18,6 @@ use BookStack\Entities\Models\PageRevision;
|
||||||
*/
|
*/
|
||||||
class EntityProvider
|
class EntityProvider
|
||||||
{
|
{
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Bookshelf
|
* @var Bookshelf
|
||||||
*/
|
*/
|
||||||
|
@ -42,7 +43,6 @@ class EntityProvider
|
||||||
*/
|
*/
|
||||||
public $pageRevision;
|
public $pageRevision;
|
||||||
|
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->bookshelf = new Bookshelf();
|
$this->bookshelf = new Bookshelf();
|
||||||
|
@ -55,15 +55,16 @@ class EntityProvider
|
||||||
/**
|
/**
|
||||||
* Fetch all core entity types as an associated array
|
* Fetch all core entity types as an associated array
|
||||||
* with their basic names as the keys.
|
* with their basic names as the keys.
|
||||||
|
*
|
||||||
* @return array<Entity>
|
* @return array<Entity>
|
||||||
*/
|
*/
|
||||||
public function all(): array
|
public function all(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'bookshelf' => $this->bookshelf,
|
'bookshelf' => $this->bookshelf,
|
||||||
'book' => $this->book,
|
'book' => $this->book,
|
||||||
'chapter' => $this->chapter,
|
'chapter' => $this->chapter,
|
||||||
'page' => $this->page,
|
'page' => $this->page,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,6 +74,7 @@ class EntityProvider
|
||||||
public function get(string $type): Entity
|
public function get(string $type): Entity
|
||||||
{
|
{
|
||||||
$type = strtolower($type);
|
$type = strtolower($type);
|
||||||
|
|
||||||
return $this->all()[$type];
|
return $this->all()[$type];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,6 +88,7 @@ class EntityProvider
|
||||||
$model = $this->get($type);
|
$model = $this->get($type);
|
||||||
$morphClasses[] = $model->getMorphClass();
|
$morphClasses[] = $model->getMorphClass();
|
||||||
}
|
}
|
||||||
|
|
||||||
return $morphClasses;
|
return $morphClasses;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
<?php namespace BookStack\Entities\Models;
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Entities\Models;
|
||||||
|
|
||||||
use BookStack\Uploads\Image;
|
use BookStack\Uploads\Image;
|
||||||
use Exception;
|
use Exception;
|
||||||
|
@ -8,9 +10,10 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class Book
|
* Class Book.
|
||||||
* @property string $description
|
*
|
||||||
* @property int $image_id
|
* @property string $description
|
||||||
|
* @property int $image_id
|
||||||
* @property Image|null $cover
|
* @property Image|null $cover
|
||||||
*/
|
*/
|
||||||
class Book extends Entity implements HasCoverImage
|
class Book extends Entity implements HasCoverImage
|
||||||
|
@ -30,8 +33,10 @@ class Book extends Entity implements HasCoverImage
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns book cover image, if book cover not exists return default cover image.
|
* Returns book cover image, if book cover not exists return default cover image.
|
||||||
* @param int $width - Width of the image
|
*
|
||||||
|
* @param int $width - Width of the image
|
||||||
* @param int $height - Height of the image
|
* @param int $height - Height of the image
|
||||||
|
*
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
public function getBookCover($width = 440, $height = 250)
|
public function getBookCover($width = 440, $height = 250)
|
||||||
|
@ -46,11 +51,12 @@ class Book extends Entity implements HasCoverImage
|
||||||
} catch (Exception $err) {
|
} catch (Exception $err) {
|
||||||
$cover = $default;
|
$cover = $default;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $cover;
|
return $cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the cover image of the book
|
* Get the cover image of the book.
|
||||||
*/
|
*/
|
||||||
public function cover(): BelongsTo
|
public function cover(): BelongsTo
|
||||||
{
|
{
|
||||||
|
@ -67,6 +73,7 @@ class Book extends Entity implements HasCoverImage
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all pages within this book.
|
* Get all pages within this book.
|
||||||
|
*
|
||||||
* @return HasMany
|
* @return HasMany
|
||||||
*/
|
*/
|
||||||
public function pages()
|
public function pages()
|
||||||
|
@ -76,6 +83,7 @@ class Book extends Entity implements HasCoverImage
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the direct child pages of this book.
|
* Get the direct child pages of this book.
|
||||||
|
*
|
||||||
* @return HasMany
|
* @return HasMany
|
||||||
*/
|
*/
|
||||||
public function directPages()
|
public function directPages()
|
||||||
|
@ -85,6 +93,7 @@ class Book extends Entity implements HasCoverImage
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all chapters within this book.
|
* Get all chapters within this book.
|
||||||
|
*
|
||||||
* @return HasMany
|
* @return HasMany
|
||||||
*/
|
*/
|
||||||
public function chapters()
|
public function chapters()
|
||||||
|
@ -94,6 +103,7 @@ class Book extends Entity implements HasCoverImage
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the shelves this book is contained within.
|
* Get the shelves this book is contained within.
|
||||||
|
*
|
||||||
* @return BelongsToMany
|
* @return BelongsToMany
|
||||||
*/
|
*/
|
||||||
public function shelves()
|
public function shelves()
|
||||||
|
@ -103,12 +113,14 @@ class Book extends Entity implements HasCoverImage
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the direct child items within this book.
|
* Get the direct child items within this book.
|
||||||
|
*
|
||||||
* @return Collection
|
* @return Collection
|
||||||
*/
|
*/
|
||||||
public function getDirectChildren(): Collection
|
public function getDirectChildren(): Collection
|
||||||
{
|
{
|
||||||
$pages = $this->directPages()->visible()->get();
|
$pages = $this->directPages()->visible()->get();
|
||||||
$chapters = $this->chapters()->visible()->get();
|
$chapters = $this->chapters()->visible()->get();
|
||||||
|
|
||||||
return $pages->concat($chapters)->sortBy('priority')->sortByDesc('draft');
|
return $pages->concat($chapters)->sortBy('priority')->sortByDesc('draft');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +1,38 @@
|
||||||
<?php namespace BookStack\Entities\Models;
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Entities\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class BookChild
|
* Class BookChild.
|
||||||
* @property int $book_id
|
*
|
||||||
* @property int $priority
|
* @property int $book_id
|
||||||
* @property Book $book
|
* @property int $priority
|
||||||
|
* @property string $book_slug
|
||||||
|
* @property Book $book
|
||||||
|
*
|
||||||
* @method Builder whereSlugs(string $bookSlug, string $childSlug)
|
* @method Builder whereSlugs(string $bookSlug, string $childSlug)
|
||||||
*/
|
*/
|
||||||
abstract class BookChild extends Entity
|
abstract class BookChild extends Entity
|
||||||
{
|
{
|
||||||
|
protected static function boot()
|
||||||
|
{
|
||||||
|
parent::boot();
|
||||||
|
|
||||||
|
// Load book slugs onto these models by default during query-time
|
||||||
|
static::addGlobalScope('book_slug', function (Builder $builder) {
|
||||||
|
$builder->addSelect(['book_slug' => function ($builder) {
|
||||||
|
$builder->select('slug')
|
||||||
|
->from('books')
|
||||||
|
->whereColumn('books.id', '=', 'book_id');
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scope a query to find items where the the child has the given childSlug
|
* Scope a query to find items where the child has the given childSlug
|
||||||
* where its parent has the bookSlug.
|
* where its parent has the bookSlug.
|
||||||
*/
|
*/
|
||||||
public function scopeWhereSlugs(Builder $query, string $bookSlug, string $childSlug)
|
public function scopeWhereSlugs(Builder $query, string $bookSlug, string $childSlug)
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
<?php namespace BookStack\Entities\Models;
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Entities\Models;
|
||||||
|
|
||||||
use BookStack\Uploads\Image;
|
use BookStack\Uploads\Image;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
@ -17,6 +19,7 @@ class Bookshelf extends Entity implements HasCoverImage
|
||||||
/**
|
/**
|
||||||
* Get the books in this shelf.
|
* Get the books in this shelf.
|
||||||
* Should not be used directly since does not take into account permissions.
|
* Should not be used directly since does not take into account permissions.
|
||||||
|
*
|
||||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
|
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
|
||||||
*/
|
*/
|
||||||
public function books()
|
public function books()
|
||||||
|
@ -44,8 +47,10 @@ class Bookshelf extends Entity implements HasCoverImage
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns BookShelf cover image, if cover does not exists return default cover image.
|
* Returns BookShelf cover image, if cover does not exists return default cover image.
|
||||||
* @param int $width - Width of the image
|
*
|
||||||
|
* @param int $width - Width of the image
|
||||||
* @param int $height - Height of the image
|
* @param int $height - Height of the image
|
||||||
|
*
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
public function getBookCover($width = 440, $height = 250)
|
public function getBookCover($width = 440, $height = 250)
|
||||||
|
@ -61,11 +66,12 @@ class Bookshelf extends Entity implements HasCoverImage
|
||||||
} catch (\Exception $err) {
|
} catch (\Exception $err) {
|
||||||
$cover = $default;
|
$cover = $default;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $cover;
|
return $cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the cover image of the shelf
|
* Get the cover image of the shelf.
|
||||||
*/
|
*/
|
||||||
public function cover(): BelongsTo
|
public function cover(): BelongsTo
|
||||||
{
|
{
|
||||||
|
@ -82,7 +88,9 @@ class Bookshelf extends Entity implements HasCoverImage
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if this shelf contains the given book.
|
* Check if this shelf contains the given book.
|
||||||
|
*
|
||||||
* @param Book $book
|
* @param Book $book
|
||||||
|
*
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function contains(Book $book): bool
|
public function contains(Book $book): bool
|
||||||
|
@ -92,6 +100,7 @@ class Bookshelf extends Entity implements HasCoverImage
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a book to the end of this shelf.
|
* Add a book to the end of this shelf.
|
||||||
|
*
|
||||||
* @param Book $book
|
* @param Book $book
|
||||||
*/
|
*/
|
||||||
public function appendBook(Book $book)
|
public function appendBook(Book $book)
|
||||||
|
|
|
@ -1,10 +1,14 @@
|
||||||
<?php namespace BookStack\Entities\Models;
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Entities\Models;
|
||||||
|
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class Chapter
|
* Class Chapter.
|
||||||
|
*
|
||||||
* @property Collection<Page> $pages
|
* @property Collection<Page> $pages
|
||||||
|
* @property mixed description
|
||||||
*/
|
*/
|
||||||
class Chapter extends BookChild
|
class Chapter extends BookChild
|
||||||
{
|
{
|
||||||
|
@ -15,7 +19,9 @@ class Chapter extends BookChild
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the pages that this chapter contains.
|
* Get the pages that this chapter contains.
|
||||||
|
*
|
||||||
* @param string $dir
|
* @param string $dir
|
||||||
|
*
|
||||||
* @return mixed
|
* @return mixed
|
||||||
*/
|
*/
|
||||||
public function pages($dir = 'ASC')
|
public function pages($dir = 'ASC')
|
||||||
|
@ -30,7 +36,7 @@ class Chapter extends BookChild
|
||||||
{
|
{
|
||||||
$parts = [
|
$parts = [
|
||||||
'books',
|
'books',
|
||||||
urlencode($this->getAttribute('bookSlug') ?? $this->book->slug),
|
urlencode($this->book_slug ?? $this->book->slug),
|
||||||
'chapter',
|
'chapter',
|
||||||
urlencode($this->slug),
|
urlencode($this->slug),
|
||||||
trim($path, '/'),
|
trim($path, '/'),
|
||||||
|
|
|
@ -1,15 +1,18 @@
|
||||||
<?php namespace BookStack\Entities\Models;
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Entities\Models;
|
||||||
|
|
||||||
use BookStack\Auth\User;
|
use BookStack\Auth\User;
|
||||||
use BookStack\Entities\Models\Entity;
|
|
||||||
use BookStack\Interfaces\Loggable;
|
use BookStack\Interfaces\Loggable;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property Model deletable
|
||||||
|
*/
|
||||||
class Deletion extends Model implements Loggable
|
class Deletion extends Model implements Loggable
|
||||||
{
|
{
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the related deletable record.
|
* Get the related deletable record.
|
||||||
*/
|
*/
|
||||||
|
@ -32,17 +35,27 @@ class Deletion extends Model implements Loggable
|
||||||
public static function createForEntity(Entity $entity): Deletion
|
public static function createForEntity(Entity $entity): Deletion
|
||||||
{
|
{
|
||||||
$record = (new self())->forceFill([
|
$record = (new self())->forceFill([
|
||||||
'deleted_by' => user()->id,
|
'deleted_by' => user()->id,
|
||||||
'deletable_type' => $entity->getMorphClass(),
|
'deletable_type' => $entity->getMorphClass(),
|
||||||
'deletable_id' => $entity->id,
|
'deletable_id' => $entity->id,
|
||||||
]);
|
]);
|
||||||
$record->save();
|
$record->save();
|
||||||
|
|
||||||
return $record;
|
return $record;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function logDescriptor(): string
|
public function logDescriptor(): string
|
||||||
{
|
{
|
||||||
$deletable = $this->deletable()->first();
|
$deletable = $this->deletable()->first();
|
||||||
|
|
||||||
return "Deletion ({$this->id}) for {$deletable->getType()} ({$deletable->id}) {$deletable->name}";
|
return "Deletion ({$this->id}) for {$deletable->getType()} ({$deletable->id}) {$deletable->name}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a URL for this specific deletion.
|
||||||
|
*/
|
||||||
|
public function getUrl($path): string
|
||||||
|
{
|
||||||
|
return url("/settings/recycle-bin/{$this->id}/" . ltrim($path, '/'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
<?php namespace BookStack\Entities\Models;
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Entities\Models;
|
||||||
|
|
||||||
use BookStack\Actions\Activity;
|
use BookStack\Actions\Activity;
|
||||||
use BookStack\Actions\Comment;
|
use BookStack\Actions\Comment;
|
||||||
|
@ -27,15 +29,16 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
* The base class for book-like items such as pages, chapters & books.
|
* The base class for book-like items such as pages, chapters & books.
|
||||||
* This is not a database model in itself but extended.
|
* This is not a database model in itself but extended.
|
||||||
*
|
*
|
||||||
* @property int $id
|
* @property int $id
|
||||||
* @property string $name
|
* @property string $name
|
||||||
* @property string $slug
|
* @property string $slug
|
||||||
* @property Carbon $created_at
|
* @property Carbon $created_at
|
||||||
* @property Carbon $updated_at
|
* @property Carbon $updated_at
|
||||||
* @property int $created_by
|
* @property int $created_by
|
||||||
* @property int $updated_by
|
* @property int $updated_by
|
||||||
* @property boolean $restricted
|
* @property bool $restricted
|
||||||
* @property Collection $tags
|
* @property Collection $tags
|
||||||
|
*
|
||||||
* @method static Entity|Builder visible()
|
* @method static Entity|Builder visible()
|
||||||
* @method static Entity|Builder hasPermission(string $permission)
|
* @method static Entity|Builder hasPermission(string $permission)
|
||||||
* @method static Builder withLastView()
|
* @method static Builder withLastView()
|
||||||
|
@ -154,11 +157,12 @@ abstract class Entity extends Model implements Sluggable, Favouritable, Viewable
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the comments for an entity
|
* Get the comments for an entity.
|
||||||
*/
|
*/
|
||||||
public function comments(bool $orderByCreated = true): MorphMany
|
public function comments(bool $orderByCreated = true): MorphMany
|
||||||
{
|
{
|
||||||
$query = $this->morphMany(Comment::class, 'entity');
|
$query = $this->morphMany(Comment::class, 'entity');
|
||||||
|
|
||||||
return $orderByCreated ? $query->orderBy('created_at', 'asc') : $query;
|
return $orderByCreated ? $query->orderBy('created_at', 'asc') : $query;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -205,7 +209,7 @@ abstract class Entity extends Model implements Sluggable, Favouritable, Viewable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if this instance or class is a certain type of entity.
|
* Check if this instance or class is a certain type of entity.
|
||||||
* Examples of $type are 'page', 'book', 'chapter'
|
* Examples of $type are 'page', 'book', 'chapter'.
|
||||||
*/
|
*/
|
||||||
public static function isA(string $type): bool
|
public static function isA(string $type): bool
|
||||||
{
|
{
|
||||||
|
@ -218,6 +222,7 @@ abstract class Entity extends Model implements Sluggable, Favouritable, Viewable
|
||||||
public static function getType(): string
|
public static function getType(): string
|
||||||
{
|
{
|
||||||
$className = array_slice(explode('\\', static::class), -1, 1)[0];
|
$className = array_slice(explode('\\', static::class), -1, 1)[0];
|
||||||
|
|
||||||
return strtolower($className);
|
return strtolower($className);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -229,6 +234,7 @@ abstract class Entity extends Model implements Sluggable, Favouritable, Viewable
|
||||||
if (mb_strlen($this->name) <= $length) {
|
if (mb_strlen($this->name) <= $length) {
|
||||||
return $this->name;
|
return $this->name;
|
||||||
}
|
}
|
||||||
|
|
||||||
return mb_substr($this->name, 0, $length - 3) . '...';
|
return mb_substr($this->name, 0, $length - 3) . '...';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -248,14 +254,14 @@ abstract class Entity extends Model implements Sluggable, Favouritable, Viewable
|
||||||
$text = $this->getText();
|
$text = $this->getText();
|
||||||
|
|
||||||
if (mb_strlen($text) > $length) {
|
if (mb_strlen($text) > $length) {
|
||||||
$text = mb_substr($text, 0, $length-3) . '...';
|
$text = mb_substr($text, 0, $length - 3) . '...';
|
||||||
}
|
}
|
||||||
|
|
||||||
return trim($text);
|
return trim($text);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the url of this entity
|
* Get the url of this entity.
|
||||||
*/
|
*/
|
||||||
abstract public function getUrl(string $path = '/'): string;
|
abstract public function getUrl(string $path = '/'): string;
|
||||||
|
|
||||||
|
@ -266,12 +272,13 @@ abstract class Entity extends Model implements Sluggable, Favouritable, Viewable
|
||||||
*/
|
*/
|
||||||
public function getParent(): ?Entity
|
public function getParent(): ?Entity
|
||||||
{
|
{
|
||||||
if ($this->isA('page')) {
|
if ($this instanceof Page) {
|
||||||
return $this->chapter_id ? $this->chapter()->withTrashed()->first() : $this->book()->withTrashed()->first();
|
return $this->chapter_id ? $this->chapter()->withTrashed()->first() : $this->book()->withTrashed()->first();
|
||||||
}
|
}
|
||||||
if ($this->isA('chapter')) {
|
if ($this instanceof Chapter) {
|
||||||
return $this->book()->withTrashed()->first();
|
return $this->book()->withTrashed()->first();
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -285,7 +292,7 @@ abstract class Entity extends Model implements Sluggable, Favouritable, Viewable
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Index the current entity for search
|
* Index the current entity for search.
|
||||||
*/
|
*/
|
||||||
public function indexForSearch()
|
public function indexForSearch()
|
||||||
{
|
{
|
||||||
|
@ -298,6 +305,7 @@ abstract class Entity extends Model implements Sluggable, Favouritable, Viewable
|
||||||
public function refreshSlug(): string
|
public function refreshSlug(): string
|
||||||
{
|
{
|
||||||
$this->slug = app(SlugGenerator::class)->generate($this);
|
$this->slug = app(SlugGenerator::class)->generate($this);
|
||||||
|
|
||||||
return $this->slug;
|
return $this->slug;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
|
||||||
namespace BookStack\Entities\Models;
|
namespace BookStack\Entities\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
interface HasCoverImage
|
interface HasCoverImage
|
||||||
{
|
{
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the cover image for this item.
|
* Get the cover image for this item.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
<?php namespace BookStack\Entities\Models;
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Entities\Models;
|
||||||
|
|
||||||
use BookStack\Entities\Tools\PageContent;
|
use BookStack\Entities\Tools\PageContent;
|
||||||
use BookStack\Uploads\Attachment;
|
use BookStack\Uploads\Attachment;
|
||||||
|
@ -9,29 +11,31 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Permissions;
|
use Permissions;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class Page
|
* Class Page.
|
||||||
* @property int $chapter_id
|
*
|
||||||
* @property string $html
|
* @property int $chapter_id
|
||||||
* @property string $markdown
|
* @property string $html
|
||||||
* @property string $text
|
* @property string $markdown
|
||||||
* @property bool $template
|
* @property string $text
|
||||||
* @property bool $draft
|
* @property bool $template
|
||||||
* @property int $revision_count
|
* @property bool $draft
|
||||||
* @property Chapter $chapter
|
* @property int $revision_count
|
||||||
|
* @property Chapter $chapter
|
||||||
* @property Collection $attachments
|
* @property Collection $attachments
|
||||||
*/
|
*/
|
||||||
class Page extends BookChild
|
class Page extends BookChild
|
||||||
{
|
{
|
||||||
protected $fillable = ['name', 'priority', 'markdown'];
|
public static $listAttributes = ['name', 'id', 'slug', 'book_id', 'chapter_id', 'draft', 'template', 'text', 'created_at', 'updated_at'];
|
||||||
|
public static $contentAttributes = ['name', 'id', 'slug', 'book_id', 'chapter_id', 'draft', 'template', 'html', 'text', 'created_at', 'updated_at'];
|
||||||
|
|
||||||
protected $simpleAttributes = ['name', 'id', 'slug'];
|
protected $fillable = ['name', 'priority', 'markdown'];
|
||||||
|
|
||||||
public $textField = 'text';
|
public $textField = 'text';
|
||||||
|
|
||||||
protected $hidden = ['html', 'markdown', 'text', 'restricted', 'pivot', 'deleted_at'];
|
protected $hidden = ['html', 'markdown', 'text', 'restricted', 'pivot', 'deleted_at'];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'draft' => 'boolean',
|
'draft' => 'boolean',
|
||||||
'template' => 'boolean',
|
'template' => 'boolean',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -41,22 +45,13 @@ class Page extends BookChild
|
||||||
public function scopeVisible(Builder $query): Builder
|
public function scopeVisible(Builder $query): Builder
|
||||||
{
|
{
|
||||||
$query = Permissions::enforceDraftVisibilityOnQuery($query);
|
$query = Permissions::enforceDraftVisibilityOnQuery($query);
|
||||||
|
|
||||||
return parent::scopeVisible($query);
|
return parent::scopeVisible($query);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts this page into a simplified array.
|
|
||||||
* @return mixed
|
|
||||||
*/
|
|
||||||
public function toSimpleArray()
|
|
||||||
{
|
|
||||||
$array = array_intersect_key($this->toArray(), array_flip($this->simpleAttributes));
|
|
||||||
$array['url'] = $this->getUrl();
|
|
||||||
return $array;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the chapter that this page is in, If applicable.
|
* Get the chapter that this page is in, If applicable.
|
||||||
|
*
|
||||||
* @return BelongsTo
|
* @return BelongsTo
|
||||||
*/
|
*/
|
||||||
public function chapter()
|
public function chapter()
|
||||||
|
@ -66,6 +61,7 @@ class Page extends BookChild
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if this page has a chapter.
|
* Check if this page has a chapter.
|
||||||
|
*
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function hasChapter()
|
public function hasChapter()
|
||||||
|
@ -96,6 +92,7 @@ class Page extends BookChild
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the attachments assigned to this page.
|
* Get the attachments assigned to this page.
|
||||||
|
*
|
||||||
* @return HasMany
|
* @return HasMany
|
||||||
*/
|
*/
|
||||||
public function attachments()
|
public function attachments()
|
||||||
|
@ -110,7 +107,7 @@ class Page extends BookChild
|
||||||
{
|
{
|
||||||
$parts = [
|
$parts = [
|
||||||
'books',
|
'books',
|
||||||
urlencode($this->getAttribute('bookSlug') ?? $this->book->slug),
|
urlencode($this->book_slug ?? $this->book->slug),
|
||||||
$this->draft ? 'draft' : 'page',
|
$this->draft ? 'draft' : 'page',
|
||||||
$this->draft ? $this->id : urlencode($this->slug),
|
$this->draft ? $this->id : urlencode($this->slug),
|
||||||
trim($path, '/'),
|
trim($path, '/'),
|
||||||
|
@ -120,7 +117,8 @@ class Page extends BookChild
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current revision for the page if existing
|
* Get the current revision for the page if existing.
|
||||||
|
*
|
||||||
* @return PageRevision|null
|
* @return PageRevision|null
|
||||||
*/
|
*/
|
||||||
public function getCurrentRevision()
|
public function getCurrentRevision()
|
||||||
|
@ -136,6 +134,7 @@ class Page extends BookChild
|
||||||
$refreshed = $this->refresh()->unsetRelations()->load(['tags', 'createdBy', 'updatedBy', 'ownedBy']);
|
$refreshed = $this->refresh()->unsetRelations()->load(['tags', 'createdBy', 'updatedBy', 'ownedBy']);
|
||||||
$refreshed->setHidden(array_diff($refreshed->getHidden(), ['html', 'markdown']));
|
$refreshed->setHidden(array_diff($refreshed->getHidden(), ['html', 'markdown']));
|
||||||
$refreshed->html = (new PageContent($refreshed))->render();
|
$refreshed->html = (new PageContent($refreshed))->render();
|
||||||
|
|
||||||
return $refreshed;
|
return $refreshed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,29 +1,32 @@
|
||||||
<?php namespace BookStack\Entities\Models;
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Entities\Models;
|
||||||
|
|
||||||
use BookStack\Auth\User;
|
use BookStack\Auth\User;
|
||||||
use BookStack\Entities\Models\Page;
|
|
||||||
use BookStack\Model;
|
use BookStack\Model;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class PageRevision
|
* Class PageRevision.
|
||||||
* @property int $page_id
|
*
|
||||||
|
* @property int $page_id
|
||||||
* @property string $slug
|
* @property string $slug
|
||||||
* @property string $book_slug
|
* @property string $book_slug
|
||||||
* @property int $created_by
|
* @property int $created_by
|
||||||
* @property Carbon $created_at
|
* @property Carbon $created_at
|
||||||
* @property string $type
|
* @property string $type
|
||||||
* @property string $summary
|
* @property string $summary
|
||||||
* @property string $markdown
|
* @property string $markdown
|
||||||
* @property string $html
|
* @property string $html
|
||||||
* @property int $revision_number
|
* @property int $revision_number
|
||||||
*/
|
*/
|
||||||
class PageRevision extends Model
|
class PageRevision extends Model
|
||||||
{
|
{
|
||||||
protected $fillable = ['name', 'html', 'text', 'markdown', 'summary'];
|
protected $fillable = ['name', 'html', 'text', 'markdown', 'summary'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the user that created the page revision
|
* Get the user that created the page revision.
|
||||||
|
*
|
||||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||||
*/
|
*/
|
||||||
public function createdBy()
|
public function createdBy()
|
||||||
|
@ -33,6 +36,7 @@ class PageRevision extends Model
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the page this revision originates from.
|
* Get the page this revision originates from.
|
||||||
|
*
|
||||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||||
*/
|
*/
|
||||||
public function page()
|
public function page()
|
||||||
|
@ -42,7 +46,9 @@ class PageRevision extends Model
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the url for this revision.
|
* Get the url for this revision.
|
||||||
|
*
|
||||||
* @param null|string $path
|
* @param null|string $path
|
||||||
|
*
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
public function getUrl($path = null)
|
public function getUrl($path = null)
|
||||||
|
@ -51,11 +57,13 @@ class PageRevision extends Model
|
||||||
if ($path) {
|
if ($path) {
|
||||||
return $url . '/' . trim($path, '/');
|
return $url . '/' . trim($path, '/');
|
||||||
}
|
}
|
||||||
|
|
||||||
return $url;
|
return $url;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the previous revision for the same page if existing
|
* Get the previous revision for the same page if existing.
|
||||||
|
*
|
||||||
* @return \BookStack\Entities\PageRevision|null
|
* @return \BookStack\Entities\PageRevision|null
|
||||||
*/
|
*/
|
||||||
public function getPrevious()
|
public function getPrevious()
|
||||||
|
@ -74,8 +82,10 @@ class PageRevision extends Model
|
||||||
/**
|
/**
|
||||||
* Allows checking of the exact class, Used to check entity type.
|
* Allows checking of the exact class, Used to check entity type.
|
||||||
* Included here to align with entities in similar use cases.
|
* Included here to align with entities in similar use cases.
|
||||||
* (Yup, Bit of an awkward hack)
|
* (Yup, Bit of an awkward hack).
|
||||||
|
*
|
||||||
* @param $type
|
* @param $type
|
||||||
|
*
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public static function isA($type)
|
public static function isA($type)
|
||||||
|
|
|
@ -1,15 +1,17 @@
|
||||||
<?php namespace BookStack\Entities\Models;
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Entities\Models;
|
||||||
|
|
||||||
use BookStack\Model;
|
use BookStack\Model;
|
||||||
|
|
||||||
class SearchTerm extends Model
|
class SearchTerm extends Model
|
||||||
{
|
{
|
||||||
|
|
||||||
protected $fillable = ['term', 'entity_id', 'entity_type', 'score'];
|
protected $fillable = ['term', 'entity_id', 'entity_type', 'score'];
|
||||||
public $timestamps = false;
|
public $timestamps = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the entity that this term belongs to
|
* Get the entity that this term belongs to.
|
||||||
|
*
|
||||||
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
|
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
|
||||||
*/
|
*/
|
||||||
public function entity()
|
public function entity()
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
<?php namespace BookStack\Entities\Queries;
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Entities\Queries;
|
||||||
|
|
||||||
use BookStack\Auth\Permissions\PermissionService;
|
use BookStack\Auth\Permissions\PermissionService;
|
||||||
use BookStack\Entities\EntityProvider;
|
use BookStack\Entities\EntityProvider;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<?php namespace BookStack\Entities\Queries;
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Entities\Queries;
|
||||||
|
|
||||||
use BookStack\Actions\View;
|
use BookStack\Actions\View;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
|
@ -25,5 +26,4 @@ class Popular extends EntityQuery
|
||||||
->pluck('viewable')
|
->pluck('viewable')
|
||||||
->filter();
|
->filter();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -1,4 +1,6 @@
|
||||||
<?php namespace BookStack\Entities\Queries;
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Entities\Queries;
|
||||||
|
|
||||||
use BookStack\Actions\View;
|
use BookStack\Actions\View;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
<?php namespace BookStack\Entities\Queries;
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Entities\Queries;
|
||||||
|
|
||||||
use BookStack\Actions\Favourite;
|
use BookStack\Actions\Favourite;
|
||||||
use Illuminate\Database\Query\JoinClause;
|
use Illuminate\Database\Query\JoinClause;
|
||||||
|
|
|
@ -2,24 +2,18 @@
|
||||||
|
|
||||||
namespace BookStack\Entities\Repos;
|
namespace BookStack\Entities\Repos;
|
||||||
|
|
||||||
use BookStack\Actions\ActivityType;
|
|
||||||
use BookStack\Actions\TagRepo;
|
use BookStack\Actions\TagRepo;
|
||||||
use BookStack\Auth\User;
|
|
||||||
use BookStack\Entities\Models\Entity;
|
use BookStack\Entities\Models\Entity;
|
||||||
use BookStack\Entities\Models\HasCoverImage;
|
use BookStack\Entities\Models\HasCoverImage;
|
||||||
use BookStack\Exceptions\ImageUploadException;
|
use BookStack\Exceptions\ImageUploadException;
|
||||||
use BookStack\Facades\Activity;
|
|
||||||
use BookStack\Uploads\ImageRepo;
|
use BookStack\Uploads\ImageRepo;
|
||||||
use Illuminate\Http\UploadedFile;
|
use Illuminate\Http\UploadedFile;
|
||||||
use Illuminate\Support\Collection;
|
|
||||||
|
|
||||||
class BaseRepo
|
class BaseRepo
|
||||||
{
|
{
|
||||||
|
|
||||||
protected $tagRepo;
|
protected $tagRepo;
|
||||||
protected $imageRepo;
|
protected $imageRepo;
|
||||||
|
|
||||||
|
|
||||||
public function __construct(TagRepo $tagRepo, ImageRepo $imageRepo)
|
public function __construct(TagRepo $tagRepo, ImageRepo $imageRepo)
|
||||||
{
|
{
|
||||||
$this->tagRepo = $tagRepo;
|
$this->tagRepo = $tagRepo;
|
||||||
|
@ -27,7 +21,7 @@ class BaseRepo
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new entity in the system
|
* Create a new entity in the system.
|
||||||
*/
|
*/
|
||||||
public function create(Entity $entity, array $input)
|
public function create(Entity $entity, array $input)
|
||||||
{
|
{
|
||||||
|
@ -35,7 +29,7 @@ class BaseRepo
|
||||||
$entity->forceFill([
|
$entity->forceFill([
|
||||||
'created_by' => user()->id,
|
'created_by' => user()->id,
|
||||||
'updated_by' => user()->id,
|
'updated_by' => user()->id,
|
||||||
'owned_by' => user()->id,
|
'owned_by' => user()->id,
|
||||||
]);
|
]);
|
||||||
$entity->refreshSlug();
|
$entity->refreshSlug();
|
||||||
$entity->save();
|
$entity->save();
|
||||||
|
@ -72,6 +66,7 @@ class BaseRepo
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the given items' cover image, or clear it.
|
* Update the given items' cover image, or clear it.
|
||||||
|
*
|
||||||
* @throws ImageUploadException
|
* @throws ImageUploadException
|
||||||
* @throws \Exception
|
* @throws \Exception
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
<?php namespace BookStack\Entities\Repos;
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Entities\Repos;
|
||||||
|
|
||||||
use BookStack\Actions\ActivityType;
|
use BookStack\Actions\ActivityType;
|
||||||
use BookStack\Actions\TagRepo;
|
use BookStack\Actions\TagRepo;
|
||||||
|
@ -15,7 +17,6 @@ use Illuminate\Support\Collection;
|
||||||
|
|
||||||
class BookRepo
|
class BookRepo
|
||||||
{
|
{
|
||||||
|
|
||||||
protected $baseRepo;
|
protected $baseRepo;
|
||||||
protected $tagRepo;
|
protected $tagRepo;
|
||||||
protected $imageRepo;
|
protected $imageRepo;
|
||||||
|
@ -84,13 +85,14 @@ class BookRepo
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new book in the system
|
* Create a new book in the system.
|
||||||
*/
|
*/
|
||||||
public function create(array $input): Book
|
public function create(array $input): Book
|
||||||
{
|
{
|
||||||
$book = new Book();
|
$book = new Book();
|
||||||
$this->baseRepo->create($book, $input);
|
$this->baseRepo->create($book, $input);
|
||||||
Activity::addForEntity($book, ActivityType::BOOK_CREATE);
|
Activity::addForEntity($book, ActivityType::BOOK_CREATE);
|
||||||
|
|
||||||
return $book;
|
return $book;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,11 +103,13 @@ class BookRepo
|
||||||
{
|
{
|
||||||
$this->baseRepo->update($book, $input);
|
$this->baseRepo->update($book, $input);
|
||||||
Activity::addForEntity($book, ActivityType::BOOK_UPDATE);
|
Activity::addForEntity($book, ActivityType::BOOK_UPDATE);
|
||||||
|
|
||||||
return $book;
|
return $book;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the given book's cover image, or clear it.
|
* Update the given book's cover image, or clear it.
|
||||||
|
*
|
||||||
* @throws ImageUploadException
|
* @throws ImageUploadException
|
||||||
* @throws Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
|
@ -116,6 +120,7 @@ class BookRepo
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove a book from the system.
|
* Remove a book from the system.
|
||||||
|
*
|
||||||
* @throws Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
public function destroy(Book $book)
|
public function destroy(Book $book)
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
<?php namespace BookStack\Entities\Repos;
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Entities\Repos;
|
||||||
|
|
||||||
use BookStack\Actions\ActivityType;
|
use BookStack\Actions\ActivityType;
|
||||||
use BookStack\Entities\Models\Book;
|
use BookStack\Entities\Models\Book;
|
||||||
|
@ -89,6 +91,7 @@ class BookshelfRepo
|
||||||
$this->baseRepo->create($shelf, $input);
|
$this->baseRepo->create($shelf, $input);
|
||||||
$this->updateBooks($shelf, $bookIds);
|
$this->updateBooks($shelf, $bookIds);
|
||||||
Activity::addForEntity($shelf, ActivityType::BOOKSHELF_CREATE);
|
Activity::addForEntity($shelf, ActivityType::BOOKSHELF_CREATE);
|
||||||
|
|
||||||
return $shelf;
|
return $shelf;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,6 +107,7 @@ class BookshelfRepo
|
||||||
}
|
}
|
||||||
|
|
||||||
Activity::addForEntity($shelf, ActivityType::BOOKSHELF_UPDATE);
|
Activity::addForEntity($shelf, ActivityType::BOOKSHELF_UPDATE);
|
||||||
|
|
||||||
return $shelf;
|
return $shelf;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,6 +133,7 @@ class BookshelfRepo
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the given shelf cover image, or clear it.
|
* Update the given shelf cover image, or clear it.
|
||||||
|
*
|
||||||
* @throws ImageUploadException
|
* @throws ImageUploadException
|
||||||
* @throws Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
|
@ -164,6 +169,7 @@ class BookshelfRepo
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove a bookshelf from the system.
|
* Remove a bookshelf from the system.
|
||||||
|
*
|
||||||
* @throws Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
public function destroy(Bookshelf $shelf)
|
public function destroy(Bookshelf $shelf)
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
<?php namespace BookStack\Entities\Repos;
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Entities\Repos;
|
||||||
|
|
||||||
use BookStack\Actions\ActivityType;
|
use BookStack\Actions\ActivityType;
|
||||||
use BookStack\Entities\Models\Book;
|
use BookStack\Entities\Models\Book;
|
||||||
|
@ -9,11 +11,9 @@ use BookStack\Exceptions\MoveOperationException;
|
||||||
use BookStack\Exceptions\NotFoundException;
|
use BookStack\Exceptions\NotFoundException;
|
||||||
use BookStack\Facades\Activity;
|
use BookStack\Facades\Activity;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Support\Collection;
|
|
||||||
|
|
||||||
class ChapterRepo
|
class ChapterRepo
|
||||||
{
|
{
|
||||||
|
|
||||||
protected $baseRepo;
|
protected $baseRepo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -26,6 +26,7 @@ class ChapterRepo
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a chapter via the slug.
|
* Get a chapter via the slug.
|
||||||
|
*
|
||||||
* @throws NotFoundException
|
* @throws NotFoundException
|
||||||
*/
|
*/
|
||||||
public function getBySlug(string $bookSlug, string $chapterSlug): Chapter
|
public function getBySlug(string $bookSlug, string $chapterSlug): Chapter
|
||||||
|
@ -49,6 +50,7 @@ class ChapterRepo
|
||||||
$chapter->priority = (new BookContents($parentBook))->getLastPriority() + 1;
|
$chapter->priority = (new BookContents($parentBook))->getLastPriority() + 1;
|
||||||
$this->baseRepo->create($chapter, $input);
|
$this->baseRepo->create($chapter, $input);
|
||||||
Activity::addForEntity($chapter, ActivityType::CHAPTER_CREATE);
|
Activity::addForEntity($chapter, ActivityType::CHAPTER_CREATE);
|
||||||
|
|
||||||
return $chapter;
|
return $chapter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,11 +61,13 @@ class ChapterRepo
|
||||||
{
|
{
|
||||||
$this->baseRepo->update($chapter, $input);
|
$this->baseRepo->update($chapter, $input);
|
||||||
Activity::addForEntity($chapter, ActivityType::CHAPTER_UPDATE);
|
Activity::addForEntity($chapter, ActivityType::CHAPTER_UPDATE);
|
||||||
|
|
||||||
return $chapter;
|
return $chapter;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove a chapter from the system.
|
* Remove a chapter from the system.
|
||||||
|
*
|
||||||
* @throws Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
public function destroy(Chapter $chapter)
|
public function destroy(Chapter $chapter)
|
||||||
|
@ -77,7 +81,8 @@ class ChapterRepo
|
||||||
/**
|
/**
|
||||||
* Move the given chapter into a new parent book.
|
* Move the given chapter into a new parent book.
|
||||||
* The $parentIdentifier must be a string of the following format:
|
* The $parentIdentifier must be a string of the following format:
|
||||||
* 'book:<id>' (book:5)
|
* 'book:<id>' (book:5).
|
||||||
|
*
|
||||||
* @throws MoveOperationException
|
* @throws MoveOperationException
|
||||||
*/
|
*/
|
||||||
public function move(Chapter $chapter, string $parentIdentifier): Book
|
public function move(Chapter $chapter, string $parentIdentifier): Book
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
<?php namespace BookStack\Entities\Repos;
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Entities\Repos;
|
||||||
|
|
||||||
use BookStack\Actions\ActivityType;
|
use BookStack\Actions\ActivityType;
|
||||||
use BookStack\Entities\Models\Book;
|
use BookStack\Entities\Models\Book;
|
||||||
use BookStack\Entities\Models\Chapter;
|
use BookStack\Entities\Models\Chapter;
|
||||||
use BookStack\Entities\Models\Entity;
|
use BookStack\Entities\Models\Entity;
|
||||||
|
use BookStack\Entities\Models\Page;
|
||||||
|
use BookStack\Entities\Models\PageRevision;
|
||||||
use BookStack\Entities\Tools\BookContents;
|
use BookStack\Entities\Tools\BookContents;
|
||||||
use BookStack\Entities\Tools\PageContent;
|
use BookStack\Entities\Tools\PageContent;
|
||||||
use BookStack\Entities\Tools\TrashCan;
|
use BookStack\Entities\Tools\TrashCan;
|
||||||
use BookStack\Entities\Models\Page;
|
|
||||||
use BookStack\Entities\Models\PageRevision;
|
|
||||||
use BookStack\Exceptions\MoveOperationException;
|
use BookStack\Exceptions\MoveOperationException;
|
||||||
use BookStack\Exceptions\NotFoundException;
|
use BookStack\Exceptions\NotFoundException;
|
||||||
use BookStack\Exceptions\PermissionsException;
|
use BookStack\Exceptions\PermissionsException;
|
||||||
|
@ -16,11 +18,9 @@ use BookStack\Facades\Activity;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Pagination\LengthAwarePaginator;
|
use Illuminate\Pagination\LengthAwarePaginator;
|
||||||
use Illuminate\Support\Collection;
|
|
||||||
|
|
||||||
class PageRepo
|
class PageRepo
|
||||||
{
|
{
|
||||||
|
|
||||||
protected $baseRepo;
|
protected $baseRepo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -33,6 +33,7 @@ class PageRepo
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a page by ID.
|
* Get a page by ID.
|
||||||
|
*
|
||||||
* @throws NotFoundException
|
* @throws NotFoundException
|
||||||
*/
|
*/
|
||||||
public function getById(int $id, array $relations = ['book']): Page
|
public function getById(int $id, array $relations = ['book']): Page
|
||||||
|
@ -48,6 +49,7 @@ class PageRepo
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a page its book and own slug.
|
* Get a page its book and own slug.
|
||||||
|
*
|
||||||
* @throws NotFoundException
|
* @throws NotFoundException
|
||||||
*/
|
*/
|
||||||
public function getBySlug(string $bookSlug, string $pageSlug): Page
|
public function getBySlug(string $bookSlug, string $pageSlug): Page
|
||||||
|
@ -77,6 +79,7 @@ class PageRepo
|
||||||
->orderBy('created_at', 'desc')
|
->orderBy('created_at', 'desc')
|
||||||
->with('page')
|
->with('page')
|
||||||
->first();
|
->first();
|
||||||
|
|
||||||
return $revision ? $revision->page : null;
|
return $revision ? $revision->page : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,6 +122,7 @@ class PageRepo
|
||||||
public function getUserDraft(Page $page): ?PageRevision
|
public function getUserDraft(Page $page): ?PageRevision
|
||||||
{
|
{
|
||||||
$revision = $this->getUserDraftQuery($page)->first();
|
$revision = $this->getUserDraftQuery($page)->first();
|
||||||
|
|
||||||
return $revision;
|
return $revision;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -128,11 +132,11 @@ class PageRepo
|
||||||
public function getNewDraftPage(Entity $parent)
|
public function getNewDraftPage(Entity $parent)
|
||||||
{
|
{
|
||||||
$page = (new Page())->forceFill([
|
$page = (new Page())->forceFill([
|
||||||
'name' => trans('entities.pages_initial_name'),
|
'name' => trans('entities.pages_initial_name'),
|
||||||
'created_by' => user()->id,
|
'created_by' => user()->id,
|
||||||
'owned_by' => user()->id,
|
'owned_by' => user()->id,
|
||||||
'updated_by' => user()->id,
|
'updated_by' => user()->id,
|
||||||
'draft' => true,
|
'draft' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if ($parent instanceof Chapter) {
|
if ($parent instanceof Chapter) {
|
||||||
|
@ -144,6 +148,7 @@ class PageRepo
|
||||||
|
|
||||||
$page->save();
|
$page->save();
|
||||||
$page->refresh()->rebuildPermissions();
|
$page->refresh()->rebuildPermissions();
|
||||||
|
|
||||||
return $page;
|
return $page;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -166,6 +171,7 @@ class PageRepo
|
||||||
$draft->refresh();
|
$draft->refresh();
|
||||||
|
|
||||||
Activity::addForEntity($draft, ActivityType::PAGE_CREATE);
|
Activity::addForEntity($draft, ActivityType::PAGE_CREATE);
|
||||||
|
|
||||||
return $draft;
|
return $draft;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -190,7 +196,7 @@ class PageRepo
|
||||||
$this->getUserDraftQuery($page)->delete();
|
$this->getUserDraftQuery($page)->delete();
|
||||||
|
|
||||||
// Save a revision after updating
|
// Save a revision after updating
|
||||||
$summary = trim($input['summary'] ?? "");
|
$summary = trim($input['summary'] ?? '');
|
||||||
$htmlChanged = isset($input['html']) && $input['html'] !== $oldHtml;
|
$htmlChanged = isset($input['html']) && $input['html'] !== $oldHtml;
|
||||||
$nameChanged = isset($input['name']) && $input['name'] !== $oldName;
|
$nameChanged = isset($input['name']) && $input['name'] !== $oldName;
|
||||||
$markdownChanged = isset($input['markdown']) && $input['markdown'] !== $oldMarkdown;
|
$markdownChanged = isset($input['markdown']) && $input['markdown'] !== $oldMarkdown;
|
||||||
|
@ -199,6 +205,7 @@ class PageRepo
|
||||||
}
|
}
|
||||||
|
|
||||||
Activity::addForEntity($page, ActivityType::PAGE_UPDATE);
|
Activity::addForEntity($page, ActivityType::PAGE_UPDATE);
|
||||||
|
|
||||||
return $page;
|
return $page;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -211,8 +218,8 @@ class PageRepo
|
||||||
$pageContent = new PageContent($page);
|
$pageContent = new PageContent($page);
|
||||||
if (!empty($input['markdown'] ?? '')) {
|
if (!empty($input['markdown'] ?? '')) {
|
||||||
$pageContent->setNewMarkdown($input['markdown']);
|
$pageContent->setNewMarkdown($input['markdown']);
|
||||||
} else {
|
} elseif (isset($input['html'])) {
|
||||||
$pageContent->setNewHTML($input['html'] ?? '');
|
$pageContent->setNewHTML($input['html']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -234,6 +241,7 @@ class PageRepo
|
||||||
$revision->save();
|
$revision->save();
|
||||||
|
|
||||||
$this->deleteOldRevisions($page);
|
$this->deleteOldRevisions($page);
|
||||||
|
|
||||||
return $revision;
|
return $revision;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -249,6 +257,7 @@ class PageRepo
|
||||||
}
|
}
|
||||||
$page->fill($input);
|
$page->fill($input);
|
||||||
$page->save();
|
$page->save();
|
||||||
|
|
||||||
return $page;
|
return $page;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -260,11 +269,13 @@ class PageRepo
|
||||||
}
|
}
|
||||||
|
|
||||||
$draft->save();
|
$draft->save();
|
||||||
|
|
||||||
return $draft;
|
return $draft;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Destroy a page from the system.
|
* Destroy a page from the system.
|
||||||
|
*
|
||||||
* @throws Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
public function destroy(Page $page)
|
public function destroy(Page $page)
|
||||||
|
@ -301,13 +312,15 @@ class PageRepo
|
||||||
$this->savePageRevision($page, $summary);
|
$this->savePageRevision($page, $summary);
|
||||||
|
|
||||||
Activity::addForEntity($page, ActivityType::PAGE_RESTORE);
|
Activity::addForEntity($page, ActivityType::PAGE_RESTORE);
|
||||||
|
|
||||||
return $page;
|
return $page;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Move the given page into a new parent book or chapter.
|
* Move the given page into a new parent book or chapter.
|
||||||
* The $parentIdentifier must be a string of the following format:
|
* The $parentIdentifier must be a string of the following format:
|
||||||
* 'book:<id>' (book:5)
|
* 'book:<id>' (book:5).
|
||||||
|
*
|
||||||
* @throws MoveOperationException
|
* @throws MoveOperationException
|
||||||
* @throws PermissionsException
|
* @throws PermissionsException
|
||||||
*/
|
*/
|
||||||
|
@ -327,12 +340,14 @@ class PageRepo
|
||||||
$page->rebuildPermissions();
|
$page->rebuildPermissions();
|
||||||
|
|
||||||
Activity::addForEntity($page, ActivityType::PAGE_MOVE);
|
Activity::addForEntity($page, ActivityType::PAGE_MOVE);
|
||||||
|
|
||||||
return $parent;
|
return $parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copy an existing page in the system.
|
* Copy an existing page in the system.
|
||||||
* Optionally providing a new parent via string identifier and a new name.
|
* Optionally providing a new parent via string identifier and a new name.
|
||||||
|
*
|
||||||
* @throws MoveOperationException
|
* @throws MoveOperationException
|
||||||
* @throws PermissionsException
|
* @throws PermissionsException
|
||||||
*/
|
*/
|
||||||
|
@ -369,7 +384,8 @@ class PageRepo
|
||||||
/**
|
/**
|
||||||
* Find a page parent entity via a identifier string in the format:
|
* Find a page parent entity via a identifier string in the format:
|
||||||
* {type}:{id}
|
* {type}:{id}
|
||||||
* Example: (book:5)
|
* Example: (book:5).
|
||||||
|
*
|
||||||
* @throws MoveOperationException
|
* @throws MoveOperationException
|
||||||
*/
|
*/
|
||||||
protected function findParentByIdentifier(string $identifier): ?Entity
|
protected function findParentByIdentifier(string $identifier): ?Entity
|
||||||
|
@ -383,6 +399,7 @@ class PageRepo
|
||||||
}
|
}
|
||||||
|
|
||||||
$parentClass = $entityType === 'book' ? Book::class : Chapter::class;
|
$parentClass = $entityType === 'book' ? Book::class : Chapter::class;
|
||||||
|
|
||||||
return $parentClass::visible()->where('id', '=', $entityId)->first();
|
return $parentClass::visible()->where('id', '=', $entityId)->first();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -420,6 +437,7 @@ class PageRepo
|
||||||
$draft->book_slug = $page->book->slug;
|
$draft->book_slug = $page->book->slug;
|
||||||
$draft->created_by = user()->id;
|
$draft->created_by = user()->id;
|
||||||
$draft->type = 'update_draft';
|
$draft->type = 'update_draft';
|
||||||
|
|
||||||
return $draft;
|
return $draft;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -445,13 +463,14 @@ class PageRepo
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a new priority for a page
|
* Get a new priority for a page.
|
||||||
*/
|
*/
|
||||||
protected function getNewPriority(Page $page): int
|
protected function getNewPriority(Page $page): int
|
||||||
{
|
{
|
||||||
$parent = $page->getParent();
|
$parent = $page->getParent();
|
||||||
if ($parent instanceof Chapter) {
|
if ($parent instanceof Chapter) {
|
||||||
$lastPage = $parent->pages('desc')->first();
|
$lastPage = $parent->pages('desc')->first();
|
||||||
|
|
||||||
return $lastPage ? $lastPage->priority + 1 : 0;
|
return $lastPage ? $lastPage->priority + 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
<?php namespace BookStack\Entities\Tools;
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Entities\Tools;
|
||||||
|
|
||||||
use BookStack\Entities\Models\Book;
|
use BookStack\Entities\Models\Book;
|
||||||
use BookStack\Entities\Models\BookChild;
|
use BookStack\Entities\Models\BookChild;
|
||||||
|
@ -10,7 +12,6 @@ use Illuminate\Support\Collection;
|
||||||
|
|
||||||
class BookContents
|
class BookContents
|
||||||
{
|
{
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Book
|
* @var Book
|
||||||
*/
|
*/
|
||||||
|
@ -35,6 +36,7 @@ class BookContents
|
||||||
->where('chapter_id', '=', 0)->max('priority');
|
->where('chapter_id', '=', 0)->max('priority');
|
||||||
$maxChapter = Chapter::visible()->where('book_id', '=', $this->book->id)
|
$maxChapter = Chapter::visible()->where('book_id', '=', $this->book->id)
|
||||||
->max('priority');
|
->max('priority');
|
||||||
|
|
||||||
return max($maxChapter, $maxPage, 1);
|
return max($maxChapter, $maxPage, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,7 +45,7 @@ class BookContents
|
||||||
*/
|
*/
|
||||||
public function getTree(bool $showDrafts = false, bool $renderPages = false): Collection
|
public function getTree(bool $showDrafts = false, bool $renderPages = false): Collection
|
||||||
{
|
{
|
||||||
$pages = $this->getPages($showDrafts);
|
$pages = $this->getPages($showDrafts, $renderPages);
|
||||||
$chapters = Chapter::visible()->where('book_id', '=', $this->book->id)->get();
|
$chapters = Chapter::visible()->where('book_id', '=', $this->book->id)->get();
|
||||||
$all = collect()->concat($pages)->concat($chapters);
|
$all = collect()->concat($pages)->concat($chapters);
|
||||||
$chapterMap = $chapters->keyBy('id');
|
$chapterMap = $chapters->keyBy('id');
|
||||||
|
@ -83,6 +85,7 @@ class BookContents
|
||||||
if (isset($entity['draft']) && $entity['draft']) {
|
if (isset($entity['draft']) && $entity['draft']) {
|
||||||
return -100;
|
return -100;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $entity['priority'] ?? 0;
|
return $entity['priority'] ?? 0;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -90,9 +93,11 @@ class BookContents
|
||||||
/**
|
/**
|
||||||
* Get the visible pages within this book.
|
* Get the visible pages within this book.
|
||||||
*/
|
*/
|
||||||
protected function getPages(bool $showDrafts = false): Collection
|
protected function getPages(bool $showDrafts = false, bool $getPageContent = false): Collection
|
||||||
{
|
{
|
||||||
$query = Page::visible()->where('book_id', '=', $this->book->id);
|
$query = Page::visible()
|
||||||
|
->select($getPageContent ? Page::$contentAttributes : Page::$listAttributes)
|
||||||
|
->where('book_id', '=', $this->book->id);
|
||||||
|
|
||||||
if (!$showDrafts) {
|
if (!$showDrafts) {
|
||||||
$query->where('draft', '=', false);
|
$query->where('draft', '=', false);
|
||||||
|
@ -110,9 +115,10 @@ class BookContents
|
||||||
* +"parentChapter": false (ID of parent chapter, as string, or false)
|
* +"parentChapter": false (ID of parent chapter, as string, or false)
|
||||||
* +"type": "page" (Entity type of item)
|
* +"type": "page" (Entity type of item)
|
||||||
* +"book": "1" (Id of book to place item in)
|
* +"book": "1" (Id of book to place item in)
|
||||||
* }
|
* }.
|
||||||
*
|
*
|
||||||
* Returns a list of books that were involved in the operation.
|
* Returns a list of books that were involved in the operation.
|
||||||
|
*
|
||||||
* @throws SortOperationException
|
* @throws SortOperationException
|
||||||
*/
|
*/
|
||||||
public function sortUsingMap(Collection $sortMap): Collection
|
public function sortUsingMap(Collection $sortMap): Collection
|
||||||
|
@ -190,6 +196,7 @@ class BookContents
|
||||||
/**
|
/**
|
||||||
* Get the books involved in a sort.
|
* Get the books involved in a sort.
|
||||||
* The given sort map should have its models loaded first.
|
* The given sort map should have its models loaded first.
|
||||||
|
*
|
||||||
* @throws SortOperationException
|
* @throws SortOperationException
|
||||||
*/
|
*/
|
||||||
protected function getBooksInvolvedInSort(Collection $sortMap): Collection
|
protected function getBooksInvolvedInSort(Collection $sortMap): Collection
|
||||||
|
@ -202,7 +209,7 @@ class BookContents
|
||||||
$books = Book::hasPermission('update')->whereIn('id', $bookIdsInvolved)->get();
|
$books = Book::hasPermission('update')->whereIn('id', $bookIdsInvolved)->get();
|
||||||
|
|
||||||
if (count($books) !== count($bookIdsInvolved)) {
|
if (count($books) !== count($bookIdsInvolved)) {
|
||||||
throw new SortOperationException("Could not find all books requested in sort operation");
|
throw new SortOperationException('Could not find all books requested in sort operation');
|
||||||
}
|
}
|
||||||
|
|
||||||
return $books;
|
return $books;
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
<?php namespace BookStack\Entities\Tools;
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Entities\Tools;
|
||||||
|
|
||||||
use BookStack\Entities\Models\Book;
|
use BookStack\Entities\Models\Book;
|
||||||
use BookStack\Entities\Models\Chapter;
|
use BookStack\Entities\Models\Chapter;
|
||||||
use BookStack\Entities\Models\Page;
|
use BookStack\Entities\Models\Page;
|
||||||
|
use BookStack\Entities\Tools\Markdown\HtmlToMarkdown;
|
||||||
use BookStack\Uploads\ImageService;
|
use BookStack\Uploads\ImageService;
|
||||||
use DomPDF;
|
use DomPDF;
|
||||||
use Exception;
|
use Exception;
|
||||||
|
@ -11,7 +14,6 @@ use Throwable;
|
||||||
|
|
||||||
class ExportFormatter
|
class ExportFormatter
|
||||||
{
|
{
|
||||||
|
|
||||||
protected $imageService;
|
protected $imageService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -25,20 +27,23 @@ class ExportFormatter
|
||||||
/**
|
/**
|
||||||
* Convert a page to a self-contained HTML file.
|
* Convert a page to a self-contained HTML file.
|
||||||
* Includes required CSS & image content. Images are base64 encoded into the HTML.
|
* Includes required CSS & image content. Images are base64 encoded into the HTML.
|
||||||
|
*
|
||||||
* @throws Throwable
|
* @throws Throwable
|
||||||
*/
|
*/
|
||||||
public function pageToContainedHtml(Page $page)
|
public function pageToContainedHtml(Page $page)
|
||||||
{
|
{
|
||||||
$page->html = (new PageContent($page))->render();
|
$page->html = (new PageContent($page))->render();
|
||||||
$pageHtml = view('pages.export', [
|
$pageHtml = view('pages.export', [
|
||||||
'page' => $page,
|
'page' => $page,
|
||||||
'format' => 'html',
|
'format' => 'html',
|
||||||
])->render();
|
])->render();
|
||||||
|
|
||||||
return $this->containHtml($pageHtml);
|
return $this->containHtml($pageHtml);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a chapter to a self-contained HTML file.
|
* Convert a chapter to a self-contained HTML file.
|
||||||
|
*
|
||||||
* @throws Throwable
|
* @throws Throwable
|
||||||
*/
|
*/
|
||||||
public function chapterToContainedHtml(Chapter $chapter)
|
public function chapterToContainedHtml(Chapter $chapter)
|
||||||
|
@ -49,43 +54,49 @@ class ExportFormatter
|
||||||
});
|
});
|
||||||
$html = view('chapters.export', [
|
$html = view('chapters.export', [
|
||||||
'chapter' => $chapter,
|
'chapter' => $chapter,
|
||||||
'pages' => $pages,
|
'pages' => $pages,
|
||||||
'format' => 'html',
|
'format' => 'html',
|
||||||
])->render();
|
])->render();
|
||||||
|
|
||||||
return $this->containHtml($html);
|
return $this->containHtml($html);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a book to a self-contained HTML file.
|
* Convert a book to a self-contained HTML file.
|
||||||
|
*
|
||||||
* @throws Throwable
|
* @throws Throwable
|
||||||
*/
|
*/
|
||||||
public function bookToContainedHtml(Book $book)
|
public function bookToContainedHtml(Book $book)
|
||||||
{
|
{
|
||||||
$bookTree = (new BookContents($book))->getTree(false, true);
|
$bookTree = (new BookContents($book))->getTree(false, true);
|
||||||
$html = view('books.export', [
|
$html = view('books.export', [
|
||||||
'book' => $book,
|
'book' => $book,
|
||||||
'bookChildren' => $bookTree,
|
'bookChildren' => $bookTree,
|
||||||
'format' => 'html',
|
'format' => 'html',
|
||||||
])->render();
|
])->render();
|
||||||
|
|
||||||
return $this->containHtml($html);
|
return $this->containHtml($html);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a page to a PDF file.
|
* Convert a page to a PDF file.
|
||||||
|
*
|
||||||
* @throws Throwable
|
* @throws Throwable
|
||||||
*/
|
*/
|
||||||
public function pageToPdf(Page $page)
|
public function pageToPdf(Page $page)
|
||||||
{
|
{
|
||||||
$page->html = (new PageContent($page))->render();
|
$page->html = (new PageContent($page))->render();
|
||||||
$html = view('pages.export', [
|
$html = view('pages.export', [
|
||||||
'page' => $page,
|
'page' => $page,
|
||||||
'format' => 'pdf',
|
'format' => 'pdf',
|
||||||
])->render();
|
])->render();
|
||||||
|
|
||||||
return $this->htmlToPdf($html);
|
return $this->htmlToPdf($html);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a chapter to a PDF file.
|
* Convert a chapter to a PDF file.
|
||||||
|
*
|
||||||
* @throws Throwable
|
* @throws Throwable
|
||||||
*/
|
*/
|
||||||
public function chapterToPdf(Chapter $chapter)
|
public function chapterToPdf(Chapter $chapter)
|
||||||
|
@ -97,8 +108,8 @@ class ExportFormatter
|
||||||
|
|
||||||
$html = view('chapters.export', [
|
$html = view('chapters.export', [
|
||||||
'chapter' => $chapter,
|
'chapter' => $chapter,
|
||||||
'pages' => $pages,
|
'pages' => $pages,
|
||||||
'format' => 'pdf',
|
'format' => 'pdf',
|
||||||
])->render();
|
])->render();
|
||||||
|
|
||||||
return $this->htmlToPdf($html);
|
return $this->htmlToPdf($html);
|
||||||
|
@ -106,38 +117,43 @@ class ExportFormatter
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a book to a PDF file.
|
* Convert a book to a PDF file.
|
||||||
|
*
|
||||||
* @throws Throwable
|
* @throws Throwable
|
||||||
*/
|
*/
|
||||||
public function bookToPdf(Book $book)
|
public function bookToPdf(Book $book)
|
||||||
{
|
{
|
||||||
$bookTree = (new BookContents($book))->getTree(false, true);
|
$bookTree = (new BookContents($book))->getTree(false, true);
|
||||||
$html = view('books.export', [
|
$html = view('books.export', [
|
||||||
'book' => $book,
|
'book' => $book,
|
||||||
'bookChildren' => $bookTree,
|
'bookChildren' => $bookTree,
|
||||||
'format' => 'pdf',
|
'format' => 'pdf',
|
||||||
])->render();
|
])->render();
|
||||||
|
|
||||||
return $this->htmlToPdf($html);
|
return $this->htmlToPdf($html);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert normal web-page HTML to a PDF.
|
* Convert normal web-page HTML to a PDF.
|
||||||
|
*
|
||||||
* @throws Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
protected function htmlToPdf(string $html): string
|
protected function htmlToPdf(string $html): string
|
||||||
{
|
{
|
||||||
$containedHtml = $this->containHtml($html);
|
$containedHtml = $this->containHtml($html);
|
||||||
$useWKHTML = config('snappy.pdf.binary') !== false;
|
$useWKHTML = config('snappy.pdf.binary') !== false && config('app.allow_untrusted_server_fetching') === true;
|
||||||
if ($useWKHTML) {
|
if ($useWKHTML) {
|
||||||
$pdf = SnappyPDF::loadHTML($containedHtml);
|
$pdf = SnappyPDF::loadHTML($containedHtml);
|
||||||
$pdf->setOption('print-media-type', true);
|
$pdf->setOption('print-media-type', true);
|
||||||
} else {
|
} else {
|
||||||
$pdf = DomPDF::loadHTML($containedHtml);
|
$pdf = DomPDF::loadHTML($containedHtml);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $pdf->output();
|
return $pdf->output();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bundle of the contents of a html file to be self-contained.
|
* Bundle of the contents of a html file to be self-contained.
|
||||||
|
*
|
||||||
* @throws Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
protected function containHtml(string $htmlContent): string
|
protected function containHtml(string $htmlContent): string
|
||||||
|
@ -194,6 +210,7 @@ class ExportFormatter
|
||||||
$text = html_entity_decode($text);
|
$text = html_entity_decode($text);
|
||||||
// Add title
|
// Add title
|
||||||
$text = $page->name . "\n\n" . $text;
|
$text = $page->name . "\n\n" . $text;
|
||||||
|
|
||||||
return $text;
|
return $text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -207,6 +224,7 @@ class ExportFormatter
|
||||||
foreach ($chapter->getVisiblePages() as $page) {
|
foreach ($chapter->getVisiblePages() as $page) {
|
||||||
$text .= $this->pageToPlainText($page);
|
$text .= $this->pageToPlainText($page);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $text;
|
return $text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -224,6 +242,51 @@ class ExportFormatter
|
||||||
$text .= $this->pageToPlainText($bookChild);
|
$text .= $this->pageToPlainText($bookChild);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return $text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a page to a Markdown file.
|
||||||
|
*/
|
||||||
|
public function pageToMarkdown(Page $page): string
|
||||||
|
{
|
||||||
|
if ($page->markdown) {
|
||||||
|
return '# ' . $page->name . "\n\n" . $page->markdown;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '# ' . $page->name . "\n\n" . (new HtmlToMarkdown($page->html))->convert();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a chapter to a Markdown file.
|
||||||
|
*/
|
||||||
|
public function chapterToMarkdown(Chapter $chapter): string
|
||||||
|
{
|
||||||
|
$text = '# ' . $chapter->name . "\n\n";
|
||||||
|
$text .= $chapter->description . "\n\n";
|
||||||
|
foreach ($chapter->pages as $page) {
|
||||||
|
$text .= $this->pageToMarkdown($page) . "\n\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a book into a plain text string.
|
||||||
|
*/
|
||||||
|
public function bookToMarkdown(Book $book): string
|
||||||
|
{
|
||||||
|
$bookTree = (new BookContents($book))->getTree(false, true);
|
||||||
|
$text = '# ' . $book->name . "\n\n";
|
||||||
|
foreach ($bookTree as $bookChild) {
|
||||||
|
if ($bookChild instanceof Chapter) {
|
||||||
|
$text .= $this->chapterToMarkdown($bookChild);
|
||||||
|
} else {
|
||||||
|
$text .= $this->pageToMarkdown($bookChild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return $text;
|
return $text;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Entities\Tools\Markdown;
|
||||||
|
|
||||||
|
use League\CommonMark\Block\Element\AbstractBlock;
|
||||||
|
use League\CommonMark\Block\Element\ListItem;
|
||||||
|
use League\CommonMark\Block\Element\Paragraph;
|
||||||
|
use League\CommonMark\Block\Renderer\BlockRendererInterface;
|
||||||
|
use League\CommonMark\Block\Renderer\ListItemRenderer;
|
||||||
|
use League\CommonMark\ElementRendererInterface;
|
||||||
|
use League\CommonMark\Extension\TaskList\TaskListItemMarker;
|
||||||
|
use League\CommonMark\HtmlElement;
|
||||||
|
|
||||||
|
class CustomListItemRenderer implements BlockRendererInterface
|
||||||
|
{
|
||||||
|
protected $baseRenderer;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->baseRenderer = new ListItemRenderer();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return HtmlElement|string|null
|
||||||
|
*/
|
||||||
|
public function render(AbstractBlock $block, ElementRendererInterface $htmlRenderer, bool $inTightList = false)
|
||||||
|
{
|
||||||
|
$listItem = $this->baseRenderer->render($block, $htmlRenderer, $inTightList);
|
||||||
|
|
||||||
|
if ($this->startsTaskListItem($block)) {
|
||||||
|
$listItem->setAttribute('class', 'task-list-item');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $listItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function startsTaskListItem(ListItem $block): bool
|
||||||
|
{
|
||||||
|
$firstChild = $block->firstChild();
|
||||||
|
|
||||||
|
return $firstChild instanceof Paragraph && $firstChild->firstChild() instanceof TaskListItemMarker;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Entities\Tools\Markdown;
|
||||||
|
|
||||||
|
use League\HTMLToMarkdown\Converter\ParagraphConverter;
|
||||||
|
use League\HTMLToMarkdown\ElementInterface;
|
||||||
|
|
||||||
|
class CustomParagraphConverter extends ParagraphConverter
|
||||||
|
{
|
||||||
|
public function convert(ElementInterface $element): string
|
||||||
|
{
|
||||||
|
$class = $element->getAttribute('class');
|
||||||
|
if (strpos($class, 'callout') !== false) {
|
||||||
|
return "<{$element->getTagName()} class=\"{$class}\">{$element->getValue()}</{$element->getTagName()}>\n\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent::convert($element);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,6 @@
|
||||||
<?php namespace BookStack\Entities\Tools\Markdown;
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Entities\Tools\Markdown;
|
||||||
|
|
||||||
use League\CommonMark\ConfigurableEnvironmentInterface;
|
use League\CommonMark\ConfigurableEnvironmentInterface;
|
||||||
use League\CommonMark\Extension\ExtensionInterface;
|
use League\CommonMark\Extension\ExtensionInterface;
|
||||||
|
@ -7,7 +9,6 @@ use League\CommonMark\Extension\Strikethrough\StrikethroughDelimiterProcessor;
|
||||||
|
|
||||||
class CustomStrikeThroughExtension implements ExtensionInterface
|
class CustomStrikeThroughExtension implements ExtensionInterface
|
||||||
{
|
{
|
||||||
|
|
||||||
public function register(ConfigurableEnvironmentInterface $environment)
|
public function register(ConfigurableEnvironmentInterface $environment)
|
||||||
{
|
{
|
||||||
$environment->addDelimiterProcessor(new StrikethroughDelimiterProcessor());
|
$environment->addDelimiterProcessor(new StrikethroughDelimiterProcessor());
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
<?php namespace BookStack\Entities\Tools\Markdown;
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Entities\Tools\Markdown;
|
||||||
|
|
||||||
use League\CommonMark\ElementRendererInterface;
|
use League\CommonMark\ElementRendererInterface;
|
||||||
use League\CommonMark\Extension\Strikethrough\Strikethrough;
|
use League\CommonMark\Extension\Strikethrough\Strikethrough;
|
||||||
|
|
|
@ -0,0 +1,93 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Entities\Tools\Markdown;
|
||||||
|
|
||||||
|
use League\HTMLToMarkdown\Converter\BlockquoteConverter;
|
||||||
|
use League\HTMLToMarkdown\Converter\CodeConverter;
|
||||||
|
use League\HTMLToMarkdown\Converter\CommentConverter;
|
||||||
|
use League\HTMLToMarkdown\Converter\DivConverter;
|
||||||
|
use League\HTMLToMarkdown\Converter\EmphasisConverter;
|
||||||
|
use League\HTMLToMarkdown\Converter\HardBreakConverter;
|
||||||
|
use League\HTMLToMarkdown\Converter\HeaderConverter;
|
||||||
|
use League\HTMLToMarkdown\Converter\HorizontalRuleConverter;
|
||||||
|
use League\HTMLToMarkdown\Converter\ImageConverter;
|
||||||
|
use League\HTMLToMarkdown\Converter\LinkConverter;
|
||||||
|
use League\HTMLToMarkdown\Converter\ListBlockConverter;
|
||||||
|
use League\HTMLToMarkdown\Converter\ListItemConverter;
|
||||||
|
use League\HTMLToMarkdown\Converter\PreformattedConverter;
|
||||||
|
use League\HTMLToMarkdown\Converter\TextConverter;
|
||||||
|
use League\HTMLToMarkdown\Environment;
|
||||||
|
use League\HTMLToMarkdown\HtmlConverter;
|
||||||
|
|
||||||
|
class HtmlToMarkdown
|
||||||
|
{
|
||||||
|
protected $html;
|
||||||
|
|
||||||
|
public function __construct(string $html)
|
||||||
|
{
|
||||||
|
$this->html = $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run the conversion.
|
||||||
|
*/
|
||||||
|
public function convert(): string
|
||||||
|
{
|
||||||
|
$converter = new HtmlConverter($this->getConverterEnvironment());
|
||||||
|
$html = $this->prepareHtml($this->html);
|
||||||
|
|
||||||
|
return $converter->convert($html);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run any pre-processing to the HTML to clean it up manually before conversion.
|
||||||
|
*/
|
||||||
|
protected function prepareHtml(string $html): string
|
||||||
|
{
|
||||||
|
// Carriage returns can cause whitespace issues in output
|
||||||
|
$html = str_replace("\r\n", "\n", $html);
|
||||||
|
// Attributes on the pre tag can cause issues with conversion
|
||||||
|
return preg_replace('/<pre .*?>/', '<pre>', $html);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the HTML to Markdown customized environment.
|
||||||
|
* Extends the default provided environment with some BookStack specific tweaks.
|
||||||
|
*/
|
||||||
|
protected function getConverterEnvironment(): Environment
|
||||||
|
{
|
||||||
|
$environment = new Environment([
|
||||||
|
'header_style' => 'atx', // Set to 'atx' to output H1 and H2 headers as # Header1 and ## Header2
|
||||||
|
'suppress_errors' => true, // Set to false to show warnings when loading malformed HTML
|
||||||
|
'strip_tags' => false, // Set to true to strip tags that don't have markdown equivalents. N.B. Strips tags, not their content. Useful to clean MS Word HTML output.
|
||||||
|
'strip_placeholder_links' => false, // Set to true to remove <a> that doesn't have href.
|
||||||
|
'bold_style' => '**', // DEPRECATED: Set to '__' if you prefer the underlined style
|
||||||
|
'italic_style' => '*', // DEPRECATED: Set to '_' if you prefer the underlined style
|
||||||
|
'remove_nodes' => '', // space-separated list of dom nodes that should be removed. example: 'meta style script'
|
||||||
|
'hard_break' => false, // Set to true to turn <br> into `\n` instead of ` \n`
|
||||||
|
'list_item_style' => '-', // Set the default character for each <li> in a <ul>. Can be '-', '*', or '+'
|
||||||
|
'preserve_comments' => false, // Set to true to preserve comments, or set to an array of strings to preserve specific comments
|
||||||
|
'use_autolinks' => false, // Set to true to use simple link syntax if possible. Will always use []() if set to false
|
||||||
|
'table_pipe_escape' => '\|', // Replacement string for pipe characters inside markdown table cells
|
||||||
|
'table_caption_side' => 'top', // Set to 'top' or 'bottom' to show <caption> content before or after table, null to suppress
|
||||||
|
]);
|
||||||
|
|
||||||
|
$environment->addConverter(new BlockquoteConverter());
|
||||||
|
$environment->addConverter(new CodeConverter());
|
||||||
|
$environment->addConverter(new CommentConverter());
|
||||||
|
$environment->addConverter(new DivConverter());
|
||||||
|
$environment->addConverter(new EmphasisConverter());
|
||||||
|
$environment->addConverter(new HardBreakConverter());
|
||||||
|
$environment->addConverter(new HeaderConverter());
|
||||||
|
$environment->addConverter(new HorizontalRuleConverter());
|
||||||
|
$environment->addConverter(new ImageConverter());
|
||||||
|
$environment->addConverter(new LinkConverter());
|
||||||
|
$environment->addConverter(new ListBlockConverter());
|
||||||
|
$environment->addConverter(new ListItemConverter());
|
||||||
|
$environment->addConverter(new CustomParagraphConverter());
|
||||||
|
$environment->addConverter(new PreformattedConverter());
|
||||||
|
$environment->addConverter(new TextConverter());
|
||||||
|
|
||||||
|
return $environment;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,6 @@
|
||||||
<?php namespace BookStack\Entities\Tools;
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Entities\Tools;
|
||||||
|
|
||||||
use BookStack\Entities\Models\BookChild;
|
use BookStack\Entities\Models\BookChild;
|
||||||
use BookStack\Entities\Models\Entity;
|
use BookStack\Entities\Models\Entity;
|
||||||
|
@ -48,6 +50,7 @@ class NextPreviousContentLocator
|
||||||
return get_class($entity) === get_class($this->relativeBookItem)
|
return get_class($entity) === get_class($this->relativeBookItem)
|
||||||
&& $entity->id === $this->relativeBookItem->id;
|
&& $entity->id === $this->relativeBookItem->id;
|
||||||
});
|
});
|
||||||
|
|
||||||
return $index === false ? null : $index;
|
return $index === false ? null : $index;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,6 +67,7 @@ class NextPreviousContentLocator
|
||||||
$childPages = $item->visible_pages ?? [];
|
$childPages = $item->visible_pages ?? [];
|
||||||
$flatOrdered = $flatOrdered->concat($childPages);
|
$flatOrdered = $flatOrdered->concat($childPages);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $flatOrdered;
|
return $flatOrdered;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue