mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-05-14 10:12:50 +08:00
commit
4b36df08a8
15
.env.example
15
.env.example
@ -1,3 +1,11 @@
|
|||||||
|
# This file, when named as ".env" in the root of your BookStack install
|
||||||
|
# folder, is used for the core configuration of the application.
|
||||||
|
# By default this file contains the most common required options but
|
||||||
|
# a full list of options can be found in the '.env.example.complete' file.
|
||||||
|
|
||||||
|
# NOTE: If any of your values contain a space or a hash you will need to
|
||||||
|
# wrap the entire value in quotes. (eg. MAIL_FROM_NAME="BookStack Mailer")
|
||||||
|
|
||||||
# Application key
|
# Application key
|
||||||
# Used for encryption where needed.
|
# Used for encryption where needed.
|
||||||
# Run `php artisan key:generate` to generate a valid key.
|
# Run `php artisan key:generate` to generate a valid key.
|
||||||
@ -5,7 +13,7 @@ APP_KEY=SomeRandomString
|
|||||||
|
|
||||||
# Application URL
|
# Application URL
|
||||||
# Remove the hash below and set a URL if using BookStack behind
|
# Remove the hash below and set a URL if using BookStack behind
|
||||||
# a proxy, if using a third-party authentication option.
|
# a proxy or if using a third-party authentication option.
|
||||||
# This must be the root URL that you want to host BookStack on.
|
# This must be the root URL that you want to host BookStack on.
|
||||||
# All URL's in BookStack will be generated using this value.
|
# All URL's in BookStack will be generated using this value.
|
||||||
#APP_URL=https://example.com
|
#APP_URL=https://example.com
|
||||||
@ -25,11 +33,10 @@ MAIL_FROM_NAME=BookStack
|
|||||||
MAIL_FROM=bookstack@example.com
|
MAIL_FROM=bookstack@example.com
|
||||||
|
|
||||||
# SMTP mail options
|
# SMTP mail options
|
||||||
|
# These settings can be checked using the "Send a Test Email"
|
||||||
|
# feature found in the "Settings > Maintenance" area of the system.
|
||||||
MAIL_HOST=localhost
|
MAIL_HOST=localhost
|
||||||
MAIL_PORT=1025
|
MAIL_PORT=1025
|
||||||
MAIL_USERNAME=null
|
MAIL_USERNAME=null
|
||||||
MAIL_PASSWORD=null
|
MAIL_PASSWORD=null
|
||||||
MAIL_ENCRYPTION=null
|
MAIL_ENCRYPTION=null
|
||||||
|
|
||||||
|
|
||||||
# A full list of options can be found in the '.env.example.complete' file.
|
|
@ -238,7 +238,10 @@ DISABLE_EXTERNAL_SERVICES=false
|
|||||||
# Example: AVATAR_URL=https://seccdn.libravatar.org/avatar/${hash}?s=${size}&d=identicon
|
# Example: AVATAR_URL=https://seccdn.libravatar.org/avatar/${hash}?s=${size}&d=identicon
|
||||||
AVATAR_URL=
|
AVATAR_URL=
|
||||||
|
|
||||||
# Enable Draw.io integration
|
# Enable diagrams.net integration
|
||||||
|
# Can simply be true/false to enable/disable the integration.
|
||||||
|
# Alternatively, It can be URL to the diagrams.net instance you want to use.
|
||||||
|
# For URLs, The following URL parameters should be included: embed=1&proto=json&spin=1
|
||||||
DRAWIO=true
|
DRAWIO=true
|
||||||
|
|
||||||
# Default item listing view
|
# Default item listing view
|
||||||
@ -252,6 +255,14 @@ APP_VIEWS_BOOKSHELVES=grid
|
|||||||
# If set to 'false' a limit will not be enforced.
|
# If set to 'false' a limit will not be enforced.
|
||||||
REVISION_LIMIT=50
|
REVISION_LIMIT=50
|
||||||
|
|
||||||
|
# Recycle Bin Lifetime
|
||||||
|
# The number of days that content will remain in the recycle bin before
|
||||||
|
# being considered for auto-removal. It is not a guarantee that content will
|
||||||
|
# be removed after this time.
|
||||||
|
# Set to 0 for no recycle bin functionality.
|
||||||
|
# Set to -1 for unlimited recycle bin lifetime.
|
||||||
|
RECYCLE_BIN_LIFETIME=30
|
||||||
|
|
||||||
# Allow <script> tags in page content
|
# Allow <script> tags in page content
|
||||||
# Note, if set to 'true' the page editor may still escape scripts.
|
# Note, if set to 'true' the page editor may still escape scripts.
|
||||||
ALLOW_CONTENT_SCRIPTS=false
|
ALLOW_CONTENT_SCRIPTS=false
|
||||||
@ -268,3 +279,11 @@ 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.
|
||||||
API_REQUESTS_PER_MIN=180
|
API_REQUESTS_PER_MIN=180
|
||||||
|
|
||||||
|
# Enable the logging of failed email+password logins with the given message.
|
||||||
|
# The default log channel below uses the php 'error_log' function which commonly
|
||||||
|
# results in messages being output to the webserver error logs.
|
||||||
|
# The message can contain a %u parameter which will be replaced with the login
|
||||||
|
# user identifier (Username or email).
|
||||||
|
LOG_FAILED_LOGIN_MESSAGE=false
|
||||||
|
LOG_FAILED_LOGIN_CHANNEL=errorlog_plain_webserver
|
||||||
|
38
.github/translators.txt
vendored
38
.github/translators.txt
vendored
@ -61,7 +61,7 @@ Rodrigo Saczuk Niz (rodrigoniz) :: Portuguese, Brazilian
|
|||||||
aekramer :: Dutch
|
aekramer :: Dutch
|
||||||
JachuPL :: Polish
|
JachuPL :: Polish
|
||||||
milesteg :: Hungarian
|
milesteg :: Hungarian
|
||||||
Beenbag :: German
|
Beenbag :: German; German Informal
|
||||||
Lett3rs :: Danish
|
Lett3rs :: Danish
|
||||||
Julian (julian.henneberg) :: German; German Informal
|
Julian (julian.henneberg) :: German; German Informal
|
||||||
3GNWn :: Danish
|
3GNWn :: Danish
|
||||||
@ -87,3 +87,39 @@ Rafael (raribeir) :: Portuguese, Brazilian
|
|||||||
Hiroyuki Odake (dakesan) :: Japanese
|
Hiroyuki Odake (dakesan) :: Japanese
|
||||||
Alex Lee (qianmengnet) :: Chinese Simplified
|
Alex Lee (qianmengnet) :: Chinese Simplified
|
||||||
swinn37 :: French
|
swinn37 :: French
|
||||||
|
Hasan Özbey (the-turk) :: Turkish
|
||||||
|
rcy :: Swedish
|
||||||
|
Ali Yasir Yılmaz (ayyilmaz) :: Turkish
|
||||||
|
scureza :: Italian
|
||||||
|
Biepa :: German Informal; German
|
||||||
|
syecu :: Chinese Simplified
|
||||||
|
Lap1t0r :: French
|
||||||
|
Thinkverse (thinkverse) :: Swedish
|
||||||
|
alef (toishoki) :: Turkish
|
||||||
|
Robbert Feunekes (Muukuro) :: Dutch
|
||||||
|
seohyeon.joo :: Korean
|
||||||
|
Orenda (OREDNA) :: Bulgarian
|
||||||
|
Marek Pavelka (marapavelka) :: Czech
|
||||||
|
Venkinovec :: Czech
|
||||||
|
Tommy Ku (tommyku) :: Chinese Traditional; Japanese
|
||||||
|
Michał Bielejewski (bielej) :: Polish
|
||||||
|
jozefrebjak :: Slovak
|
||||||
|
Ikhwan Koo (Ikhwan.Koo) :: Korean
|
||||||
|
Whay (remkovdhoef) :: Dutch
|
||||||
|
jc7115 :: Chinese Traditional
|
||||||
|
주서현 (seohyeon.joo) :: Korean
|
||||||
|
ReadySystems :: Arabic
|
||||||
|
HFinch :: German; German Informal
|
||||||
|
brechtgijsens :: Dutch
|
||||||
|
Lowkey (v587ygq) :: Chinese Simplified
|
||||||
|
sdl-blue :: German Informal
|
||||||
|
sqlik :: Polish
|
||||||
|
Roy van Schaijk (royvanschaijk) :: Dutch
|
||||||
|
Simsimpicpic :: French
|
||||||
|
Zenahr Barzani (Zenahr) :: German; Japanese; Dutch; German Informal
|
||||||
|
tatsuya.info :: Japanese
|
||||||
|
fadiapp :: Arabic
|
||||||
|
Jakub Bouček (jakubboucek) :: Czech
|
||||||
|
Marco (cdrfun) :: German
|
||||||
|
10935336 :: Chinese Simplified
|
||||||
|
孟繁阳 (FanyangMeng) :: Chinese Simplified
|
||||||
|
2
.github/workflows/phpunit.yml
vendored
2
.github/workflows/phpunit.yml
vendored
@ -16,7 +16,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
php: [7.2, 7.3]
|
php: [7.2, 7.4]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v1
|
||||||
|
|
||||||
|
58
.github/workflows/test-migrations.yml
vendored
Normal file
58
.github/workflows/test-migrations.yml
vendored
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
name: test-migrations
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- release
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- '*'
|
||||||
|
- '*/*'
|
||||||
|
- '!l10n_master'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
php: [7.2, 7.4]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v1
|
||||||
|
|
||||||
|
- name: Get Composer Cache Directory
|
||||||
|
id: composer-cache
|
||||||
|
run: |
|
||||||
|
echo "::set-output name=dir::$(composer config cache-files-dir)"
|
||||||
|
|
||||||
|
- name: Cache composer packages
|
||||||
|
uses: actions/cache@v1
|
||||||
|
with:
|
||||||
|
path: ${{ steps.composer-cache.outputs.dir }}
|
||||||
|
key: ${{ runner.os }}-composer-${{ matrix.php }}
|
||||||
|
|
||||||
|
- name: Start MySQL
|
||||||
|
run: |
|
||||||
|
sudo /etc/init.d/mysql start
|
||||||
|
|
||||||
|
- name: Create database & user
|
||||||
|
run: |
|
||||||
|
mysql -uroot -proot -e 'CREATE DATABASE IF NOT EXISTS `bookstack-test`;'
|
||||||
|
mysql -uroot -proot -e "CREATE USER 'bookstack-test'@'localhost' IDENTIFIED BY 'bookstack-test';"
|
||||||
|
mysql -uroot -proot -e "GRANT ALL ON \`bookstack-test\`.* TO 'bookstack-test'@'localhost';"
|
||||||
|
mysql -uroot -proot -e 'FLUSH PRIVILEGES;'
|
||||||
|
|
||||||
|
- name: Install composer dependencies
|
||||||
|
run: composer install --prefer-dist --no-interaction --ansi
|
||||||
|
|
||||||
|
- name: Start migration test
|
||||||
|
run: |
|
||||||
|
php${{ matrix.php }} artisan migrate --force -n --database=mysql_testing
|
||||||
|
|
||||||
|
- name: Start migration:rollback test
|
||||||
|
run: |
|
||||||
|
php${{ matrix.php }} artisan migrate:rollback --force -n --database=mysql_testing
|
||||||
|
|
||||||
|
- name: Start migration rerun test
|
||||||
|
run: |
|
||||||
|
php${{ matrix.php }} artisan migrate --force -n --database=mysql_testing
|
@ -3,18 +3,19 @@
|
|||||||
namespace BookStack\Actions;
|
namespace BookStack\Actions;
|
||||||
|
|
||||||
use BookStack\Auth\User;
|
use BookStack\Auth\User;
|
||||||
use BookStack\Entities\Entity;
|
use BookStack\Entities\Models\Entity;
|
||||||
use BookStack\Model;
|
use BookStack\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property string $key
|
* @property string $type
|
||||||
* @property User $user
|
* @property User $user
|
||||||
* @property Entity $entity
|
* @property Entity $entity
|
||||||
* @property string $extra
|
* @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
|
||||||
* @property int $book_id
|
|
||||||
*/
|
*/
|
||||||
class Activity extends Model
|
class Activity extends Model
|
||||||
{
|
{
|
||||||
@ -32,29 +33,35 @@ class Activity extends Model
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the user this activity relates to.
|
* Get the user this activity relates to.
|
||||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
|
||||||
*/
|
*/
|
||||||
public function user()
|
public function user(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(User::class);
|
return $this->belongsTo(User::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns text from the language files, Looks up by using the
|
* Returns text from the language files, Looks up by using the activity key.
|
||||||
* activity key.
|
|
||||||
*/
|
*/
|
||||||
public function getText()
|
public function getText(): string
|
||||||
{
|
{
|
||||||
return trans('activities.' . $this->key);
|
return trans('activities.' . $this->type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this activity is intended to be for an entity.
|
||||||
|
*/
|
||||||
|
public function isForEntity(): bool
|
||||||
|
{
|
||||||
|
return Str::startsWith($this->type, [
|
||||||
|
'page_', 'chapter_', 'book_', 'bookshelf_'
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if another Activity matches the general information of another.
|
* Checks if another Activity matches the general information of another.
|
||||||
* @param $activityB
|
|
||||||
* @return bool
|
|
||||||
*/
|
*/
|
||||||
public function isSimilarTo($activityB)
|
public function isSimilarTo(Activity $activityB): bool
|
||||||
{
|
{
|
||||||
return [$this->key, $this->entity_type, $this->entity_id] === [$activityB->key, $activityB->entity_type, $activityB->entity_id];
|
return [$this->type, $this->entity_type, $this->entity_id] === [$activityB->type, $activityB->entity_type, $activityB->entity_id];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,67 +1,60 @@
|
|||||||
<?php namespace BookStack\Actions;
|
<?php namespace BookStack\Actions;
|
||||||
|
|
||||||
use BookStack\Auth\Permissions\PermissionService;
|
use BookStack\Auth\Permissions\PermissionService;
|
||||||
use BookStack\Entities\Book;
|
use BookStack\Auth\User;
|
||||||
use BookStack\Entities\Entity;
|
use BookStack\Entities\Models\Chapter;
|
||||||
|
use BookStack\Entities\Models\Entity;
|
||||||
|
use BookStack\Entities\Models\Page;
|
||||||
|
use BookStack\Interfaces\Loggable;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
class ActivityService
|
class ActivityService
|
||||||
{
|
{
|
||||||
protected $activity;
|
protected $activity;
|
||||||
protected $user;
|
|
||||||
protected $permissionService;
|
protected $permissionService;
|
||||||
|
|
||||||
/**
|
|
||||||
* ActivityService constructor.
|
|
||||||
* @param Activity $activity
|
|
||||||
* @param PermissionService $permissionService
|
|
||||||
*/
|
|
||||||
public function __construct(Activity $activity, PermissionService $permissionService)
|
public function __construct(Activity $activity, PermissionService $permissionService)
|
||||||
{
|
{
|
||||||
$this->activity = $activity;
|
$this->activity = $activity;
|
||||||
$this->permissionService = $permissionService;
|
$this->permissionService = $permissionService;
|
||||||
$this->user = user();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add activity data to database.
|
* Add activity data to database for an entity.
|
||||||
* @param \BookStack\Entities\Entity $entity
|
|
||||||
* @param string $activityKey
|
|
||||||
* @param int $bookId
|
|
||||||
*/
|
*/
|
||||||
public function add(Entity $entity, string $activityKey, int $bookId = null)
|
public function addForEntity(Entity $entity, string $type)
|
||||||
{
|
{
|
||||||
$activity = $this->newActivityForUser($activityKey, $bookId);
|
$activity = $this->newActivityForUser($type);
|
||||||
$entity->activity()->save($activity);
|
$entity->activity()->save($activity);
|
||||||
$this->setNotification($activityKey);
|
$this->setNotification($type);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a activity history with a message, without binding to a entity.
|
* Add a generic activity event to the database.
|
||||||
* @param string $activityKey
|
* @param string|Loggable $detail
|
||||||
* @param string $message
|
|
||||||
* @param int $bookId
|
|
||||||
*/
|
*/
|
||||||
public function addMessage(string $activityKey, string $message, int $bookId = null)
|
public function add(string $type, $detail = '')
|
||||||
{
|
{
|
||||||
$this->newActivityForUser($activityKey, $bookId)->forceFill([
|
if ($detail instanceof Loggable) {
|
||||||
'extra' => $message
|
$detail = $detail->logDescriptor();
|
||||||
])->save();
|
}
|
||||||
|
|
||||||
$this->setNotification($activityKey);
|
$activity = $this->newActivityForUser($type);
|
||||||
|
$activity->detail = $detail;
|
||||||
|
$activity->save();
|
||||||
|
$this->setNotification($type);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a new activity instance for the current user.
|
* Get a new activity instance for the current user.
|
||||||
* @param string $key
|
|
||||||
* @param int|null $bookId
|
|
||||||
* @return Activity
|
|
||||||
*/
|
*/
|
||||||
protected function newActivityForUser(string $key, int $bookId = null)
|
protected function newActivityForUser(string $type): Activity
|
||||||
{
|
{
|
||||||
return $this->activity->newInstance()->forceFill([
|
return $this->activity->newInstance()->forceFill([
|
||||||
'key' => strtolower($key),
|
'type' => strtolower($type),
|
||||||
'user_id' => $this->user->id,
|
'user_id' => user()->id,
|
||||||
'book_id' => $bookId ?? 0,
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,34 +62,25 @@ class ActivityService
|
|||||||
* Removes the entity attachment from each of its activities
|
* Removes the entity attachment from each of its activities
|
||||||
* and instead uses the 'extra' field with the entities name.
|
* and instead uses the 'extra' field with the entities name.
|
||||||
* Used when an entity is deleted.
|
* Used when an entity is deleted.
|
||||||
* @param \BookStack\Entities\Entity $entity
|
|
||||||
* @return mixed
|
|
||||||
*/
|
*/
|
||||||
public function removeEntity(Entity $entity)
|
public function removeEntity(Entity $entity)
|
||||||
{
|
{
|
||||||
// TODO - Rewrite to db query.
|
$entity->activity()->update([
|
||||||
$activities = $entity->activity;
|
'detail' => $entity->name,
|
||||||
foreach ($activities as $activity) {
|
'entity_id' => null,
|
||||||
$activity->extra = $entity->name;
|
'entity_type' => null,
|
||||||
$activity->entity_id = 0;
|
]);
|
||||||
$activity->entity_type = null;
|
|
||||||
$activity->save();
|
|
||||||
}
|
|
||||||
return $activities;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the latest activity.
|
* Gets the latest activity.
|
||||||
* @param int $count
|
|
||||||
* @param int $page
|
|
||||||
* @return array
|
|
||||||
*/
|
*/
|
||||||
public function latest($count = 20, $page = 0)
|
public function latest(int $count = 20, int $page = 0): array
|
||||||
{
|
{
|
||||||
$activityList = $this->permissionService
|
$activityList = $this->permissionService
|
||||||
->filterRestrictedEntityRelations($this->activity, 'activities', 'entity_id', 'entity_type')
|
->filterRestrictedEntityRelations($this->activity, 'activities', 'entity_id', 'entity_type')
|
||||||
->orderBy('created_at', 'desc')
|
->orderBy('created_at', 'desc')
|
||||||
->with('user', 'entity')
|
->with(['user', 'entity'])
|
||||||
->skip($count * $page)
|
->skip($count * $page)
|
||||||
->take($count)
|
->take($count)
|
||||||
->get();
|
->get();
|
||||||
@ -107,24 +91,33 @@ class ActivityService
|
|||||||
/**
|
/**
|
||||||
* Gets the latest activity for an entity, Filtering out similar
|
* Gets the latest activity for an entity, Filtering out similar
|
||||||
* items to prevent a message activity list.
|
* items to prevent a message activity list.
|
||||||
* @param \BookStack\Entities\Entity $entity
|
|
||||||
* @param int $count
|
|
||||||
* @param int $page
|
|
||||||
* @return array
|
|
||||||
*/
|
*/
|
||||||
public function entityActivity($entity, $count = 20, $page = 1)
|
public function entityActivity(Entity $entity, int $count = 20, int $page = 1): array
|
||||||
{
|
{
|
||||||
|
/** @var [string => int[]] $queryIds */
|
||||||
|
$queryIds = [$entity->getMorphClass() => [$entity->id]];
|
||||||
|
|
||||||
if ($entity->isA('book')) {
|
if ($entity->isA('book')) {
|
||||||
$query = $this->activity->where('book_id', '=', $entity->id);
|
$queryIds[(new Chapter)->getMorphClass()] = $entity->chapters()->visible()->pluck('id');
|
||||||
} else {
|
}
|
||||||
$query = $this->activity->where('entity_type', '=', $entity->getMorphClass())
|
if ($entity->isA('book') || $entity->isA('chapter')) {
|
||||||
->where('entity_id', '=', $entity->id);
|
$queryIds[(new Page)->getMorphClass()] = $entity->pages()->visible()->pluck('id');
|
||||||
}
|
}
|
||||||
|
|
||||||
$activity = $this->permissionService
|
$query = $this->activity->newQuery();
|
||||||
->filterRestrictedEntityRelations($query, 'activities', 'entity_id', 'entity_type')
|
$query->where(function (Builder $query) use ($queryIds) {
|
||||||
->orderBy('created_at', 'desc')
|
foreach ($queryIds as $morphClass => $idArr) {
|
||||||
->with(['entity', 'user.avatar'])
|
$query->orWhere(function (Builder $innerQuery) use ($morphClass, $idArr) {
|
||||||
|
$innerQuery->where('entity_type', '=', $morphClass)
|
||||||
|
->whereIn('entity_id', $idArr);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$activity = $query->orderBy('created_at', 'desc')
|
||||||
|
->with(['entity' => function (Relation $query) {
|
||||||
|
$query->withTrashed();
|
||||||
|
}, 'user.avatar'])
|
||||||
->skip($count * ($page - 1))
|
->skip($count * ($page - 1))
|
||||||
->take($count)
|
->take($count)
|
||||||
->get();
|
->get();
|
||||||
@ -133,18 +126,18 @@ class ActivityService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get latest activity for a user, Filtering out similar
|
* Get latest activity for a user, Filtering out similar items.
|
||||||
* items.
|
|
||||||
* @param $user
|
|
||||||
* @param int $count
|
|
||||||
* @param int $page
|
|
||||||
* @return array
|
|
||||||
*/
|
*/
|
||||||
public function userActivity($user, $count = 20, $page = 0)
|
public function userActivity(User $user, int $count = 20, int $page = 0): array
|
||||||
{
|
{
|
||||||
$activityList = $this->permissionService
|
$activityList = $this->permissionService
|
||||||
->filterRestrictedEntityRelations($this->activity, 'activities', 'entity_id', 'entity_type')
|
->filterRestrictedEntityRelations($this->activity, 'activities', 'entity_id', 'entity_type')
|
||||||
->orderBy('created_at', 'desc')->where('user_id', '=', $user->id)->skip($count * $page)->take($count)->get();
|
->orderBy('created_at', 'desc')
|
||||||
|
->where('user_id', '=', $user->id)
|
||||||
|
->skip($count * $page)
|
||||||
|
->take($count)
|
||||||
|
->get();
|
||||||
|
|
||||||
return $this->filterSimilar($activityList);
|
return $this->filterSimilar($activityList);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -153,34 +146,47 @@ class ActivityService
|
|||||||
* @param Activity[] $activities
|
* @param Activity[] $activities
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
protected function filterSimilar($activities)
|
protected function filterSimilar(iterable $activities): array
|
||||||
{
|
{
|
||||||
$newActivity = [];
|
$newActivity = [];
|
||||||
$previousItem = false;
|
$previousItem = null;
|
||||||
|
|
||||||
foreach ($activities as $activityItem) {
|
foreach ($activities as $activityItem) {
|
||||||
if ($previousItem === false) {
|
if (!$previousItem || !$activityItem->isSimilarTo($previousItem)) {
|
||||||
$previousItem = $activityItem;
|
|
||||||
$newActivity[] = $activityItem;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!$activityItem->isSimilarTo($previousItem)) {
|
|
||||||
$newActivity[] = $activityItem;
|
$newActivity[] = $activityItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
$previousItem = $activityItem;
|
$previousItem = $activityItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $newActivity;
|
return $newActivity;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flashes a notification message to the session if an appropriate message is available.
|
* Flashes a notification message to the session if an appropriate message is available.
|
||||||
* @param $activityKey
|
|
||||||
*/
|
*/
|
||||||
protected function setNotification($activityKey)
|
protected function setNotification(string $type)
|
||||||
{
|
{
|
||||||
$notificationTextKey = 'activities.' . $activityKey . '_notification';
|
$notificationTextKey = 'activities.' . $type . '_notification';
|
||||||
if (trans()->has($notificationTextKey)) {
|
if (trans()->has($notificationTextKey)) {
|
||||||
$message = trans($notificationTextKey);
|
$message = trans($notificationTextKey);
|
||||||
session()->flash('success', $message);
|
session()->flash('success', $message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log out a failed login attempt, Providing the given username
|
||||||
|
* as part of the message if the '%u' string is used.
|
||||||
|
*/
|
||||||
|
public function logFailedLogin(string $username)
|
||||||
|
{
|
||||||
|
$message = config('logging.failed_login.message');
|
||||||
|
if (!$message) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$message = str_replace("%u", $username, $message);
|
||||||
|
$channel = config('logging.failed_login.channel');
|
||||||
|
Log::channel($channel)->warning($message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
51
app/Actions/ActivityType.php
Normal file
51
app/Actions/ActivityType.php
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<?php namespace BookStack\Actions;
|
||||||
|
|
||||||
|
class ActivityType
|
||||||
|
{
|
||||||
|
const PAGE_CREATE = 'page_create';
|
||||||
|
const PAGE_UPDATE = 'page_update';
|
||||||
|
const PAGE_DELETE = 'page_delete';
|
||||||
|
const PAGE_RESTORE = 'page_restore';
|
||||||
|
const PAGE_MOVE = 'page_move';
|
||||||
|
|
||||||
|
const CHAPTER_CREATE = 'chapter_create';
|
||||||
|
const CHAPTER_UPDATE = 'chapter_update';
|
||||||
|
const CHAPTER_DELETE = 'chapter_delete';
|
||||||
|
const CHAPTER_MOVE = 'chapter_move';
|
||||||
|
|
||||||
|
const BOOK_CREATE = 'book_create';
|
||||||
|
const BOOK_UPDATE = 'book_update';
|
||||||
|
const BOOK_DELETE = 'book_delete';
|
||||||
|
const BOOK_SORT = 'book_sort';
|
||||||
|
|
||||||
|
const BOOKSHELF_CREATE = 'bookshelf_create';
|
||||||
|
const BOOKSHELF_UPDATE = 'bookshelf_update';
|
||||||
|
const BOOKSHELF_DELETE = 'bookshelf_delete';
|
||||||
|
|
||||||
|
const COMMENTED_ON = 'commented_on';
|
||||||
|
const PERMISSIONS_UPDATE = 'permissions_update';
|
||||||
|
|
||||||
|
const SETTINGS_UPDATE = 'settings_update';
|
||||||
|
const MAINTENANCE_ACTION_RUN = 'maintenance_action_run';
|
||||||
|
|
||||||
|
const RECYCLE_BIN_EMPTY = 'recycle_bin_empty';
|
||||||
|
const RECYCLE_BIN_RESTORE = 'recycle_bin_restore';
|
||||||
|
const RECYCLE_BIN_DESTROY = 'recycle_bin_destroy';
|
||||||
|
|
||||||
|
const USER_CREATE = 'user_create';
|
||||||
|
const USER_UPDATE = 'user_update';
|
||||||
|
const USER_DELETE = 'user_delete';
|
||||||
|
|
||||||
|
const API_TOKEN_CREATE = 'api_token_create';
|
||||||
|
const API_TOKEN_UPDATE = 'api_token_update';
|
||||||
|
const API_TOKEN_DELETE = 'api_token_delete';
|
||||||
|
|
||||||
|
const ROLE_CREATE = 'role_create';
|
||||||
|
const ROLE_UPDATE = 'role_update';
|
||||||
|
const ROLE_DELETE = 'role_delete';
|
||||||
|
|
||||||
|
const AUTH_PASSWORD_RESET = 'auth_password_reset_request';
|
||||||
|
const AUTH_PASSWORD_RESET_UPDATE = 'auth_password_reset_update';
|
||||||
|
const AUTH_LOGIN = 'auth_login';
|
||||||
|
const AUTH_REGISTER = 'auth_register';
|
||||||
|
}
|
@ -2,9 +2,15 @@
|
|||||||
|
|
||||||
use BookStack\Ownable;
|
use BookStack\Ownable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property string text
|
||||||
|
* @property string html
|
||||||
|
* @property int|null parent_id
|
||||||
|
* @property int local_id
|
||||||
|
*/
|
||||||
class Comment extends Ownable
|
class Comment extends Ownable
|
||||||
{
|
{
|
||||||
protected $fillable = ['text', 'html', 'parent_id'];
|
protected $fillable = ['text', 'parent_id'];
|
||||||
protected $appends = ['created', 'updated'];
|
protected $appends = ['created', 'updated'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,23 +1,21 @@
|
|||||||
<?php namespace BookStack\Actions;
|
<?php namespace BookStack\Actions;
|
||||||
|
|
||||||
use BookStack\Entities\Entity;
|
use BookStack\Entities\Models\Entity;
|
||||||
|
use League\CommonMark\CommonMarkConverter;
|
||||||
|
use BookStack\Facades\Activity as ActivityService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class CommentRepo
|
* Class CommentRepo
|
||||||
* @package BookStack\Repos
|
|
||||||
*/
|
*/
|
||||||
class CommentRepo
|
class CommentRepo
|
||||||
{
|
{
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var \BookStack\Actions\Comment $comment
|
* @var Comment $comment
|
||||||
*/
|
*/
|
||||||
protected $comment;
|
protected $comment;
|
||||||
|
|
||||||
/**
|
|
||||||
* CommentRepo constructor.
|
|
||||||
* @param \BookStack\Actions\Comment $comment
|
|
||||||
*/
|
|
||||||
public function __construct(Comment $comment)
|
public function __construct(Comment $comment)
|
||||||
{
|
{
|
||||||
$this->comment = $comment;
|
$this->comment = $comment;
|
||||||
@ -25,65 +23,72 @@ class CommentRepo
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a comment by ID.
|
* Get a comment by ID.
|
||||||
* @param $id
|
|
||||||
* @return \BookStack\Actions\Comment|\Illuminate\Database\Eloquent\Model
|
|
||||||
*/
|
*/
|
||||||
public function getById($id)
|
public function getById(int $id): Comment
|
||||||
{
|
{
|
||||||
return $this->comment->newQuery()->findOrFail($id);
|
return $this->comment->newQuery()->findOrFail($id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new comment on an entity.
|
* Create a new comment on an entity.
|
||||||
* @param \BookStack\Entities\Entity $entity
|
|
||||||
* @param array $data
|
|
||||||
* @return \BookStack\Actions\Comment
|
|
||||||
*/
|
*/
|
||||||
public function create(Entity $entity, $data = [])
|
public function create(Entity $entity, string $text, ?int $parent_id): Comment
|
||||||
{
|
{
|
||||||
$userId = user()->id;
|
$userId = user()->id;
|
||||||
$comment = $this->comment->newInstance($data);
|
$comment = $this->comment->newInstance();
|
||||||
|
|
||||||
|
$comment->text = $text;
|
||||||
|
$comment->html = $this->commentToHtml($text);
|
||||||
$comment->created_by = $userId;
|
$comment->created_by = $userId;
|
||||||
$comment->updated_by = $userId;
|
$comment->updated_by = $userId;
|
||||||
$comment->local_id = $this->getNextLocalId($entity);
|
$comment->local_id = $this->getNextLocalId($entity);
|
||||||
|
$comment->parent_id = $parent_id;
|
||||||
|
|
||||||
$entity->comments()->save($comment);
|
$entity->comments()->save($comment);
|
||||||
|
ActivityService::addForEntity($entity, ActivityType::COMMENTED_ON);
|
||||||
return $comment;
|
return $comment;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update an existing comment.
|
* Update an existing comment.
|
||||||
* @param \BookStack\Actions\Comment $comment
|
|
||||||
* @param array $input
|
|
||||||
* @return mixed
|
|
||||||
*/
|
*/
|
||||||
public function update($comment, $input)
|
public function update(Comment $comment, string $text): Comment
|
||||||
{
|
{
|
||||||
$comment->updated_by = user()->id;
|
$comment->updated_by = user()->id;
|
||||||
$comment->update($input);
|
$comment->text = $text;
|
||||||
|
$comment->html = $this->commentToHtml($text);
|
||||||
|
$comment->save();
|
||||||
return $comment;
|
return $comment;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a comment from the system.
|
* Delete a comment from the system.
|
||||||
* @param \BookStack\Actions\Comment $comment
|
|
||||||
* @return mixed
|
|
||||||
*/
|
*/
|
||||||
public function delete($comment)
|
public function delete(Comment $comment)
|
||||||
{
|
{
|
||||||
return $comment->delete();
|
$comment->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert the given comment markdown text to HTML.
|
||||||
|
*/
|
||||||
|
public function commentToHtml(string $commentText): string
|
||||||
|
{
|
||||||
|
$converter = new CommonMarkConverter([
|
||||||
|
'html_input' => 'strip',
|
||||||
|
'max_nesting_level' => 10,
|
||||||
|
'allow_unsafe_links' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $converter->convertToHtml($commentText);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the next local ID relative to the linked entity.
|
* Get the next local ID relative to the linked entity.
|
||||||
* @param \BookStack\Entities\Entity $entity
|
|
||||||
* @return int
|
|
||||||
*/
|
*/
|
||||||
protected function getNextLocalId(Entity $entity)
|
protected function getNextLocalId(Entity $entity): int
|
||||||
{
|
{
|
||||||
$comments = $entity->comments(false)->orderBy('local_id', 'desc')->first();
|
$comments = $entity->comments(false)->orderBy('local_id', 'desc')->first();
|
||||||
if ($comments === null) {
|
return ($comments->local_id ?? 0) + 1;
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return $comments->local_id + 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,13 +2,10 @@
|
|||||||
|
|
||||||
use BookStack\Model;
|
use BookStack\Model;
|
||||||
|
|
||||||
/**
|
|
||||||
* Class Attribute
|
|
||||||
* @package BookStack
|
|
||||||
*/
|
|
||||||
class Tag extends Model
|
class Tag extends Model
|
||||||
{
|
{
|
||||||
protected $fillable = ['name', 'value', 'order'];
|
protected $fillable = ['name', 'value', 'order'];
|
||||||
|
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
|
||||||
|
@ -1,72 +1,32 @@
|
|||||||
<?php namespace BookStack\Actions;
|
<?php namespace BookStack\Actions;
|
||||||
|
|
||||||
use BookStack\Auth\Permissions\PermissionService;
|
use BookStack\Auth\Permissions\PermissionService;
|
||||||
use BookStack\Entities\Entity;
|
use BookStack\Entities\Models\Entity;
|
||||||
|
use DB;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
/**
|
|
||||||
* Class TagRepo
|
|
||||||
* @package BookStack\Repos
|
|
||||||
*/
|
|
||||||
class TagRepo
|
class TagRepo
|
||||||
{
|
{
|
||||||
|
|
||||||
protected $tag;
|
protected $tag;
|
||||||
protected $entity;
|
|
||||||
protected $permissionService;
|
protected $permissionService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TagRepo constructor.
|
* TagRepo constructor.
|
||||||
* @param \BookStack\Actions\Tag $attr
|
|
||||||
* @param \BookStack\Entities\Entity $ent
|
|
||||||
* @param \BookStack\Auth\Permissions\PermissionService $ps
|
|
||||||
*/
|
*/
|
||||||
public function __construct(Tag $attr, Entity $ent, PermissionService $ps)
|
public function __construct(Tag $tag, PermissionService $ps)
|
||||||
{
|
{
|
||||||
$this->tag = $attr;
|
$this->tag = $tag;
|
||||||
$this->entity = $ent;
|
|
||||||
$this->permissionService = $ps;
|
$this->permissionService = $ps;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get an entity instance of its particular type.
|
|
||||||
* @param $entityType
|
|
||||||
* @param $entityId
|
|
||||||
* @param string $action
|
|
||||||
* @return \Illuminate\Database\Eloquent\Model|null|static
|
|
||||||
*/
|
|
||||||
public function getEntity($entityType, $entityId, $action = 'view')
|
|
||||||
{
|
|
||||||
$entityInstance = $this->entity->getEntityInstance($entityType);
|
|
||||||
$searchQuery = $entityInstance->where('id', '=', $entityId)->with('tags');
|
|
||||||
$searchQuery = $this->permissionService->enforceEntityRestrictions($entityType, $searchQuery, $action);
|
|
||||||
return $searchQuery->first();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all tags for a particular entity.
|
|
||||||
* @param string $entityType
|
|
||||||
* @param int $entityId
|
|
||||||
* @return mixed
|
|
||||||
*/
|
|
||||||
public function getForEntity($entityType, $entityId)
|
|
||||||
{
|
|
||||||
$entity = $this->getEntity($entityType, $entityId);
|
|
||||||
if ($entity === null) {
|
|
||||||
return collect();
|
|
||||||
}
|
|
||||||
|
|
||||||
return $entity->tags;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get tag name suggestions from scanning existing tag names.
|
* Get tag name suggestions from scanning existing tag names.
|
||||||
* If no search term is given the 50 most popular tag names are provided.
|
* If no search term is given the 50 most popular tag names are provided.
|
||||||
* @param $searchTerm
|
|
||||||
* @return array
|
|
||||||
*/
|
*/
|
||||||
public function getNameSuggestions($searchTerm = false)
|
public function getNameSuggestions(?string $searchTerm): Collection
|
||||||
{
|
{
|
||||||
$query = $this->tag->select('*', \DB::raw('count(*) as count'))->groupBy('name');
|
$query = $this->tag->select('*', DB::raw('count(*) as count'))->groupBy('name');
|
||||||
|
|
||||||
if ($searchTerm) {
|
if ($searchTerm) {
|
||||||
$query = $query->where('name', 'LIKE', $searchTerm . '%')->orderBy('name', 'desc');
|
$query = $query->where('name', 'LIKE', $searchTerm . '%')->orderBy('name', 'desc');
|
||||||
@ -82,13 +42,10 @@ class TagRepo
|
|||||||
* Get tag value suggestions from scanning existing tag values.
|
* Get tag value suggestions from scanning existing tag values.
|
||||||
* If no search is given the 50 most popular values are provided.
|
* If no search is given the 50 most popular values are provided.
|
||||||
* Passing a tagName will only find values for a tags with a particular name.
|
* Passing a tagName will only find values for a tags with a particular name.
|
||||||
* @param $searchTerm
|
|
||||||
* @param $tagName
|
|
||||||
* @return array
|
|
||||||
*/
|
*/
|
||||||
public function getValueSuggestions($searchTerm = false, $tagName = false)
|
public function getValueSuggestions(?string $searchTerm, ?string $tagName): Collection
|
||||||
{
|
{
|
||||||
$query = $this->tag->select('*', \DB::raw('count(*) as count'))->groupBy('value');
|
$query = $this->tag->select('*', DB::raw('count(*) as count'))->groupBy('value');
|
||||||
|
|
||||||
if ($searchTerm) {
|
if ($searchTerm) {
|
||||||
$query = $query->where('value', 'LIKE', $searchTerm . '%')->orderBy('value', 'desc');
|
$query = $query->where('value', 'LIKE', $searchTerm . '%')->orderBy('value', 'desc');
|
||||||
@ -96,7 +53,7 @@ class TagRepo
|
|||||||
$query = $query->orderBy('count', 'desc')->take(50);
|
$query = $query->orderBy('count', 'desc')->take(50);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($tagName !== false) {
|
if ($tagName) {
|
||||||
$query = $query->where('name', '=', $tagName);
|
$query = $query->where('name', '=', $tagName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,35 +63,28 @@ class TagRepo
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Save an array of tags to an entity
|
* Save an array of tags to an entity
|
||||||
* @param \BookStack\Entities\Entity $entity
|
|
||||||
* @param array $tags
|
|
||||||
* @return array|\Illuminate\Database\Eloquent\Collection
|
|
||||||
*/
|
*/
|
||||||
public function saveTagsToEntity(Entity $entity, $tags = [])
|
public function saveTagsToEntity(Entity $entity, array $tags = []): iterable
|
||||||
{
|
{
|
||||||
$entity->tags()->delete();
|
$entity->tags()->delete();
|
||||||
$newTags = [];
|
|
||||||
foreach ($tags as $tag) {
|
$newTags = collect($tags)->filter(function ($tag) {
|
||||||
if (trim($tag['name']) === '') {
|
return boolval(trim($tag['name']));
|
||||||
continue;
|
})->map(function ($tag) {
|
||||||
}
|
return $this->newInstanceFromInput($tag);
|
||||||
$newTags[] = $this->newInstanceFromInput($tag);
|
})->all();
|
||||||
}
|
|
||||||
|
|
||||||
return $entity->tags()->saveMany($newTags);
|
return $entity->tags()->saveMany($newTags);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new Tag instance from user input.
|
* Create a new Tag instance from user input.
|
||||||
* @param $input
|
* Input must be an array with a 'name' and an optional 'value' key.
|
||||||
* @return \BookStack\Actions\Tag
|
|
||||||
*/
|
*/
|
||||||
protected function newInstanceFromInput($input)
|
protected function newInstanceFromInput(array $input): Tag
|
||||||
{
|
{
|
||||||
$name = trim($input['name']);
|
$name = trim($input['name']);
|
||||||
$value = isset($input['value']) ? trim($input['value']) : '';
|
$value = isset($input['value']) ? trim($input['value']) : '';
|
||||||
// Any other modification or cleanup required can go here
|
return $this->tag->newInstance(['name' => $name, 'value' => $value]);
|
||||||
$values = ['name' => $name, 'value' => $value];
|
|
||||||
return $this->tag->newInstance($values);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
<?php namespace BookStack\Actions;
|
<?php namespace BookStack\Actions;
|
||||||
|
|
||||||
use BookStack\Auth\Permissions\PermissionService;
|
use BookStack\Auth\Permissions\PermissionService;
|
||||||
use BookStack\Entities\Book;
|
use BookStack\Entities\Models\Book;
|
||||||
use BookStack\Entities\Entity;
|
use BookStack\Entities\Models\Entity;
|
||||||
use BookStack\Entities\EntityProvider;
|
use BookStack\Entities\EntityProvider;
|
||||||
use DB;
|
use DB;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
@ -28,7 +28,7 @@ class ViewService
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a view to the given entity.
|
* Add a view to the given entity.
|
||||||
* @param \BookStack\Entities\Entity $entity
|
* @param \BookStack\Entities\Models\Entity $entity
|
||||||
* @return int
|
* @return int
|
||||||
*/
|
*/
|
||||||
public function add(Entity $entity)
|
public function add(Entity $entity)
|
||||||
@ -79,29 +79,26 @@ class ViewService
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all recently viewed entities for the current user.
|
* Get all recently viewed entities for the current user.
|
||||||
* @param int $count
|
|
||||||
* @param int $page
|
|
||||||
* @param Entity|bool $filterModel
|
|
||||||
* @return mixed
|
|
||||||
*/
|
*/
|
||||||
public function getUserRecentlyViewed($count = 10, $page = 0, $filterModel = false)
|
public function getUserRecentlyViewed(int $count = 10, int $page = 1)
|
||||||
{
|
{
|
||||||
$user = user();
|
$user = user();
|
||||||
if ($user === null || $user->isDefault()) {
|
if ($user === null || $user->isDefault()) {
|
||||||
return collect();
|
return collect();
|
||||||
}
|
}
|
||||||
|
|
||||||
$query = $this->permissionService
|
$all = collect();
|
||||||
->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type');
|
/** @var Entity $instance */
|
||||||
|
foreach ($this->entityProvider->all() as $name => $instance) {
|
||||||
if ($filterModel) {
|
$items = $instance::visible()->withLastView()
|
||||||
$query = $query->where('viewable_type', '=', $filterModel->getMorphClass());
|
->orderBy('last_viewed_at', 'desc')
|
||||||
|
->skip($count * ($page - 1))
|
||||||
|
->take($count)
|
||||||
|
->get();
|
||||||
|
$all = $all->concat($items);
|
||||||
}
|
}
|
||||||
$query = $query->where('user_id', '=', $user->id);
|
|
||||||
|
|
||||||
$viewables = $query->with('viewable')->orderBy('updated_at', 'desc')
|
return $all->sortByDesc('last_viewed_at')->slice(0, $count);
|
||||||
->skip($count * $page)->take($count)->get()->pluck('viewable');
|
|
||||||
return $viewables;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
<?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\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
use ReflectionClass;
|
use ReflectionClass;
|
||||||
use ReflectionException;
|
use ReflectionException;
|
||||||
use ReflectionMethod;
|
use ReflectionMethod;
|
||||||
@ -13,10 +16,27 @@ class ApiDocsGenerator
|
|||||||
protected $reflectionClasses = [];
|
protected $reflectionClasses = [];
|
||||||
protected $controllerClasses = [];
|
protected $controllerClasses = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the docs form the cache if existing
|
||||||
|
* otherwise generate and store in the cache.
|
||||||
|
*/
|
||||||
|
public static function generateConsideringCache(): Collection
|
||||||
|
{
|
||||||
|
$appVersion = trim(file_get_contents(base_path('version')));
|
||||||
|
$cacheKey = 'api-docs::' . $appVersion;
|
||||||
|
if (Cache::has($cacheKey) && config('app.env') === 'production') {
|
||||||
|
$docs = Cache::get($cacheKey);
|
||||||
|
} else {
|
||||||
|
$docs = (new static())->generate();
|
||||||
|
Cache::put($cacheKey, $docs, 60 * 24);
|
||||||
|
}
|
||||||
|
return $docs;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate API documentation.
|
* Generate API documentation.
|
||||||
*/
|
*/
|
||||||
public function generate(): Collection
|
protected function generate(): Collection
|
||||||
{
|
{
|
||||||
$apiRoutes = $this->getFlatApiRoutes();
|
$apiRoutes = $this->getFlatApiRoutes();
|
||||||
$apiRoutes = $this->loadDetailsFromControllers($apiRoutes);
|
$apiRoutes = $this->loadDetailsFromControllers($apiRoutes);
|
||||||
@ -57,7 +77,7 @@ class ApiDocsGenerator
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 \Illuminate\Contracts\Container\BindingResolutionException
|
* @throws BindingResolutionException
|
||||||
*/
|
*/
|
||||||
protected function getBodyParamsFromClass(string $className, string $methodName): ?array
|
protected function getBodyParamsFromClass(string $className, string $methodName): ?array
|
||||||
{
|
{
|
||||||
@ -117,6 +137,7 @@ class ApiDocsGenerator
|
|||||||
'method' => $route->methods[0],
|
'method' => $route->methods[0],
|
||||||
'controller' => $controller,
|
'controller' => $controller,
|
||||||
'controller_method' => $controllerMethod,
|
'controller_method' => $controllerMethod,
|
||||||
|
'controller_method_kebab' => Str::kebab($controllerMethod),
|
||||||
'base_model' => $baseModelName,
|
'base_model' => $baseModelName,
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
@ -1,11 +1,21 @@
|
|||||||
<?php namespace BookStack\Api;
|
<?php namespace BookStack\Api;
|
||||||
|
|
||||||
use BookStack\Auth\User;
|
use BookStack\Auth\User;
|
||||||
|
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\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
class ApiToken extends Model
|
/**
|
||||||
|
* Class ApiToken
|
||||||
|
* @property int $id
|
||||||
|
* @property string $token_id
|
||||||
|
* @property string $secret
|
||||||
|
* @property string $name
|
||||||
|
* @property Carbon $expires_at
|
||||||
|
* @property User $user
|
||||||
|
*/
|
||||||
|
class ApiToken extends Model implements Loggable
|
||||||
{
|
{
|
||||||
protected $fillable = ['name', 'expires_at'];
|
protected $fillable = ['name', 'expires_at'];
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
@ -28,4 +38,12 @@ class ApiToken extends Model
|
|||||||
{
|
{
|
||||||
return Carbon::now()->addYears(100)->format('Y-m-d');
|
return Carbon::now()->addYears(100)->format('Y-m-d');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
public function logDescriptor(): string
|
||||||
|
{
|
||||||
|
return "({$this->id}) {$this->name}; User: {$this->user->logDescriptor()}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,8 +36,10 @@ class ListingResponseBuilder
|
|||||||
*/
|
*/
|
||||||
public function toResponse()
|
public function toResponse()
|
||||||
{
|
{
|
||||||
$data = $this->fetchData();
|
$filteredQuery = $this->filterQuery($this->query);
|
||||||
$total = $this->query->count();
|
|
||||||
|
$total = $filteredQuery->count();
|
||||||
|
$data = $this->fetchData($filteredQuery);
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'data' => $data,
|
'data' => $data,
|
||||||
@ -48,23 +50,22 @@ class ListingResponseBuilder
|
|||||||
/**
|
/**
|
||||||
* Fetch the data to return in the response.
|
* Fetch the data to return in the response.
|
||||||
*/
|
*/
|
||||||
protected function fetchData(): Collection
|
protected function fetchData(Builder $query): Collection
|
||||||
{
|
{
|
||||||
$this->applyCountAndOffset($this->query);
|
$query = $this->countAndOffsetQuery($query);
|
||||||
$this->applySorting($this->query);
|
$query = $this->sortQuery($query);
|
||||||
$this->applyFiltering($this->query);
|
return $query->get($this->fields);
|
||||||
|
|
||||||
return $this->query->get($this->fields);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply any filtering operations found in the request.
|
* Apply any filtering operations found in the request.
|
||||||
*/
|
*/
|
||||||
protected function applyFiltering(Builder $query)
|
protected function filterQuery(Builder $query): Builder
|
||||||
{
|
{
|
||||||
|
$query = clone $query;
|
||||||
$requestFilters = $this->request->get('filter', []);
|
$requestFilters = $this->request->get('filter', []);
|
||||||
if (!is_array($requestFilters)) {
|
if (!is_array($requestFilters)) {
|
||||||
return;
|
return $query;
|
||||||
}
|
}
|
||||||
|
|
||||||
$queryFilters = collect($requestFilters)->map(function ($value, $key) {
|
$queryFilters = collect($requestFilters)->map(function ($value, $key) {
|
||||||
@ -73,7 +74,7 @@ class ListingResponseBuilder
|
|||||||
return !is_null($value);
|
return !is_null($value);
|
||||||
})->values()->toArray();
|
})->values()->toArray();
|
||||||
|
|
||||||
$query->where($queryFilters);
|
return $query->where($queryFilters);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -101,8 +102,9 @@ class ListingResponseBuilder
|
|||||||
* Apply sorting operations to the query from given parameters
|
* Apply sorting operations to the query from given parameters
|
||||||
* otherwise falling back to the first given field, ascending.
|
* otherwise falling back to the first given field, ascending.
|
||||||
*/
|
*/
|
||||||
protected function applySorting(Builder $query)
|
protected function sortQuery(Builder $query): Builder
|
||||||
{
|
{
|
||||||
|
$query = clone $query;
|
||||||
$defaultSortName = $this->fields[0];
|
$defaultSortName = $this->fields[0];
|
||||||
$direction = 'asc';
|
$direction = 'asc';
|
||||||
|
|
||||||
@ -116,20 +118,21 @@ class ListingResponseBuilder
|
|||||||
$sortName = $defaultSortName;
|
$sortName = $defaultSortName;
|
||||||
}
|
}
|
||||||
|
|
||||||
$query->orderBy($sortName, $direction);
|
return $query->orderBy($sortName, $direction);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply count and offset for paging, based on params from the request while falling
|
* Apply count and offset for paging, based on params from the request while falling
|
||||||
* back to system defined default, taking the max limit into account.
|
* back to system defined default, taking the max limit into account.
|
||||||
*/
|
*/
|
||||||
protected function applyCountAndOffset(Builder $query)
|
protected function countAndOffsetQuery(Builder $query): Builder
|
||||||
{
|
{
|
||||||
|
$query = clone $query;
|
||||||
$offset = max(0, $this->request->get('offset', 0));
|
$offset = max(0, $this->request->get('offset', 0));
|
||||||
$maxCount = config('api.max_item_count');
|
$maxCount = config('api.max_item_count');
|
||||||
$count = $this->request->get('count', config('api.default_item_count'));
|
$count = $this->request->get('count', config('api.default_item_count'));
|
||||||
$count = max(min($maxCount, $count), 1);
|
$count = max(min($maxCount, $count), 1);
|
||||||
|
|
||||||
$query->skip($offset)->take($count);
|
return $query->skip($offset)->take($count);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,8 @@
|
|||||||
use BookStack\Auth\Role;
|
use BookStack\Auth\Role;
|
||||||
use BookStack\Auth\User;
|
use BookStack\Auth\User;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
class ExternalAuthService
|
class ExternalAuthService
|
||||||
{
|
{
|
||||||
@ -39,22 +41,14 @@ class ExternalAuthService
|
|||||||
/**
|
/**
|
||||||
* Match an array of group names to BookStack system roles.
|
* Match an array of group names to BookStack system roles.
|
||||||
* Formats group names to be lower-case and hyphenated.
|
* Formats group names to be lower-case and hyphenated.
|
||||||
* @param array $groupNames
|
|
||||||
* @return \Illuminate\Support\Collection
|
|
||||||
*/
|
*/
|
||||||
protected function matchGroupsToSystemsRoles(array $groupNames)
|
protected function matchGroupsToSystemsRoles(array $groupNames): Collection
|
||||||
{
|
{
|
||||||
foreach ($groupNames as $i => $groupName) {
|
foreach ($groupNames as $i => $groupName) {
|
||||||
$groupNames[$i] = str_replace(' ', '-', trim(strtolower($groupName)));
|
$groupNames[$i] = str_replace(' ', '-', trim(strtolower($groupName)));
|
||||||
}
|
}
|
||||||
|
|
||||||
$roles = Role::query()->where(function (Builder $query) use ($groupNames) {
|
$roles = Role::query()->get(['id', 'external_auth_id', 'display_name']);
|
||||||
$query->whereIn('name', $groupNames);
|
|
||||||
foreach ($groupNames as $groupName) {
|
|
||||||
$query->orWhere('external_auth_id', 'LIKE', '%' . $groupName . '%');
|
|
||||||
}
|
|
||||||
})->get();
|
|
||||||
|
|
||||||
$matchedRoles = $roles->filter(function (Role $role) use ($groupNames) {
|
$matchedRoles = $roles->filter(function (Role $role) use ($groupNames) {
|
||||||
return $this->roleMatchesGroupNames($role, $groupNames);
|
return $this->roleMatchesGroupNames($role, $groupNames);
|
||||||
});
|
});
|
||||||
|
@ -15,8 +15,6 @@ use Illuminate\Contracts\Session\Session;
|
|||||||
* guard with 'remember' functionality removed. Basic auth and event emission
|
* guard with 'remember' functionality removed. Basic auth and event emission
|
||||||
* has also been removed to keep this simple. Designed to be extended by external
|
* has also been removed to keep this simple. Designed to be extended by external
|
||||||
* Auth Guards.
|
* Auth Guards.
|
||||||
*
|
|
||||||
* @package Illuminate\Auth
|
|
||||||
*/
|
*/
|
||||||
class ExternalBaseSessionGuard implements StatefulGuard
|
class ExternalBaseSessionGuard implements StatefulGuard
|
||||||
{
|
{
|
||||||
|
@ -60,10 +60,8 @@ class LdapSessionGuard extends ExternalBaseSessionGuard
|
|||||||
* @param array $credentials
|
* @param array $credentials
|
||||||
* @param bool $remember
|
* @param bool $remember
|
||||||
* @return bool
|
* @return bool
|
||||||
* @throws LoginAttemptEmailNeededException
|
|
||||||
* @throws LoginAttemptException
|
* @throws LoginAttemptException
|
||||||
* @throws LdapException
|
* @throws LdapException
|
||||||
* @throws UserRegistrationException
|
|
||||||
*/
|
*/
|
||||||
public function attempt(array $credentials = [], $remember = false)
|
public function attempt(array $credentials = [], $remember = false)
|
||||||
{
|
{
|
||||||
@ -82,7 +80,11 @@ class LdapSessionGuard extends ExternalBaseSessionGuard
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (is_null($user)) {
|
if (is_null($user)) {
|
||||||
|
try {
|
||||||
$user = $this->createNewFromLdapAndCreds($userDetails, $credentials);
|
$user = $this->createNewFromLdapAndCreds($userDetails, $credentials);
|
||||||
|
} catch (UserRegistrationException $exception) {
|
||||||
|
throw new LoginAttemptException($exception->message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync LDAP groups if required
|
// Sync LDAP groups if required
|
||||||
|
@ -9,8 +9,6 @@ namespace BookStack\Auth\Access\Guards;
|
|||||||
* 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
|
||||||
* via the Saml2 controller & Saml2Service. This class provides a safer, thin
|
* via the Saml2 controller & Saml2Service. This class provides a safer, thin
|
||||||
* version of SessionGuard.
|
* version of SessionGuard.
|
||||||
*
|
|
||||||
* @package BookStack\Auth\Access\Guards
|
|
||||||
*/
|
*/
|
||||||
class Saml2SessionGuard extends ExternalBaseSessionGuard
|
class Saml2SessionGuard extends ExternalBaseSessionGuard
|
||||||
{
|
{
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
* Class Ldap
|
* Class Ldap
|
||||||
* An object-orientated thin abstraction wrapper for common PHP LDAP functions.
|
* An object-orientated thin abstraction wrapper for common PHP LDAP functions.
|
||||||
* Allows the standard LDAP functions to be mocked for testing.
|
* Allows the standard LDAP functions to be mocked for testing.
|
||||||
* @package BookStack\Services
|
|
||||||
*/
|
*/
|
||||||
class Ldap
|
class Ldap
|
||||||
{
|
{
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
<?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\Auth\UserRepo;
|
use BookStack\Auth\UserRepo;
|
||||||
use BookStack\Exceptions\UserRegistrationException;
|
use BookStack\Exceptions\UserRegistrationException;
|
||||||
|
use BookStack\Facades\Activity;
|
||||||
use Exception;
|
use Exception;
|
||||||
|
|
||||||
class RegistrationService
|
class RegistrationService
|
||||||
@ -57,7 +59,7 @@ class RegistrationService
|
|||||||
// Ensure user does not already exist
|
// Ensure user does not already exist
|
||||||
$alreadyUser = !is_null($this->userRepo->getByEmail($userEmail));
|
$alreadyUser = !is_null($this->userRepo->getByEmail($userEmail));
|
||||||
if ($alreadyUser) {
|
if ($alreadyUser) {
|
||||||
throw new UserRegistrationException(trans('errors.error_user_exists_different_creds', ['email' => $userEmail]));
|
throw new UserRegistrationException(trans('errors.error_user_exists_different_creds', ['email' => $userEmail]), '/login');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the user
|
// Create the user
|
||||||
@ -68,18 +70,20 @@ class RegistrationService
|
|||||||
$newUser->socialAccounts()->save($socialAccount);
|
$newUser->socialAccounts()->save($socialAccount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Activity::add(ActivityType::AUTH_REGISTER, $socialAccount ?? $newUser);
|
||||||
|
|
||||||
// Start email confirmation flow if required
|
// Start email confirmation flow if required
|
||||||
if ($this->emailConfirmationService->confirmationRequired() && !$emailConfirmed) {
|
if ($this->emailConfirmationService->confirmationRequired() && !$emailConfirmed) {
|
||||||
$newUser->save();
|
$newUser->save();
|
||||||
$message = '';
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$this->emailConfirmationService->sendConfirmation($newUser);
|
$this->emailConfirmationService->sendConfirmation($newUser);
|
||||||
|
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');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $newUser;
|
return $newUser;
|
||||||
@ -106,13 +110,4 @@ class RegistrationService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Alias to the UserRepo method of the same name.
|
|
||||||
* Attaches the default system role, if configured, to the given user.
|
|
||||||
*/
|
|
||||||
public function attachDefaultRole(User $user): void
|
|
||||||
{
|
|
||||||
$this->userRepo->attachDefaultRole($user);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
@ -1,9 +1,11 @@
|
|||||||
<?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\UserRegistrationException;
|
use BookStack\Exceptions\UserRegistrationException;
|
||||||
|
use BookStack\Facades\Activity;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use OneLogin\Saml2\Auth;
|
use OneLogin\Saml2\Auth;
|
||||||
@ -311,7 +313,6 @@ 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 SamlException
|
|
||||||
* @throws UserRegistrationException
|
* @throws UserRegistrationException
|
||||||
*/
|
*/
|
||||||
protected function getOrRegisterUser(array $userDetails): ?User
|
protected function getOrRegisterUser(array $userDetails): ?User
|
||||||
@ -373,6 +374,7 @@ class Saml2Service extends ExternalAuthService
|
|||||||
}
|
}
|
||||||
|
|
||||||
auth()->login($user);
|
auth()->login($user);
|
||||||
|
Activity::add(ActivityType::AUTH_LOGIN, "saml2; {$user->logDescriptor()}");
|
||||||
return $user;
|
return $user;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +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\UserRepo;
|
use BookStack\Auth\UserRepo;
|
||||||
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 Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Laravel\Socialite\Contracts\Factory as Socialite;
|
use Laravel\Socialite\Contracts\Factory as Socialite;
|
||||||
use Laravel\Socialite\Contracts\Provider;
|
use Laravel\Socialite\Contracts\Provider;
|
||||||
@ -98,6 +100,7 @@ class SocialAuthService
|
|||||||
// 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);
|
auth()->login($socialAccount->user);
|
||||||
|
Activity::add(ActivityType::AUTH_LOGIN, $socialAccount);
|
||||||
return redirect()->intended('/');
|
return redirect()->intended('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,27 +1,28 @@
|
|||||||
<?php namespace BookStack\Auth\Permissions;
|
<?php namespace BookStack\Auth\Permissions;
|
||||||
|
|
||||||
use BookStack\Auth\Role;
|
use BookStack\Auth\Role;
|
||||||
use BookStack\Entities\Entity;
|
use BookStack\Entities\Models\Entity;
|
||||||
use BookStack\Model;
|
use BookStack\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\MorphOne;
|
||||||
|
|
||||||
class JointPermission extends Model
|
class JointPermission extends Model
|
||||||
{
|
{
|
||||||
|
protected $primaryKey = null;
|
||||||
public $timestamps = false;
|
public $timestamps = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the role that this points to.
|
* Get the role that this points to.
|
||||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
|
||||||
*/
|
*/
|
||||||
public function role()
|
public function role(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Role::class);
|
return $this->belongsTo(Role::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the entity this points to.
|
* Get the entity this points to.
|
||||||
* @return \Illuminate\Database\Eloquent\Relations\MorphOne
|
|
||||||
*/
|
*/
|
||||||
public function entity()
|
public function entity(): MorphOne
|
||||||
{
|
{
|
||||||
return $this->morphOne(Entity::class, 'entity');
|
return $this->morphOne(Entity::class, 'entity');
|
||||||
}
|
}
|
||||||
|
@ -2,12 +2,9 @@
|
|||||||
|
|
||||||
use BookStack\Auth\Permissions;
|
use BookStack\Auth\Permissions;
|
||||||
use BookStack\Auth\Role;
|
use BookStack\Auth\Role;
|
||||||
use BookStack\Entities\Book;
|
use BookStack\Entities\Models\Book;
|
||||||
use BookStack\Entities\Bookshelf;
|
use BookStack\Entities\Models\Entity;
|
||||||
use BookStack\Entities\Chapter;
|
|
||||||
use BookStack\Entities\Entity;
|
|
||||||
use BookStack\Entities\EntityProvider;
|
use BookStack\Entities\EntityProvider;
|
||||||
use BookStack\Entities\Page;
|
|
||||||
use BookStack\Ownable;
|
use BookStack\Ownable;
|
||||||
use Illuminate\Database\Connection;
|
use Illuminate\Database\Connection;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
@ -51,11 +48,6 @@ class PermissionService
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* PermissionService constructor.
|
* PermissionService constructor.
|
||||||
* @param JointPermission $jointPermission
|
|
||||||
* @param EntityPermission $entityPermission
|
|
||||||
* @param Role $role
|
|
||||||
* @param Connection $db
|
|
||||||
* @param EntityProvider $entityProvider
|
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
JointPermission $jointPermission,
|
JointPermission $jointPermission,
|
||||||
@ -82,7 +74,7 @@ class PermissionService
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Prepare the local entity cache and ensure it's empty
|
* Prepare the local entity cache and ensure it's empty
|
||||||
* @param \BookStack\Entities\Entity[] $entities
|
* @param \BookStack\Entities\Models\Entity[] $entities
|
||||||
*/
|
*/
|
||||||
protected function readyEntityCache($entities = [])
|
protected function readyEntityCache($entities = [])
|
||||||
{
|
{
|
||||||
@ -119,7 +111,7 @@ class PermissionService
|
|||||||
/**
|
/**
|
||||||
* Get a chapter via ID, Checks local cache
|
* Get a chapter via ID, Checks local cache
|
||||||
* @param $chapterId
|
* @param $chapterId
|
||||||
* @return \BookStack\Entities\Book
|
* @return \BookStack\Entities\Models\Book
|
||||||
*/
|
*/
|
||||||
protected function getChapter($chapterId)
|
protected function getChapter($chapterId)
|
||||||
{
|
{
|
||||||
@ -176,7 +168,7 @@ class PermissionService
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Chunk through all bookshelves
|
// Chunk through all bookshelves
|
||||||
$this->entityProvider->bookshelf->newQuery()->select(['id', 'restricted', 'created_by'])
|
$this->entityProvider->bookshelf->newQuery()->withTrashed()->select(['id', 'restricted', 'created_by'])
|
||||||
->chunk(50, function ($shelves) use ($roles) {
|
->chunk(50, function ($shelves) use ($roles) {
|
||||||
$this->buildJointPermissionsForShelves($shelves, $roles);
|
$this->buildJointPermissionsForShelves($shelves, $roles);
|
||||||
});
|
});
|
||||||
@ -188,11 +180,11 @@ class PermissionService
|
|||||||
*/
|
*/
|
||||||
protected function bookFetchQuery()
|
protected function bookFetchQuery()
|
||||||
{
|
{
|
||||||
return $this->entityProvider->book->newQuery()
|
return $this->entityProvider->book->withTrashed()->newQuery()
|
||||||
->select(['id', 'restricted', 'created_by'])->with(['chapters' => function ($query) {
|
->select(['id', 'restricted', 'created_by'])->with(['chapters' => function ($query) {
|
||||||
$query->select(['id', 'restricted', 'created_by', 'book_id']);
|
$query->withTrashed()->select(['id', 'restricted', 'created_by', 'book_id']);
|
||||||
}, 'pages' => function ($query) {
|
}, 'pages' => function ($query) {
|
||||||
$query->select(['id', 'restricted', 'created_by', 'book_id', 'chapter_id']);
|
$query->withTrashed()->select(['id', 'restricted', 'created_by', 'book_id', 'chapter_id']);
|
||||||
}]);
|
}]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -238,7 +230,7 @@ class PermissionService
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Rebuild the entity jointPermissions for a particular entity.
|
* Rebuild the entity jointPermissions for a particular entity.
|
||||||
* @param \BookStack\Entities\Entity $entity
|
* @param \BookStack\Entities\Models\Entity $entity
|
||||||
* @throws \Throwable
|
* @throws \Throwable
|
||||||
*/
|
*/
|
||||||
public function buildJointPermissionsForEntity(Entity $entity)
|
public function buildJointPermissionsForEntity(Entity $entity)
|
||||||
@ -333,7 +325,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 \BookStack\Entities\Entity[] $entities
|
* @param \BookStack\Entities\Models\Entity[] $entities
|
||||||
* @throws \Throwable
|
* @throws \Throwable
|
||||||
*/
|
*/
|
||||||
protected function deleteManyJointPermissionsForEntities($entities)
|
protected function deleteManyJointPermissionsForEntities($entities)
|
||||||
@ -414,7 +406,7 @@ class PermissionService
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the actions related to an entity.
|
* Get the actions related to an entity.
|
||||||
* @param \BookStack\Entities\Entity $entity
|
* @param \BookStack\Entities\Models\Entity $entity
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
protected function getActions(Entity $entity)
|
protected function getActions(Entity $entity)
|
||||||
@ -500,7 +492,7 @@ class PermissionService
|
|||||||
/**
|
/**
|
||||||
* Create an array of data with the information of an entity jointPermissions.
|
* Create an array of data with the information of an entity jointPermissions.
|
||||||
* Used to build data for bulk insertion.
|
* Used to build data for bulk insertion.
|
||||||
* @param \BookStack\Entities\Entity $entity
|
* @param \BookStack\Entities\Models\Entity $entity
|
||||||
* @param Role $role
|
* @param Role $role
|
||||||
* @param $action
|
* @param $action
|
||||||
* @param $permissionAll
|
* @param $permissionAll
|
||||||
@ -591,7 +583,7 @@ class PermissionService
|
|||||||
/**
|
/**
|
||||||
* Check if an entity has restrictions set on itself or its
|
* Check if an entity has restrictions set on itself or its
|
||||||
* parent tree.
|
* parent tree.
|
||||||
* @param \BookStack\Entities\Entity $entity
|
* @param \BookStack\Entities\Models\Entity $entity
|
||||||
* @param $action
|
* @param $action
|
||||||
* @return bool|mixed
|
* @return bool|mixed
|
||||||
*/
|
*/
|
||||||
@ -672,7 +664,7 @@ class PermissionService
|
|||||||
/**
|
/**
|
||||||
* Add restrictions for a generic entity
|
* Add restrictions for a generic entity
|
||||||
* @param string $entityType
|
* @param string $entityType
|
||||||
* @param Builder|\BookStack\Entities\Entity $query
|
* @param Builder|\BookStack\Entities\Models\Entity $query
|
||||||
* @param string $action
|
* @param string $action
|
||||||
* @return Builder
|
* @return Builder
|
||||||
*/
|
*/
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
<?php namespace BookStack\Auth\Permissions;
|
<?php namespace BookStack\Auth\Permissions;
|
||||||
|
|
||||||
use BookStack\Auth\Permissions;
|
use BookStack\Actions\ActivityType;
|
||||||
use BookStack\Auth\Role;
|
use BookStack\Auth\Role;
|
||||||
use BookStack\Exceptions\PermissionsException;
|
use BookStack\Exceptions\PermissionsException;
|
||||||
use Illuminate\Support\Str;
|
use BookStack\Facades\Activity;
|
||||||
|
use Exception;
|
||||||
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
|
|
||||||
class PermissionsRepo
|
class PermissionsRepo
|
||||||
{
|
{
|
||||||
@ -16,11 +18,8 @@ class PermissionsRepo
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* PermissionsRepo constructor.
|
* PermissionsRepo constructor.
|
||||||
* @param RolePermission $permission
|
|
||||||
* @param Role $role
|
|
||||||
* @param \BookStack\Auth\Permissions\PermissionService $permissionService
|
|
||||||
*/
|
*/
|
||||||
public function __construct(RolePermission $permission, Role $role, Permissions\PermissionService $permissionService)
|
public function __construct(RolePermission $permission, Role $role, PermissionService $permissionService)
|
||||||
{
|
{
|
||||||
$this->permission = $permission;
|
$this->permission = $permission;
|
||||||
$this->role = $role;
|
$this->role = $role;
|
||||||
@ -29,64 +28,51 @@ class PermissionsRepo
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all the user roles from the system.
|
* Get all the user roles from the system.
|
||||||
* @return \Illuminate\Database\Eloquent\Collection|static[]
|
|
||||||
*/
|
*/
|
||||||
public function getAllRoles()
|
public function getAllRoles(): Collection
|
||||||
{
|
{
|
||||||
return $this->role->all();
|
return $this->role->all();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all the roles except for the provided one.
|
* Get all the roles except for the provided one.
|
||||||
* @param Role $role
|
|
||||||
* @return mixed
|
|
||||||
*/
|
*/
|
||||||
public function getAllRolesExcept(Role $role)
|
public function getAllRolesExcept(Role $role): Collection
|
||||||
{
|
{
|
||||||
return $this->role->where('id', '!=', $role->id)->get();
|
return $this->role->where('id', '!=', $role->id)->get();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a role via its ID.
|
* Get a role via its ID.
|
||||||
* @param $id
|
|
||||||
* @return mixed
|
|
||||||
*/
|
*/
|
||||||
public function getRoleById($id)
|
public function getRoleById($id): Role
|
||||||
{
|
{
|
||||||
return $this->role->findOrFail($id);
|
return $this->role->newQuery()->findOrFail($id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save a new role into the system.
|
* Save a new role into the system.
|
||||||
* @param array $roleData
|
|
||||||
* @return Role
|
|
||||||
*/
|
*/
|
||||||
public function saveNewRole($roleData)
|
public function saveNewRole(array $roleData): Role
|
||||||
{
|
{
|
||||||
$role = $this->role->newInstance($roleData);
|
$role = $this->role->newInstance($roleData);
|
||||||
$role->name = str_replace(' ', '-', strtolower($roleData['display_name']));
|
|
||||||
// Prevent duplicate names
|
|
||||||
while ($this->role->where('name', '=', $role->name)->count() > 0) {
|
|
||||||
$role->name .= strtolower(Str::random(2));
|
|
||||||
}
|
|
||||||
$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);
|
||||||
return $role;
|
return $role;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates an existing role.
|
* Updates an existing role.
|
||||||
* Ensure Admin role always have core permissions.
|
* Ensure Admin role always have core permissions.
|
||||||
* @param $roleId
|
|
||||||
* @param $roleData
|
|
||||||
* @throws PermissionsException
|
|
||||||
*/
|
*/
|
||||||
public function updateRole($roleId, $roleData)
|
public function updateRole($roleId, array $roleData)
|
||||||
{
|
{
|
||||||
$role = $this->role->findOrFail($roleId);
|
/** @var Role $role */
|
||||||
|
$role = $this->role->newQuery()->findOrFail($roleId);
|
||||||
|
|
||||||
$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
|
$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
|
||||||
if ($role->system_name === 'admin') {
|
if ($role->system_name === 'admin') {
|
||||||
@ -104,20 +90,24 @@ class PermissionsRepo
|
|||||||
$role->fill($roleData);
|
$role->fill($roleData);
|
||||||
$role->save();
|
$role->save();
|
||||||
$this->permissionService->buildJointPermissionForRole($role);
|
$this->permissionService->buildJointPermissionForRole($role);
|
||||||
|
Activity::add(ActivityType::ROLE_UPDATE, $role);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assign an list of permission names to an role.
|
* Assign an list of permission names to an role.
|
||||||
* @param Role $role
|
|
||||||
* @param array $permissionNameArray
|
|
||||||
*/
|
*/
|
||||||
public function assignRolePermissions(Role $role, $permissionNameArray = [])
|
protected function assignRolePermissions(Role $role, array $permissionNameArray = [])
|
||||||
{
|
{
|
||||||
$permissions = [];
|
$permissions = [];
|
||||||
$permissionNameArray = array_values($permissionNameArray);
|
$permissionNameArray = array_values($permissionNameArray);
|
||||||
if ($permissionNameArray && count($permissionNameArray) > 0) {
|
|
||||||
$permissions = $this->permission->whereIn('name', $permissionNameArray)->pluck('id')->toArray();
|
if ($permissionNameArray) {
|
||||||
|
$permissions = $this->permission->newQuery()
|
||||||
|
->whereIn('name', $permissionNameArray)
|
||||||
|
->pluck('id')
|
||||||
|
->toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
$role->permissions()->sync($permissions);
|
$role->permissions()->sync($permissions);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,13 +116,13 @@ 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.
|
||||||
* @param $roleId
|
|
||||||
* @param $migrateRoleId
|
|
||||||
* @throws PermissionsException
|
* @throws PermissionsException
|
||||||
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
public function deleteRole($roleId, $migrateRoleId)
|
public function deleteRole($roleId, $migrateRoleId)
|
||||||
{
|
{
|
||||||
$role = $this->role->findOrFail($roleId);
|
/** @var Role $role */
|
||||||
|
$role = $this->role->newQuery()->findOrFail($roleId);
|
||||||
|
|
||||||
// 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)) {
|
||||||
@ -142,14 +132,15 @@ class PermissionsRepo
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($migrateRoleId) {
|
if ($migrateRoleId) {
|
||||||
$newRole = $this->role->find($migrateRoleId);
|
$newRole = $this->role->newQuery()->find($migrateRoleId);
|
||||||
if ($newRole) {
|
if ($newRole) {
|
||||||
$users = $role->users->pluck('id')->toArray();
|
$users = $role->users()->pluck('id')->toArray();
|
||||||
$newRole->users()->sync($users);
|
$newRole->users()->sync($users);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->permissionService->deleteJointPermissionsForRole($role);
|
$this->permissionService->deleteJointPermissionsForRole($role);
|
||||||
|
Activity::add(ActivityType::ROLE_DELETE, $role);
|
||||||
$role->delete();
|
$role->delete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,9 @@
|
|||||||
use BookStack\Auth\Role;
|
use BookStack\Auth\Role;
|
||||||
use BookStack\Model;
|
use BookStack\Model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property int $id
|
||||||
|
*/
|
||||||
class RolePermission extends Model
|
class RolePermission extends Model
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
@ -2,16 +2,20 @@
|
|||||||
|
|
||||||
use BookStack\Auth\Permissions\JointPermission;
|
use BookStack\Auth\Permissions\JointPermission;
|
||||||
use BookStack\Auth\Permissions\RolePermission;
|
use BookStack\Auth\Permissions\RolePermission;
|
||||||
|
use BookStack\Interfaces\Loggable;
|
||||||
use BookStack\Model;
|
use BookStack\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class Role
|
* Class Role
|
||||||
|
* @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
|
||||||
* @package BookStack\Auth
|
* @property string $system_name
|
||||||
*/
|
*/
|
||||||
class Role extends Model
|
class Role extends Model implements Loggable
|
||||||
{
|
{
|
||||||
|
|
||||||
protected $fillable = ['display_name', 'description', 'external_auth_id'];
|
protected $fillable = ['display_name', 'description', 'external_auth_id'];
|
||||||
@ -26,9 +30,8 @@ class Role extends Model
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all related JointPermissions.
|
* Get all related JointPermissions.
|
||||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
|
||||||
*/
|
*/
|
||||||
public function jointPermissions()
|
public function jointPermissions(): HasMany
|
||||||
{
|
{
|
||||||
return $this->hasMany(JointPermission::class);
|
return $this->hasMany(JointPermission::class);
|
||||||
}
|
}
|
||||||
@ -43,10 +46,8 @@ class Role extends Model
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if this role has a permission.
|
* Check if this role has a permission.
|
||||||
* @param $permissionName
|
|
||||||
* @return bool
|
|
||||||
*/
|
*/
|
||||||
public function hasPermission($permissionName)
|
public function hasPermission(string $permissionName): bool
|
||||||
{
|
{
|
||||||
$permissions = $this->getRelationValue('permissions');
|
$permissions = $this->getRelationValue('permissions');
|
||||||
foreach ($permissions as $permission) {
|
foreach ($permissions as $permission) {
|
||||||
@ -59,7 +60,6 @@ class Role extends Model
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a permission to this role.
|
* Add a permission to this role.
|
||||||
* @param RolePermission $permission
|
|
||||||
*/
|
*/
|
||||||
public function attachPermission(RolePermission $permission)
|
public function attachPermission(RolePermission $permission)
|
||||||
{
|
{
|
||||||
@ -68,7 +68,6 @@ class Role extends Model
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Detach a single permission from this role.
|
* Detach a single permission from this role.
|
||||||
* @param RolePermission $permission
|
|
||||||
*/
|
*/
|
||||||
public function detachPermission(RolePermission $permission)
|
public function detachPermission(RolePermission $permission)
|
||||||
{
|
{
|
||||||
@ -76,40 +75,42 @@ class Role extends Model
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the role object for the specified role.
|
* Get the role of the specified display name.
|
||||||
* @param $roleName
|
|
||||||
* @return Role
|
|
||||||
*/
|
*/
|
||||||
public static function getRole($roleName)
|
public static function getRole(string $displayName): ?Role
|
||||||
{
|
{
|
||||||
return static::query()->where('name', '=', $roleName)->first();
|
return static::query()->where('display_name', '=', $displayName)->first();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the role object for the specified system role.
|
* Get the role object for the specified system role.
|
||||||
* @param $roleName
|
|
||||||
* @return Role
|
|
||||||
*/
|
*/
|
||||||
public static function getSystemRole($roleName)
|
public static function getSystemRole(string $systemName): ?Role
|
||||||
{
|
{
|
||||||
return static::query()->where('system_name', '=', $roleName)->first();
|
return static::query()->where('system_name', '=', $systemName)->first();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all visible roles
|
* Get all visible roles
|
||||||
* @return mixed
|
|
||||||
*/
|
*/
|
||||||
public static function visible()
|
public static function visible(): Collection
|
||||||
{
|
{
|
||||||
return static::query()->where('hidden', '=', false)->orderBy('name')->get();
|
return static::query()->where('hidden', '=', false)->orderBy('name')->get();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the roles that can be restricted.
|
* Get the roles that can be restricted.
|
||||||
* @return \Illuminate\Database\Eloquent\Builder[]|\Illuminate\Database\Eloquent\Collection
|
|
||||||
*/
|
*/
|
||||||
public static function restrictable()
|
public static function restrictable(): Collection
|
||||||
{
|
{
|
||||||
return static::query()->where('system_name', '!=', 'admin')->get();
|
return static::query()->where('system_name', '!=', 'admin')->get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
public function logDescriptor(): string
|
||||||
|
{
|
||||||
|
return "({$this->id}) {$this->display_name}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,14 @@
|
|||||||
<?php namespace BookStack\Auth;
|
<?php namespace BookStack\Auth;
|
||||||
|
|
||||||
|
use BookStack\Interfaces\Loggable;
|
||||||
use BookStack\Model;
|
use BookStack\Model;
|
||||||
|
|
||||||
class SocialAccount extends Model
|
/**
|
||||||
|
* Class SocialAccount
|
||||||
|
* @property string $driver
|
||||||
|
* @property User $user
|
||||||
|
*/
|
||||||
|
class SocialAccount extends Model implements Loggable
|
||||||
{
|
{
|
||||||
|
|
||||||
protected $fillable = ['user_id', 'driver', 'driver_id', 'timestamps'];
|
protected $fillable = ['user_id', 'driver', 'driver_id', 'timestamps'];
|
||||||
@ -11,4 +17,12 @@ class SocialAccount extends Model
|
|||||||
{
|
{
|
||||||
return $this->belongsTo(User::class);
|
return $this->belongsTo(User::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
public function logDescriptor(): string
|
||||||
|
{
|
||||||
|
return "{$this->driver}; {$this->user->logDescriptor()}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
<?php namespace BookStack\Auth;
|
<?php namespace BookStack\Auth;
|
||||||
|
|
||||||
|
use BookStack\Actions\Activity;
|
||||||
use BookStack\Api\ApiToken;
|
use BookStack\Api\ApiToken;
|
||||||
|
use BookStack\Interfaces\Loggable;
|
||||||
use BookStack\Model;
|
use BookStack\Model;
|
||||||
use BookStack\Notifications\ResetPassword;
|
use BookStack\Notifications\ResetPassword;
|
||||||
use BookStack\Uploads\Image;
|
use BookStack\Uploads\Image;
|
||||||
@ -11,11 +13,11 @@ use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
|
|||||||
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
|
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class User
|
* Class User
|
||||||
* @package BookStack\Auth
|
|
||||||
* @property string $id
|
* @property string $id
|
||||||
* @property string $name
|
* @property string $name
|
||||||
* @property string $email
|
* @property string $email
|
||||||
@ -27,7 +29,7 @@ use Illuminate\Notifications\Notifiable;
|
|||||||
* @property string $external_auth_id
|
* @property string $external_auth_id
|
||||||
* @property string $system_name
|
* @property string $system_name
|
||||||
*/
|
*/
|
||||||
class User extends Model implements AuthenticatableContract, CanResetPasswordContract
|
class User extends Model implements AuthenticatableContract, CanResetPasswordContract, Loggable
|
||||||
{
|
{
|
||||||
use Authenticatable, CanResetPassword, Notifiable;
|
use Authenticatable, CanResetPassword, Notifiable;
|
||||||
|
|
||||||
@ -47,7 +49,10 @@ 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 = ['password', 'remember_token', 'system_name', 'email_confirmed', 'external_auth_id', 'email'];
|
protected $hidden = [
|
||||||
|
'password', 'remember_token', 'system_name', 'email_confirmed', 'external_auth_id', 'email',
|
||||||
|
'created_at', 'updated_at', 'image_id',
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This holds the user's permissions when loaded.
|
* This holds the user's permissions when loaded.
|
||||||
@ -98,12 +103,10 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the user has a role.
|
* Check if the user has a role.
|
||||||
* @param $role
|
|
||||||
* @return mixed
|
|
||||||
*/
|
*/
|
||||||
public function hasRole($role)
|
public function hasRole($roleId): bool
|
||||||
{
|
{
|
||||||
return $this->roles->pluck('name')->contains($role);
|
return $this->roles->pluck('id')->contains($roleId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -160,7 +163,6 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Attach a role to this user.
|
* Attach a role to this user.
|
||||||
* @param Role $role
|
|
||||||
*/
|
*/
|
||||||
public function attachRole(Role $role)
|
public function attachRole(Role $role)
|
||||||
{
|
{
|
||||||
@ -229,6 +231,14 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
|||||||
return $this->hasMany(ApiToken::class);
|
return $this->hasMany(ApiToken::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the latest activity instance for this user.
|
||||||
|
*/
|
||||||
|
public function latestActivity(): HasOne
|
||||||
|
{
|
||||||
|
return $this->hasOne(Activity::class)->latest();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the url for editing this user.
|
* Get the url for editing this user.
|
||||||
*/
|
*/
|
||||||
@ -274,4 +284,12 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
|||||||
{
|
{
|
||||||
$this->notify(new ResetPassword($token));
|
$this->notify(new ResetPassword($token));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
public function logDescriptor(): string
|
||||||
|
{
|
||||||
|
return "({$this->id}) {$this->name}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
<?php namespace BookStack\Auth;
|
<?php namespace BookStack\Auth;
|
||||||
|
|
||||||
use Activity;
|
use Activity;
|
||||||
use BookStack\Entities\Book;
|
use BookStack\Entities\Models\Book;
|
||||||
use BookStack\Entities\Bookshelf;
|
use BookStack\Entities\Models\Bookshelf;
|
||||||
use BookStack\Entities\Chapter;
|
use BookStack\Entities\Models\Chapter;
|
||||||
use BookStack\Entities\Page;
|
use BookStack\Entities\Models\Page;
|
||||||
use BookStack\Exceptions\NotFoundException;
|
use BookStack\Exceptions\NotFoundException;
|
||||||
use BookStack\Exceptions\UserUpdateException;
|
use BookStack\Exceptions\UserUpdateException;
|
||||||
use BookStack\Uploads\Image;
|
use BookStack\Uploads\Image;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Pagination\LengthAwarePaginator;
|
||||||
use Images;
|
use Images;
|
||||||
use Log;
|
use Log;
|
||||||
|
|
||||||
@ -56,13 +57,19 @@ class UserRepo
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all the users with their permissions in a paginated format.
|
* Get all the users with their permissions in a paginated format.
|
||||||
* @param int $count
|
|
||||||
* @param $sortData
|
|
||||||
* @return Builder|static
|
|
||||||
*/
|
*/
|
||||||
public function getAllUsersPaginatedAndSorted($count, $sortData)
|
public function getAllUsersPaginatedAndSorted(int $count, array $sortData): LengthAwarePaginator
|
||||||
{
|
{
|
||||||
$query = $this->user->with('roles', 'avatar')->orderBy($sortData['sort'], $sortData['order']);
|
$sort = $sortData['sort'];
|
||||||
|
if ($sort === 'latest_activity') {
|
||||||
|
$sort = \BookStack\Actions\Activity::query()->select('created_at')
|
||||||
|
->whereColumn('activities.user_id', 'users.id')
|
||||||
|
->latest()
|
||||||
|
->take(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = $this->user->with(['roles', 'avatar', 'latestActivity'])
|
||||||
|
->orderBy($sort, $sortData['order']);
|
||||||
|
|
||||||
if ($sortData['search']) {
|
if ($sortData['search']) {
|
||||||
$term = '%' . $sortData['search'] . '%';
|
$term = '%' . $sortData['search'] . '%';
|
||||||
@ -238,7 +245,7 @@ class UserRepo
|
|||||||
*/
|
*/
|
||||||
public function getAllRoles()
|
public function getAllRoles()
|
||||||
{
|
{
|
||||||
return $this->role->newQuery()->orderBy('name', 'asc')->get();
|
return $this->role->newQuery()->orderBy('display_name', 'asc')->get();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -31,6 +31,13 @@ return [
|
|||||||
// If set to false then a limit will not be enforced.
|
// If set to false then a limit will not be enforced.
|
||||||
'revision_limit' => env('REVISION_LIMIT', 50),
|
'revision_limit' => env('REVISION_LIMIT', 50),
|
||||||
|
|
||||||
|
// The number of days that content will remain in the recycle bin before
|
||||||
|
// being considered for auto-removal. It is not a guarantee that content will
|
||||||
|
// be removed after this time.
|
||||||
|
// Set to 0 for no recycle bin functionality.
|
||||||
|
// Set to -1 for unlimited recycle bin lifetime.
|
||||||
|
'recycle_bin_lifetime' => env('RECYCLE_BIN_LIFETIME', 30),
|
||||||
|
|
||||||
// Allow <script> tags to entered within page content.
|
// Allow <script> tags to entered within page content.
|
||||||
// <script> tags are escaped by default.
|
// <script> tags are escaped by default.
|
||||||
// Even when overridden the WYSIWYG editor may still escape script content.
|
// Even when overridden the WYSIWYG editor may still escape script content.
|
||||||
@ -52,7 +59,7 @@ return [
|
|||||||
'locale' => env('APP_LANG', 'en'),
|
'locale' => env('APP_LANG', 'en'),
|
||||||
|
|
||||||
// Locales available
|
// Locales available
|
||||||
'locales' => ['en', 'ar', 'cs', 'da', 'de', 'de_informal', 'es', 'es_AR', 'fa', 'fr', 'he', 'hu', 'it', 'ja', 'ko', 'nl', 'pt', 'pt_BR', 'sk', 'sl', 'sv', 'pl', 'ru', 'tr', 'uk', 'vi', 'zh_CN', 'zh_TW',],
|
'locales' => ['en', 'ar', 'bg', 'cs', 'da', 'de', 'de_informal', 'es', 'es_AR', 'fa', 'fr', 'he', 'hu', 'it', 'ja', 'ko', 'nl', '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',
|
||||||
@ -117,6 +124,7 @@ return [
|
|||||||
BookStack\Providers\EventServiceProvider::class,
|
BookStack\Providers\EventServiceProvider::class,
|
||||||
BookStack\Providers\RouteServiceProvider::class,
|
BookStack\Providers\RouteServiceProvider::class,
|
||||||
BookStack\Providers\CustomFacadeProvider::class,
|
BookStack\Providers\CustomFacadeProvider::class,
|
||||||
|
BookStack\Providers\CustomValidationServiceProvider::class,
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use Monolog\Formatter\LineFormatter;
|
||||||
|
use Monolog\Handler\ErrorLogHandler;
|
||||||
use Monolog\Handler\NullHandler;
|
use Monolog\Handler\NullHandler;
|
||||||
use Monolog\Handler\StreamHandler;
|
use Monolog\Handler\StreamHandler;
|
||||||
|
|
||||||
@ -73,10 +75,38 @@ return [
|
|||||||
'level' => 'debug',
|
'level' => 'debug',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
// Custom errorlog implementation that logs out a plain,
|
||||||
|
// non-formatted message intended for the webserver log.
|
||||||
|
'errorlog_plain_webserver' => [
|
||||||
|
'driver' => 'monolog',
|
||||||
|
'level' => 'debug',
|
||||||
|
'handler' => ErrorLogHandler::class,
|
||||||
|
'handler_with' => [4],
|
||||||
|
'formatter' => LineFormatter::class,
|
||||||
|
'formatter_with' => [
|
||||||
|
'format' => "%message%",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
'null' => [
|
'null' => [
|
||||||
'driver' => 'monolog',
|
'driver' => 'monolog',
|
||||||
'handler' => NullHandler::class,
|
'handler' => NullHandler::class,
|
||||||
],
|
],
|
||||||
|
|
||||||
|
// Testing channel
|
||||||
|
// Uses a shared testing instance during tests
|
||||||
|
// so that logs can be checked against.
|
||||||
|
'testing' => [
|
||||||
|
'driver' => 'testing',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
|
||||||
|
// Failed Login Message
|
||||||
|
// Allows a configurable message to be logged when a login request fails.
|
||||||
|
'failed_login' => [
|
||||||
|
'message' => env('LOG_FAILED_LOGIN_MESSAGE', null),
|
||||||
|
'channel' => env('LOG_FAILED_LOGIN_CHANNEL', 'errorlog_plain_webserver'),
|
||||||
],
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
|
@ -101,7 +101,7 @@ return [
|
|||||||
'url' => env('SAML2_IDP_SLO', null),
|
'url' => env('SAML2_IDP_SLO', null),
|
||||||
// URL location of the IdP where the SP will send the SLO Response (ResponseLocation)
|
// URL location of the IdP where the SP will send the SLO Response (ResponseLocation)
|
||||||
// if not set, url for the SLO Request will be used
|
// if not set, url for the SLO Request will be used
|
||||||
'responseUrl' => '',
|
'responseUrl' => null,
|
||||||
// SAML protocol binding to be used when returning the <Response>
|
// SAML protocol binding to be used when returning the <Response>
|
||||||
// message. Onelogin Toolkit supports for this endpoint the
|
// message. Onelogin Toolkit supports for this endpoint the
|
||||||
// HTTP-Redirect binding only
|
// HTTP-Redirect binding only
|
||||||
|
@ -13,7 +13,9 @@ return [
|
|||||||
'enabled' => true,
|
'enabled' => true,
|
||||||
'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
|
||||||
|
],
|
||||||
'env' => [],
|
'env' => [],
|
||||||
],
|
],
|
||||||
'image' => [
|
'image' => [
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace BookStack\Console\Commands;
|
namespace BookStack\Console\Commands;
|
||||||
|
|
||||||
use BookStack\Entities\PageRevision;
|
use BookStack\Entities\Models\PageRevision;
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
class ClearRevisions extends Command
|
class ClearRevisions extends Command
|
||||||
|
@ -18,7 +18,7 @@ class ClearViews extends Command
|
|||||||
*
|
*
|
||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
protected $description = 'Clear all view-counts for all entities.';
|
protected $description = 'Clear all view-counts for all entities';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new command instance.
|
* Create a new command instance.
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace BookStack\Console\Commands;
|
namespace BookStack\Console\Commands;
|
||||||
|
|
||||||
use BookStack\Entities\Bookshelf;
|
use BookStack\Entities\Models\Bookshelf;
|
||||||
use BookStack\Entities\Repos\BookshelfRepo;
|
use BookStack\Entities\Repos\BookshelfRepo;
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
@ -23,7 +23,7 @@ class CopyShelfPermissions extends Command
|
|||||||
*
|
*
|
||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
protected $description = 'Copy shelf permissions to all child books.';
|
protected $description = 'Copy shelf permissions to all child books';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var BookshelfRepo
|
* @var BookshelfRepo
|
||||||
|
@ -25,7 +25,7 @@ class DeleteUsers extends Command
|
|||||||
*
|
*
|
||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
protected $description = 'Delete users that are not "admin" or system users.';
|
protected $description = 'Delete users that are not "admin" or system users';
|
||||||
|
|
||||||
public function __construct(User $user, UserRepo $userRepo)
|
public function __construct(User $user, UserRepo $userRepo)
|
||||||
{
|
{
|
||||||
|
61
app/Console/Commands/RegenerateCommentContent.php
Normal file
61
app/Console/Commands/RegenerateCommentContent.php
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Console\Commands;
|
||||||
|
|
||||||
|
use BookStack\Actions\Comment;
|
||||||
|
use BookStack\Actions\CommentRepo;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class RegenerateCommentContent extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'bookstack:regenerate-comment-content {--database= : The database connection to use.}';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Regenerate the stored HTML of all comments';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var CommentRepo
|
||||||
|
*/
|
||||||
|
protected $commentRepo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*/
|
||||||
|
public function __construct(CommentRepo $commentRepo)
|
||||||
|
{
|
||||||
|
$this->commentRepo = $commentRepo;
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$connection = \DB::getDefaultConnection();
|
||||||
|
if ($this->option('database') !== null) {
|
||||||
|
\DB::setDefaultConnection($this->option('database'));
|
||||||
|
}
|
||||||
|
|
||||||
|
Comment::query()->chunk(100, function ($comments) {
|
||||||
|
foreach ($comments as $comment) {
|
||||||
|
$comment->html = $this->commentRepo->commentToHtml($comment->text);
|
||||||
|
$comment->save();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
\DB::setDefaultConnection($connection);
|
||||||
|
$this->comment('Comment HTML content has been regenerated');
|
||||||
|
}
|
||||||
|
}
|
@ -30,8 +30,6 @@ class RegeneratePermissions extends Command
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new command instance.
|
* Create a new command instance.
|
||||||
*
|
|
||||||
* @param \BookStack\Auth\\BookStack\Auth\Permissions\PermissionService $permissionService
|
|
||||||
*/
|
*/
|
||||||
public function __construct(PermissionService $permissionService)
|
public function __construct(PermissionService $permissionService)
|
||||||
{
|
{
|
||||||
|
@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
namespace BookStack\Console\Commands;
|
namespace BookStack\Console\Commands;
|
||||||
|
|
||||||
use BookStack\Entities\SearchService;
|
use BookStack\Entities\Tools\SearchIndex;
|
||||||
|
use DB;
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
class RegenerateSearch extends Command
|
class RegenerateSearch extends Command
|
||||||
@ -21,17 +22,15 @@ class RegenerateSearch extends Command
|
|||||||
*/
|
*/
|
||||||
protected $description = 'Re-index all content for searching';
|
protected $description = 'Re-index all content for searching';
|
||||||
|
|
||||||
protected $searchService;
|
protected $searchIndex;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new command instance.
|
* Create a new command instance.
|
||||||
*
|
|
||||||
* @param \BookStack\Entities\SearchService $searchService
|
|
||||||
*/
|
*/
|
||||||
public function __construct(SearchService $searchService)
|
public function __construct(SearchIndex $searchIndex)
|
||||||
{
|
{
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
$this->searchService = $searchService;
|
$this->searchIndex = $searchIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -41,14 +40,13 @@ class RegenerateSearch extends Command
|
|||||||
*/
|
*/
|
||||||
public function handle()
|
public function handle()
|
||||||
{
|
{
|
||||||
$connection = \DB::getDefaultConnection();
|
$connection = DB::getDefaultConnection();
|
||||||
if ($this->option('database') !== null) {
|
if ($this->option('database') !== null) {
|
||||||
\DB::setDefaultConnection($this->option('database'));
|
DB::setDefaultConnection($this->option('database'));
|
||||||
$this->searchService->setConnection(\DB::connection($this->option('database')));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->searchService->indexAllEntities();
|
$this->searchIndex->indexAllEntities();
|
||||||
\DB::setDefaultConnection($connection);
|
DB::setDefaultConnection($connection);
|
||||||
$this->comment('Search index regenerated');
|
$this->comment('Search index regenerated');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
91
app/Console/Commands/UpdateUrl.php
Normal file
91
app/Console/Commands/UpdateUrl.php
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Database\Connection;
|
||||||
|
|
||||||
|
class UpdateUrl extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'bookstack:update-url
|
||||||
|
{oldUrl : URL to replace}
|
||||||
|
{newUrl : URL to use as the replacement}';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Find and replace the given URLs in your BookStack database';
|
||||||
|
|
||||||
|
protected $db;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct(Connection $db)
|
||||||
|
{
|
||||||
|
$this->db = $db;
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$oldUrl = str_replace("'", '', $this->argument('oldUrl'));
|
||||||
|
$newUrl = str_replace("'", '', $this->argument('newUrl'));
|
||||||
|
|
||||||
|
$urlPattern = '/https?:\/\/(.+)/';
|
||||||
|
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://");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->checkUserOkayToProceed($oldUrl, $newUrl)) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$columnsToUpdateByTable = [
|
||||||
|
"attachments" => ["path"],
|
||||||
|
"pages" => ["html", "text", "markdown"],
|
||||||
|
"images" => ["url"],
|
||||||
|
"comments" => ["html", "text"],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($columnsToUpdateByTable as $table => $columns) {
|
||||||
|
foreach ($columns as $column) {
|
||||||
|
$changeCount = $this->db->table($table)->update([
|
||||||
|
$column => $this->db->raw("REPLACE({$column}, '{$oldUrl}', '{$newUrl}')")
|
||||||
|
]);
|
||||||
|
$this->info("Updated {$changeCount} rows in {$table}->{$column}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info("URL update procedure complete.");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Warn the user of the dangers of this operation.
|
||||||
|
* Returns a boolean indicating if they've accepted the warnings.
|
||||||
|
*/
|
||||||
|
protected function checkUserOkayToProceed(string $oldUrl, string $newUrl): bool
|
||||||
|
{
|
||||||
|
$dangerWarning = "This will search for \"{$oldUrl}\" in your database and replace it with \"{$newUrl}\".\n";
|
||||||
|
$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?";
|
||||||
|
|
||||||
|
return $this->confirm($dangerWarning) && $this->confirm($backupConfirmation);
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
<?php namespace BookStack\Entities;
|
<?php namespace BookStack\Entities;
|
||||||
|
|
||||||
use BookStack\Entities\Managers\EntityContext;
|
use BookStack\Entities\Models\Book;
|
||||||
|
use BookStack\Entities\Tools\ShelfContext;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
|
|
||||||
class BreadcrumbsViewComposer
|
class BreadcrumbsViewComposer
|
||||||
@ -10,9 +11,9 @@ class BreadcrumbsViewComposer
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* BreadcrumbsViewComposer constructor.
|
* BreadcrumbsViewComposer constructor.
|
||||||
* @param EntityContext $entityContextManager
|
* @param ShelfContext $entityContextManager
|
||||||
*/
|
*/
|
||||||
public function __construct(EntityContext $entityContextManager)
|
public function __construct(ShelfContext $entityContextManager)
|
||||||
{
|
{
|
||||||
$this->entityContextManager = $entityContextManager;
|
$this->entityContextManager = $entityContextManager;
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,18 @@
|
|||||||
<?php namespace BookStack\Entities;
|
<?php namespace BookStack\Entities;
|
||||||
|
|
||||||
|
use BookStack\Entities\Models\Book;
|
||||||
|
use BookStack\Entities\Models\Bookshelf;
|
||||||
|
use BookStack\Entities\Models\Chapter;
|
||||||
|
use BookStack\Entities\Models\Entity;
|
||||||
|
use BookStack\Entities\Models\Page;
|
||||||
|
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
|
||||||
* so this is a neater alternative to injecting all in individually.
|
* so this is a neater alternative to injecting all in individually.
|
||||||
*
|
|
||||||
* @package BookStack\Entities
|
|
||||||
*/
|
*/
|
||||||
class EntityProvider
|
class EntityProvider
|
||||||
{
|
{
|
||||||
@ -37,26 +42,20 @@ class EntityProvider
|
|||||||
*/
|
*/
|
||||||
public $pageRevision;
|
public $pageRevision;
|
||||||
|
|
||||||
/**
|
|
||||||
* EntityProvider constructor.
|
public function __construct()
|
||||||
*/
|
{
|
||||||
public function __construct(
|
$this->bookshelf = new Bookshelf();
|
||||||
Bookshelf $bookshelf,
|
$this->book = new Book();
|
||||||
Book $book,
|
$this->chapter = new Chapter();
|
||||||
Chapter $chapter,
|
$this->page = new Page();
|
||||||
Page $page,
|
$this->pageRevision = new PageRevision();
|
||||||
PageRevision $pageRevision
|
|
||||||
) {
|
|
||||||
$this->bookshelf = $bookshelf;
|
|
||||||
$this->book = $book;
|
|
||||||
$this->chapter = $chapter;
|
|
||||||
$this->page = $page;
|
|
||||||
$this->pageRevision = $pageRevision;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 [string => Entity]
|
||||||
*/
|
*/
|
||||||
public function all(): array
|
public function all(): array
|
||||||
{
|
{
|
||||||
|
@ -1,109 +0,0 @@
|
|||||||
<?php namespace BookStack\Entities\Managers;
|
|
||||||
|
|
||||||
use BookStack\Entities\Book;
|
|
||||||
use BookStack\Entities\Bookshelf;
|
|
||||||
use BookStack\Entities\Chapter;
|
|
||||||
use BookStack\Entities\Entity;
|
|
||||||
use BookStack\Entities\HasCoverImage;
|
|
||||||
use BookStack\Entities\Page;
|
|
||||||
use BookStack\Exceptions\NotifyException;
|
|
||||||
use BookStack\Facades\Activity;
|
|
||||||
use BookStack\Uploads\AttachmentService;
|
|
||||||
use BookStack\Uploads\ImageService;
|
|
||||||
use Exception;
|
|
||||||
use Illuminate\Contracts\Container\BindingResolutionException;
|
|
||||||
|
|
||||||
class TrashCan
|
|
||||||
{
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a bookshelf from the system.
|
|
||||||
* @throws Exception
|
|
||||||
*/
|
|
||||||
public function destroyShelf(Bookshelf $shelf)
|
|
||||||
{
|
|
||||||
$this->destroyCommonRelations($shelf);
|
|
||||||
$shelf->delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a book from the system.
|
|
||||||
* @throws NotifyException
|
|
||||||
* @throws BindingResolutionException
|
|
||||||
*/
|
|
||||||
public function destroyBook(Book $book)
|
|
||||||
{
|
|
||||||
foreach ($book->pages as $page) {
|
|
||||||
$this->destroyPage($page);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($book->chapters as $chapter) {
|
|
||||||
$this->destroyChapter($chapter);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->destroyCommonRelations($book);
|
|
||||||
$book->delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a page from the system.
|
|
||||||
* @throws NotifyException
|
|
||||||
*/
|
|
||||||
public function destroyPage(Page $page)
|
|
||||||
{
|
|
||||||
// Check if set as custom homepage & remove setting if not used or throw error if active
|
|
||||||
$customHome = setting('app-homepage', '0:');
|
|
||||||
if (intval($page->id) === intval(explode(':', $customHome)[0])) {
|
|
||||||
if (setting('app-homepage-type') === 'page') {
|
|
||||||
throw new NotifyException(trans('errors.page_custom_home_deletion'), $page->getUrl());
|
|
||||||
}
|
|
||||||
setting()->remove('app-homepage');
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->destroyCommonRelations($page);
|
|
||||||
|
|
||||||
// Delete Attached Files
|
|
||||||
$attachmentService = app(AttachmentService::class);
|
|
||||||
foreach ($page->attachments as $attachment) {
|
|
||||||
$attachmentService->deleteFile($attachment);
|
|
||||||
}
|
|
||||||
|
|
||||||
$page->delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a chapter from the system.
|
|
||||||
* @throws Exception
|
|
||||||
*/
|
|
||||||
public function destroyChapter(Chapter $chapter)
|
|
||||||
{
|
|
||||||
if (count($chapter->pages) > 0) {
|
|
||||||
foreach ($chapter->pages as $page) {
|
|
||||||
$page->chapter_id = 0;
|
|
||||||
$page->save();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->destroyCommonRelations($chapter);
|
|
||||||
$chapter->delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update entity relations to remove or update outstanding connections.
|
|
||||||
*/
|
|
||||||
protected function destroyCommonRelations(Entity $entity)
|
|
||||||
{
|
|
||||||
Activity::removeEntity($entity);
|
|
||||||
$entity->views()->delete();
|
|
||||||
$entity->permissions()->delete();
|
|
||||||
$entity->tags()->delete();
|
|
||||||
$entity->comments()->delete();
|
|
||||||
$entity->jointPermissions()->delete();
|
|
||||||
$entity->searchTerms()->delete();
|
|
||||||
|
|
||||||
if ($entity instanceof HasCoverImage && $entity->cover) {
|
|
||||||
$imageService = app()->make(ImageService::class);
|
|
||||||
$imageService->destroy($entity->cover);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,4 +1,4 @@
|
|||||||
<?php namespace BookStack\Entities;
|
<?php namespace BookStack\Entities\Models;
|
||||||
|
|
||||||
use BookStack\Uploads\Image;
|
use BookStack\Uploads\Image;
|
||||||
use Exception;
|
use Exception;
|
||||||
@ -12,26 +12,20 @@ use Illuminate\Support\Collection;
|
|||||||
* @property string $description
|
* @property string $description
|
||||||
* @property int $image_id
|
* @property int $image_id
|
||||||
* @property Image|null $cover
|
* @property Image|null $cover
|
||||||
* @package BookStack\Entities
|
|
||||||
*/
|
*/
|
||||||
class Book extends Entity implements HasCoverImage
|
class Book extends Entity implements HasCoverImage
|
||||||
{
|
{
|
||||||
public $searchFactor = 2;
|
public $searchFactor = 2;
|
||||||
|
|
||||||
protected $fillable = ['name', 'description'];
|
protected $fillable = ['name', 'description'];
|
||||||
protected $hidden = ['restricted'];
|
protected $hidden = ['restricted', 'pivot', 'image_id', 'deleted_at'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the url for this book.
|
* Get the url for this book.
|
||||||
* @param string|bool $path
|
|
||||||
* @return string
|
|
||||||
*/
|
*/
|
||||||
public function getUrl($path = false)
|
public function getUrl(string $path = ''): string
|
||||||
{
|
{
|
||||||
if ($path !== false) {
|
return url('/books/' . implode('/', [urlencode($this->slug), trim($path, '/')]));
|
||||||
return url('/books/' . urlencode($this->slug) . '/' . trim($path, '/'));
|
|
||||||
}
|
|
||||||
return url('/books/' . urlencode($this->slug));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -117,15 +111,4 @@ class Book extends Entity implements HasCoverImage
|
|||||||
$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');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get an excerpt of this book's description to the specified length or less.
|
|
||||||
* @param int $length
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function getExcerpt(int $length = 100)
|
|
||||||
{
|
|
||||||
$description = $this->description;
|
|
||||||
return mb_strlen($description) > $length ? mb_substr($description, 0, $length-3) . '...' : $description;
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -1,5 +1,8 @@
|
|||||||
<?php namespace BookStack\Entities;
|
<?php namespace BookStack\Entities\Models;
|
||||||
|
|
||||||
|
use BookStack\Entities\Models\Chapter;
|
||||||
|
use BookStack\Entities\Models\Entity;
|
||||||
|
use BookStack\Entities\Models\Book;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
@ -10,7 +13,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|||||||
* @property Book $book
|
* @property Book $book
|
||||||
* @method Builder whereSlugs(string $bookSlug, string $childSlug)
|
* @method Builder whereSlugs(string $bookSlug, string $childSlug)
|
||||||
*/
|
*/
|
||||||
class BookChild extends Entity
|
abstract class BookChild extends Entity
|
||||||
{
|
{
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -45,9 +48,6 @@ class BookChild extends Entity
|
|||||||
$this->save();
|
$this->save();
|
||||||
$this->refresh();
|
$this->refresh();
|
||||||
|
|
||||||
// Update related activity
|
|
||||||
$this->activity()->update(['book_id' => $newBookId]);
|
|
||||||
|
|
||||||
// Update all child pages if a chapter
|
// Update all child pages if a chapter
|
||||||
if ($this instanceof Chapter) {
|
if ($this instanceof Chapter) {
|
||||||
foreach ($this->pages as $page) {
|
foreach ($this->pages as $page) {
|
@ -1,4 +1,4 @@
|
|||||||
<?php namespace BookStack\Entities;
|
<?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;
|
||||||
@ -12,6 +12,8 @@ class Bookshelf extends Entity implements HasCoverImage
|
|||||||
|
|
||||||
protected $fillable = ['name', 'description', 'image_id'];
|
protected $fillable = ['name', 'description', 'image_id'];
|
||||||
|
|
||||||
|
protected $hidden = ['restricted', 'image_id', 'deleted_at'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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.
|
||||||
@ -34,15 +36,10 @@ class Bookshelf extends Entity implements HasCoverImage
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the url for this bookshelf.
|
* Get the url for this bookshelf.
|
||||||
* @param string|bool $path
|
|
||||||
* @return string
|
|
||||||
*/
|
*/
|
||||||
public function getUrl($path = false)
|
public function getUrl(string $path = ''): string
|
||||||
{
|
{
|
||||||
if ($path !== false) {
|
return url('/shelves/' . implode('/', [urlencode($this->slug), trim($path, '/')]));
|
||||||
return url('/shelves/' . urlencode($this->slug) . '/' . trim($path, '/'));
|
|
||||||
}
|
|
||||||
return url('/shelves/' . urlencode($this->slug));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -83,17 +80,6 @@ class Bookshelf extends Entity implements HasCoverImage
|
|||||||
return 'cover_shelf';
|
return 'cover_shelf';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get an excerpt of this book's description to the specified length or less.
|
|
||||||
* @param int $length
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function getExcerpt(int $length = 100)
|
|
||||||
{
|
|
||||||
$description = $this->description;
|
|
||||||
return mb_strlen($description) > $length ? mb_substr($description, 0, $length-3) . '...' : $description;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if this shelf contains the given book.
|
* Check if this shelf contains the given book.
|
||||||
* @param Book $book
|
* @param Book $book
|
@ -1,17 +1,17 @@
|
|||||||
<?php namespace BookStack\Entities;
|
<?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
|
||||||
* @package BookStack\Entities
|
|
||||||
*/
|
*/
|
||||||
class Chapter extends BookChild
|
class Chapter extends BookChild
|
||||||
{
|
{
|
||||||
public $searchFactor = 1.3;
|
public $searchFactor = 1.3;
|
||||||
|
|
||||||
protected $fillable = ['name', 'description', 'priority', 'book_id'];
|
protected $fillable = ['name', 'description', 'priority', 'book_id'];
|
||||||
|
protected $hidden = ['restricted', 'pivot', 'deleted_at'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the pages that this chapter contains.
|
* Get the pages that this chapter contains.
|
||||||
@ -25,30 +25,18 @@ class Chapter extends BookChild
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the url of this chapter.
|
* Get the url of this chapter.
|
||||||
* @param string|bool $path
|
|
||||||
* @return string
|
|
||||||
*/
|
*/
|
||||||
public function getUrl($path = false)
|
public function getUrl($path = ''): string
|
||||||
{
|
{
|
||||||
$bookSlug = $this->getAttribute('bookSlug') ? $this->getAttribute('bookSlug') : $this->book->slug;
|
$parts = [
|
||||||
$fullPath = '/books/' . urlencode($bookSlug) . '/chapter/' . urlencode($this->slug);
|
'books',
|
||||||
|
urlencode($this->getAttribute('bookSlug') ?? $this->book->slug),
|
||||||
|
'chapter',
|
||||||
|
urlencode($this->slug),
|
||||||
|
trim($path, '/'),
|
||||||
|
];
|
||||||
|
|
||||||
if ($path !== false) {
|
return url('/' . implode('/', $parts));
|
||||||
$fullPath .= '/' . trim($path, '/');
|
|
||||||
}
|
|
||||||
|
|
||||||
return url($fullPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get an excerpt of this chapter's description to the specified length or less.
|
|
||||||
* @param int $length
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function getExcerpt(int $length = 100)
|
|
||||||
{
|
|
||||||
$description = $this->text ?? $this->description;
|
|
||||||
return mb_strlen($description) > $length ? mb_substr($description, 0, $length-3) . '...' : $description;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
48
app/Entities/Models/Deletion.php
Normal file
48
app/Entities/Models/Deletion.php
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<?php namespace BookStack\Entities\Models;
|
||||||
|
|
||||||
|
use BookStack\Auth\User;
|
||||||
|
use BookStack\Entities\Models\Entity;
|
||||||
|
use BookStack\Interfaces\Loggable;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||||
|
|
||||||
|
class Deletion extends Model implements Loggable
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the related deletable record.
|
||||||
|
*/
|
||||||
|
public function deletable(): MorphTo
|
||||||
|
{
|
||||||
|
return $this->morphTo('deletable')->withTrashed();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The the user that performed the deletion.
|
||||||
|
*/
|
||||||
|
public function deleter(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'deleted_by');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new deletion record for the provided entity.
|
||||||
|
*/
|
||||||
|
public static function createForEntity(Entity $entity): Deletion
|
||||||
|
{
|
||||||
|
$record = (new self())->forceFill([
|
||||||
|
'deleted_by' => user()->id,
|
||||||
|
'deletable_type' => $entity->getMorphClass(),
|
||||||
|
'deletable_id' => $entity->id,
|
||||||
|
]);
|
||||||
|
$record->save();
|
||||||
|
return $record;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function logDescriptor(): string
|
||||||
|
{
|
||||||
|
$deletable = $this->deletable()->first();
|
||||||
|
return "Deletion ({$this->id}) for {$deletable->getType()} ({$deletable->id}) {$deletable->name}";
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
<?php namespace BookStack\Entities;
|
<?php namespace BookStack\Entities\Models;
|
||||||
|
|
||||||
use BookStack\Actions\Activity;
|
use BookStack\Actions\Activity;
|
||||||
use BookStack\Actions\Comment;
|
use BookStack\Actions\Comment;
|
||||||
@ -6,12 +6,15 @@ use BookStack\Actions\Tag;
|
|||||||
use BookStack\Actions\View;
|
use BookStack\Actions\View;
|
||||||
use BookStack\Auth\Permissions\EntityPermission;
|
use BookStack\Auth\Permissions\EntityPermission;
|
||||||
use BookStack\Auth\Permissions\JointPermission;
|
use BookStack\Auth\Permissions\JointPermission;
|
||||||
|
use BookStack\Entities\Tools\SearchIndex;
|
||||||
|
use BookStack\Entities\Tools\SlugGenerator;
|
||||||
use BookStack\Facades\Permissions;
|
use BookStack\Facades\Permissions;
|
||||||
use BookStack\Ownable;
|
use BookStack\Ownable;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Eloquent\Collection;
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class Entity
|
* Class Entity
|
||||||
@ -31,11 +34,10 @@ use Illuminate\Database\Eloquent\Relations\MorphMany;
|
|||||||
* @method static Entity|Builder hasPermission(string $permission)
|
* @method static Entity|Builder hasPermission(string $permission)
|
||||||
* @method static Builder withLastView()
|
* @method static Builder withLastView()
|
||||||
* @method static Builder withViewCount()
|
* @method static Builder withViewCount()
|
||||||
*
|
|
||||||
* @package BookStack\Entities
|
|
||||||
*/
|
*/
|
||||||
class Entity extends Ownable
|
abstract class Entity extends Ownable
|
||||||
{
|
{
|
||||||
|
use SoftDeletes;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string - Name of property where the main text content is found
|
* @var string - Name of property where the main text content is found
|
||||||
@ -50,7 +52,7 @@ class Entity extends Ownable
|
|||||||
/**
|
/**
|
||||||
* Get the entities that are visible to the current user.
|
* Get the entities that are visible to the current user.
|
||||||
*/
|
*/
|
||||||
public function scopeVisible(Builder $query)
|
public function scopeVisible(Builder $query): Builder
|
||||||
{
|
{
|
||||||
return $this->scopeHasPermission($query, 'view');
|
return $this->scopeHasPermission($query, 'view');
|
||||||
}
|
}
|
||||||
@ -92,24 +94,18 @@ class Entity extends Ownable
|
|||||||
/**
|
/**
|
||||||
* Compares this entity to another given entity.
|
* Compares this entity to another given entity.
|
||||||
* Matches by comparing class and id.
|
* Matches by comparing class and id.
|
||||||
* @param $entity
|
|
||||||
* @return bool
|
|
||||||
*/
|
*/
|
||||||
public function matches($entity)
|
public function matches(Entity $entity): bool
|
||||||
{
|
{
|
||||||
return [get_class($this), $this->id] === [get_class($entity), $entity->id];
|
return [get_class($this), $this->id] === [get_class($entity), $entity->id];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if an entity matches or contains another given entity.
|
* Checks if the current entity matches or contains the given.
|
||||||
* @param Entity $entity
|
|
||||||
* @return bool
|
|
||||||
*/
|
*/
|
||||||
public function matchesOrContains(Entity $entity)
|
public function matchesOrContains(Entity $entity): bool
|
||||||
{
|
{
|
||||||
$matches = [get_class($this), $this->id] === [get_class($entity), $entity->id];
|
if ($this->matches($entity)) {
|
||||||
|
|
||||||
if ($matches) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,9 +122,8 @@ class Entity extends Ownable
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the activity objects for this entity.
|
* Gets the activity objects for this entity.
|
||||||
* @return MorphMany
|
|
||||||
*/
|
*/
|
||||||
public function activity()
|
public function activity(): MorphMany
|
||||||
{
|
{
|
||||||
return $this->morphMany(Activity::class, 'entity')
|
return $this->morphMany(Activity::class, 'entity')
|
||||||
->orderBy('created_at', 'desc');
|
->orderBy('created_at', 'desc');
|
||||||
@ -137,26 +132,23 @@ class Entity extends Ownable
|
|||||||
/**
|
/**
|
||||||
* Get View objects for this entity.
|
* Get View objects for this entity.
|
||||||
*/
|
*/
|
||||||
public function views()
|
public function views(): MorphMany
|
||||||
{
|
{
|
||||||
return $this->morphMany(View::class, 'viewable');
|
return $this->morphMany(View::class, 'viewable');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the Tag models that have been user assigned to this entity.
|
* Get the Tag models that have been user assigned to this entity.
|
||||||
* @return MorphMany
|
|
||||||
*/
|
*/
|
||||||
public function tags()
|
public function tags(): MorphMany
|
||||||
{
|
{
|
||||||
return $this->morphMany(Tag::class, 'entity')->orderBy('order', 'asc');
|
return $this->morphMany(Tag::class, 'entity')->orderBy('order', 'asc');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the comments for an entity
|
* Get the comments for an entity
|
||||||
* @param bool $orderByCreated
|
|
||||||
* @return MorphMany
|
|
||||||
*/
|
*/
|
||||||
public function comments($orderByCreated = true)
|
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;
|
||||||
@ -164,9 +156,8 @@ class Entity extends Ownable
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the related search terms.
|
* Get the related search terms.
|
||||||
* @return MorphMany
|
|
||||||
*/
|
*/
|
||||||
public function searchTerms()
|
public function searchTerms(): MorphMany
|
||||||
{
|
{
|
||||||
return $this->morphMany(SearchTerm::class, 'entity');
|
return $this->morphMany(SearchTerm::class, 'entity');
|
||||||
}
|
}
|
||||||
@ -174,18 +165,15 @@ class Entity extends Ownable
|
|||||||
/**
|
/**
|
||||||
* Get this entities restrictions.
|
* Get this entities restrictions.
|
||||||
*/
|
*/
|
||||||
public function permissions()
|
public function permissions(): MorphMany
|
||||||
{
|
{
|
||||||
return $this->morphMany(EntityPermission::class, 'restrictable');
|
return $this->morphMany(EntityPermission::class, 'restrictable');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if this entity has a specific restriction set against it.
|
* Check if this entity has a specific restriction set against it.
|
||||||
* @param $role_id
|
|
||||||
* @param $action
|
|
||||||
* @return bool
|
|
||||||
*/
|
*/
|
||||||
public function hasRestriction($role_id, $action)
|
public function hasRestriction(int $role_id, string $action): bool
|
||||||
{
|
{
|
||||||
return $this->permissions()->where('role_id', '=', $role_id)
|
return $this->permissions()->where('role_id', '=', $role_id)
|
||||||
->where('action', '=', $action)->count() > 0;
|
->where('action', '=', $action)->count() > 0;
|
||||||
@ -193,20 +181,25 @@ class Entity extends Ownable
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the entity jointPermissions this is connected to.
|
* Get the entity jointPermissions this is connected to.
|
||||||
* @return MorphMany
|
|
||||||
*/
|
*/
|
||||||
public function jointPermissions()
|
public function jointPermissions(): MorphMany
|
||||||
{
|
{
|
||||||
return $this->morphMany(JointPermission::class, 'entity');
|
return $this->morphMany(JointPermission::class, 'entity');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allows checking of the exact class, Used to check entity type.
|
* Get the related delete records for this entity.
|
||||||
* Cleaner method for is_a.
|
|
||||||
* @param $type
|
|
||||||
* @return bool
|
|
||||||
*/
|
*/
|
||||||
public static function isA($type)
|
public function deletions(): MorphMany
|
||||||
|
{
|
||||||
|
return $this->morphMany(Deletion::class, 'deletable');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this instance or class is a certain type of entity.
|
||||||
|
* Examples of $type are 'page', 'book', 'chapter'
|
||||||
|
*/
|
||||||
|
public static function isA(string $type): bool
|
||||||
{
|
{
|
||||||
return static::getType() === strtolower($type);
|
return static::getType() === strtolower($type);
|
||||||
}
|
}
|
||||||
@ -220,28 +213,10 @@ class Entity extends Ownable
|
|||||||
return strtolower(static::getClassName());
|
return strtolower(static::getClassName());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get an instance of an entity of the given type.
|
|
||||||
* @param $type
|
|
||||||
* @return Entity
|
|
||||||
*/
|
|
||||||
public static function getEntityInstance($type)
|
|
||||||
{
|
|
||||||
$types = ['Page', 'Book', 'Chapter', 'Bookshelf'];
|
|
||||||
$className = str_replace([' ', '-', '_'], '', ucwords($type));
|
|
||||||
if (!in_array($className, $types)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return app('BookStack\\Entities\\' . $className);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a limited-length version of the entities name.
|
* Gets a limited-length version of the entities name.
|
||||||
* @param int $length
|
|
||||||
* @return string
|
|
||||||
*/
|
*/
|
||||||
public function getShortName($length = 25)
|
public function getShortName(int $length = 25): string
|
||||||
{
|
{
|
||||||
if (mb_strlen($this->name) <= $length) {
|
if (mb_strlen($this->name) <= $length) {
|
||||||
return $this->name;
|
return $this->name;
|
||||||
@ -251,35 +226,45 @@ class Entity extends Ownable
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the body text of this entity.
|
* Get the body text of this entity.
|
||||||
* @return mixed
|
|
||||||
*/
|
*/
|
||||||
public function getText()
|
public function getText(): string
|
||||||
{
|
{
|
||||||
return $this->{$this->textField};
|
return $this->{$this->textField} ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get an excerpt of this entity's descriptive content to the specified length.
|
* Get an excerpt of this entity's descriptive content to the specified length.
|
||||||
* @param int $length
|
|
||||||
* @return mixed
|
|
||||||
*/
|
*/
|
||||||
public function getExcerpt(int $length = 100)
|
public function getExcerpt(int $length = 100): string
|
||||||
{
|
{
|
||||||
$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
|
||||||
* @param $path
|
|
||||||
* @return string
|
|
||||||
*/
|
*/
|
||||||
public function getUrl($path = '/')
|
abstract public function getUrl(string $path = '/'): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the parent entity if existing.
|
||||||
|
* This is the "static" parent and does not include dynamic
|
||||||
|
* relations such as shelves to books.
|
||||||
|
*/
|
||||||
|
public function getParent(): ?Entity
|
||||||
{
|
{
|
||||||
return $path;
|
if ($this->isA('page')) {
|
||||||
|
return $this->chapter_id ? $this->chapter()->withTrashed()->first() : $this->book()->withTrashed()->first();
|
||||||
|
}
|
||||||
|
if ($this->isA('chapter')) {
|
||||||
|
return $this->book()->withTrashed()->first();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -288,7 +273,7 @@ class Entity extends Ownable
|
|||||||
public function rebuildPermissions()
|
public function rebuildPermissions()
|
||||||
{
|
{
|
||||||
/** @noinspection PhpUnhandledExceptionInspection */
|
/** @noinspection PhpUnhandledExceptionInspection */
|
||||||
Permissions::buildJointPermissionsForEntity($this);
|
Permissions::buildJointPermissionsForEntity(clone $this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -296,8 +281,7 @@ class Entity extends Ownable
|
|||||||
*/
|
*/
|
||||||
public function indexForSearch()
|
public function indexForSearch()
|
||||||
{
|
{
|
||||||
$searchService = app()->make(SearchService::class);
|
app(SearchIndex::class)->indexEntity(clone $this);
|
||||||
$searchService->indexEntity($this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -305,8 +289,7 @@ class Entity extends Ownable
|
|||||||
*/
|
*/
|
||||||
public function refreshSlug(): string
|
public function refreshSlug(): string
|
||||||
{
|
{
|
||||||
$generator = new SlugGenerator($this);
|
$this->slug = (new SlugGenerator)->generate($this);
|
||||||
$this->slug = $generator->generate();
|
|
||||||
return $this->slug;
|
return $this->slug;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
|
||||||
namespace BookStack\Entities;
|
namespace BookStack\Entities\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
@ -1,5 +1,6 @@
|
|||||||
<?php namespace BookStack\Entities;
|
<?php namespace BookStack\Entities\Models;
|
||||||
|
|
||||||
|
use BookStack\Entities\Tools\PageContent;
|
||||||
use BookStack\Uploads\Attachment;
|
use BookStack\Uploads\Attachment;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Eloquent\Collection;
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
@ -21,16 +22,23 @@ use Permissions;
|
|||||||
*/
|
*/
|
||||||
class Page extends BookChild
|
class Page extends BookChild
|
||||||
{
|
{
|
||||||
protected $fillable = ['name', 'html', 'priority', 'markdown'];
|
protected $fillable = ['name', 'priority', 'markdown'];
|
||||||
|
|
||||||
protected $simpleAttributes = ['name', 'id', 'slug'];
|
protected $simpleAttributes = ['name', 'id', 'slug'];
|
||||||
|
|
||||||
public $textField = 'text';
|
public $textField = 'text';
|
||||||
|
|
||||||
|
protected $hidden = ['html', 'markdown', 'text', 'restricted', 'pivot', 'deleted_at'];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'draft' => 'boolean',
|
||||||
|
'template' => 'boolean',
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the entities that are visible to the current user.
|
* Get the entities that are visible to the current user.
|
||||||
*/
|
*/
|
||||||
public function scopeVisible(Builder $query)
|
public function scopeVisible(Builder $query): Builder
|
||||||
{
|
{
|
||||||
$query = Permissions::enforceDraftVisiblityOnQuery($query);
|
$query = Permissions::enforceDraftVisiblityOnQuery($query);
|
||||||
return parent::scopeVisible($query);
|
return parent::scopeVisible($query);
|
||||||
@ -47,14 +55,6 @@ class Page extends BookChild
|
|||||||
return $array;
|
return $array;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the parent item
|
|
||||||
*/
|
|
||||||
public function parent(): Entity
|
|
||||||
{
|
|
||||||
return $this->chapter_id ? $this->chapter : $this->book;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the chapter that this page is in, If applicable.
|
* Get the chapter that this page is in, If applicable.
|
||||||
* @return BelongsTo
|
* @return BelongsTo
|
||||||
@ -92,22 +92,19 @@ class Page extends BookChild
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the url for this page.
|
* Get the url of this page.
|
||||||
* @param string|bool $path
|
|
||||||
* @return string
|
|
||||||
*/
|
*/
|
||||||
public function getUrl($path = false)
|
public function getUrl($path = ''): string
|
||||||
{
|
{
|
||||||
$bookSlug = $this->getAttribute('bookSlug') ? $this->getAttribute('bookSlug') : $this->book->slug;
|
$parts = [
|
||||||
$midText = $this->draft ? '/draft/' : '/page/';
|
'books',
|
||||||
$idComponent = $this->draft ? $this->id : urlencode($this->slug);
|
urlencode($this->getAttribute('bookSlug') ?? $this->book->slug),
|
||||||
|
$this->draft ? 'draft' : 'page',
|
||||||
|
$this->draft ? $this->id : urlencode($this->slug),
|
||||||
|
trim($path, '/'),
|
||||||
|
];
|
||||||
|
|
||||||
$url = '/books/' . urlencode($bookSlug) . $midText . $idComponent;
|
return url('/' . implode('/', $parts));
|
||||||
if ($path !== false) {
|
|
||||||
$url .= '/' . trim($path, '/');
|
|
||||||
}
|
|
||||||
|
|
||||||
return url($url);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -118,4 +115,15 @@ class Page extends BookChild
|
|||||||
{
|
{
|
||||||
return $this->revisions()->first();
|
return $this->revisions()->first();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get this page for JSON display.
|
||||||
|
*/
|
||||||
|
public function forJsonDisplay(): Page
|
||||||
|
{
|
||||||
|
$refreshed = $this->refresh()->unsetRelations()->load(['tags', 'createdBy', 'updatedBy']);
|
||||||
|
$refreshed->setHidden(array_diff($refreshed->getHidden(), ['html', 'markdown']));
|
||||||
|
$refreshed->html = (new PageContent($refreshed))->render();
|
||||||
|
return $refreshed;
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,6 +1,7 @@
|
|||||||
<?php namespace BookStack\Entities;
|
<?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;
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
<?php namespace BookStack\Entities;
|
<?php namespace BookStack\Entities\Models;
|
||||||
|
|
||||||
use BookStack\Model;
|
use BookStack\Model;
|
||||||
|
|
@ -2,11 +2,12 @@
|
|||||||
|
|
||||||
namespace BookStack\Entities\Repos;
|
namespace BookStack\Entities\Repos;
|
||||||
|
|
||||||
|
use BookStack\Actions\ActivityType;
|
||||||
use BookStack\Actions\TagRepo;
|
use BookStack\Actions\TagRepo;
|
||||||
use BookStack\Entities\Book;
|
use BookStack\Entities\Models\Entity;
|
||||||
use BookStack\Entities\Entity;
|
use BookStack\Entities\Models\HasCoverImage;
|
||||||
use BookStack\Entities\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;
|
use Illuminate\Support\Collection;
|
||||||
@ -18,10 +19,6 @@ class BaseRepo
|
|||||||
protected $imageRepo;
|
protected $imageRepo;
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* BaseRepo constructor.
|
|
||||||
* @param $tagRepo
|
|
||||||
*/
|
|
||||||
public function __construct(TagRepo $tagRepo, ImageRepo $imageRepo)
|
public function __construct(TagRepo $tagRepo, ImageRepo $imageRepo)
|
||||||
{
|
{
|
||||||
$this->tagRepo = $tagRepo;
|
$this->tagRepo = $tagRepo;
|
||||||
@ -115,5 +112,6 @@ class BaseRepo
|
|||||||
|
|
||||||
$entity->save();
|
$entity->save();
|
||||||
$entity->rebuildPermissions();
|
$entity->rebuildPermissions();
|
||||||
|
Activity::addForEntity($entity, ActivityType::PERMISSIONS_UPDATE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
<?php namespace BookStack\Entities\Repos;
|
<?php namespace BookStack\Entities\Repos;
|
||||||
|
|
||||||
|
use BookStack\Actions\ActivityType;
|
||||||
use BookStack\Actions\TagRepo;
|
use BookStack\Actions\TagRepo;
|
||||||
use BookStack\Entities\Book;
|
use BookStack\Entities\Models\Book;
|
||||||
use BookStack\Entities\Managers\TrashCan;
|
use BookStack\Entities\Tools\TrashCan;
|
||||||
use BookStack\Exceptions\ImageUploadException;
|
use BookStack\Exceptions\ImageUploadException;
|
||||||
use BookStack\Exceptions\NotFoundException;
|
use BookStack\Exceptions\NotFoundException;
|
||||||
use BookStack\Exceptions\NotifyException;
|
use BookStack\Facades\Activity;
|
||||||
use BookStack\Uploads\ImageRepo;
|
use BookStack\Uploads\ImageRepo;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Contracts\Container\BindingResolutionException;
|
|
||||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||||
use Illuminate\Http\UploadedFile;
|
use Illuminate\Http\UploadedFile;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
@ -22,7 +22,6 @@ class BookRepo
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* BookRepo constructor.
|
* BookRepo constructor.
|
||||||
* @param $tagRepo
|
|
||||||
*/
|
*/
|
||||||
public function __construct(BaseRepo $baseRepo, TagRepo $tagRepo, ImageRepo $imageRepo)
|
public function __construct(BaseRepo $baseRepo, TagRepo $tagRepo, ImageRepo $imageRepo)
|
||||||
{
|
{
|
||||||
@ -91,6 +90,7 @@ class BookRepo
|
|||||||
{
|
{
|
||||||
$book = new Book();
|
$book = new Book();
|
||||||
$this->baseRepo->create($book, $input);
|
$this->baseRepo->create($book, $input);
|
||||||
|
Activity::addForEntity($book, ActivityType::BOOK_CREATE);
|
||||||
return $book;
|
return $book;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,6 +100,7 @@ class BookRepo
|
|||||||
public function update(Book $book, array $input): Book
|
public function update(Book $book, array $input): Book
|
||||||
{
|
{
|
||||||
$this->baseRepo->update($book, $input);
|
$this->baseRepo->update($book, $input);
|
||||||
|
Activity::addForEntity($book, ActivityType::BOOK_UPDATE);
|
||||||
return $book;
|
return $book;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,12 +124,14 @@ class BookRepo
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove a book from the system.
|
* Remove a book from the system.
|
||||||
* @throws NotifyException
|
* @throws Exception
|
||||||
* @throws BindingResolutionException
|
|
||||||
*/
|
*/
|
||||||
public function destroy(Book $book)
|
public function destroy(Book $book)
|
||||||
{
|
{
|
||||||
$trashCan = new TrashCan();
|
$trashCan = new TrashCan();
|
||||||
$trashCan->destroyBook($book);
|
$trashCan->softDestroyBook($book);
|
||||||
|
Activity::addForEntity($book, ActivityType::BOOK_DELETE);
|
||||||
|
|
||||||
|
$trashCan->autoClearOld();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
<?php namespace BookStack\Entities\Repos;
|
<?php namespace BookStack\Entities\Repos;
|
||||||
|
|
||||||
use BookStack\Entities\Book;
|
use BookStack\Actions\ActivityType;
|
||||||
use BookStack\Entities\Bookshelf;
|
use BookStack\Entities\Models\Book;
|
||||||
use BookStack\Entities\Managers\TrashCan;
|
use BookStack\Entities\Models\Bookshelf;
|
||||||
|
use BookStack\Entities\Tools\TrashCan;
|
||||||
use BookStack\Exceptions\ImageUploadException;
|
use BookStack\Exceptions\ImageUploadException;
|
||||||
use BookStack\Exceptions\NotFoundException;
|
use BookStack\Exceptions\NotFoundException;
|
||||||
|
use BookStack\Facades\Activity;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||||
use Illuminate\Http\UploadedFile;
|
use Illuminate\Http\UploadedFile;
|
||||||
@ -16,7 +18,6 @@ class BookshelfRepo
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* BookshelfRepo constructor.
|
* BookshelfRepo constructor.
|
||||||
* @param $baseRepo
|
|
||||||
*/
|
*/
|
||||||
public function __construct(BaseRepo $baseRepo)
|
public function __construct(BaseRepo $baseRepo)
|
||||||
{
|
{
|
||||||
@ -28,8 +29,10 @@ class BookshelfRepo
|
|||||||
*/
|
*/
|
||||||
public function getAllPaginated(int $count = 20, string $sort = 'name', string $order = 'asc'): LengthAwarePaginator
|
public function getAllPaginated(int $count = 20, string $sort = 'name', string $order = 'asc'): LengthAwarePaginator
|
||||||
{
|
{
|
||||||
return Bookshelf::visible()->with('visibleBooks')
|
return Bookshelf::visible()
|
||||||
->orderBy($sort, $order)->paginate($count);
|
->with('visibleBooks')
|
||||||
|
->orderBy($sort, $order)
|
||||||
|
->paginate($count);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -85,16 +88,22 @@ class BookshelfRepo
|
|||||||
$shelf = new Bookshelf();
|
$shelf = new Bookshelf();
|
||||||
$this->baseRepo->create($shelf, $input);
|
$this->baseRepo->create($shelf, $input);
|
||||||
$this->updateBooks($shelf, $bookIds);
|
$this->updateBooks($shelf, $bookIds);
|
||||||
|
Activity::addForEntity($shelf, ActivityType::BOOKSHELF_CREATE);
|
||||||
return $shelf;
|
return $shelf;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new shelf in the system.
|
* Update an existing shelf in the system using the given input.
|
||||||
*/
|
*/
|
||||||
public function update(Bookshelf $shelf, array $input, array $bookIds): Bookshelf
|
public function update(Bookshelf $shelf, array $input, ?array $bookIds): Bookshelf
|
||||||
{
|
{
|
||||||
$this->baseRepo->update($shelf, $input);
|
$this->baseRepo->update($shelf, $input);
|
||||||
|
|
||||||
|
if (!is_null($bookIds)) {
|
||||||
$this->updateBooks($shelf, $bookIds);
|
$this->updateBooks($shelf, $bookIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
Activity::addForEntity($shelf, ActivityType::BOOKSHELF_UPDATE);
|
||||||
return $shelf;
|
return $shelf;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,6 +177,8 @@ class BookshelfRepo
|
|||||||
public function destroy(Bookshelf $shelf)
|
public function destroy(Bookshelf $shelf)
|
||||||
{
|
{
|
||||||
$trashCan = new TrashCan();
|
$trashCan = new TrashCan();
|
||||||
$trashCan->destroyShelf($shelf);
|
$trashCan->softDestroyShelf($shelf);
|
||||||
|
Activity::addForEntity($shelf, ActivityType::BOOKSHELF_DELETE);
|
||||||
|
$trashCan->autoClearOld();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,14 @@
|
|||||||
<?php namespace BookStack\Entities\Repos;
|
<?php namespace BookStack\Entities\Repos;
|
||||||
|
|
||||||
use BookStack\Entities\Book;
|
use BookStack\Actions\ActivityType;
|
||||||
use BookStack\Entities\Chapter;
|
use BookStack\Entities\Models\Book;
|
||||||
use BookStack\Entities\Managers\BookContents;
|
use BookStack\Entities\Models\Chapter;
|
||||||
use BookStack\Entities\Managers\TrashCan;
|
use BookStack\Entities\Tools\BookContents;
|
||||||
|
use BookStack\Entities\Tools\TrashCan;
|
||||||
use BookStack\Exceptions\MoveOperationException;
|
use BookStack\Exceptions\MoveOperationException;
|
||||||
use BookStack\Exceptions\NotFoundException;
|
use BookStack\Exceptions\NotFoundException;
|
||||||
use BookStack\Exceptions\NotifyException;
|
use BookStack\Facades\Activity;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Contracts\Container\BindingResolutionException;
|
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
class ChapterRepo
|
class ChapterRepo
|
||||||
@ -19,7 +18,6 @@ class ChapterRepo
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* ChapterRepo constructor.
|
* ChapterRepo constructor.
|
||||||
* @param $baseRepo
|
|
||||||
*/
|
*/
|
||||||
public function __construct(BaseRepo $baseRepo)
|
public function __construct(BaseRepo $baseRepo)
|
||||||
{
|
{
|
||||||
@ -50,6 +48,7 @@ class ChapterRepo
|
|||||||
$chapter->book_id = $parentBook->id;
|
$chapter->book_id = $parentBook->id;
|
||||||
$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);
|
||||||
return $chapter;
|
return $chapter;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,6 +58,7 @@ class ChapterRepo
|
|||||||
public function update(Chapter $chapter, array $input): Chapter
|
public function update(Chapter $chapter, array $input): Chapter
|
||||||
{
|
{
|
||||||
$this->baseRepo->update($chapter, $input);
|
$this->baseRepo->update($chapter, $input);
|
||||||
|
Activity::addForEntity($chapter, ActivityType::CHAPTER_UPDATE);
|
||||||
return $chapter;
|
return $chapter;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,7 +77,9 @@ class ChapterRepo
|
|||||||
public function destroy(Chapter $chapter)
|
public function destroy(Chapter $chapter)
|
||||||
{
|
{
|
||||||
$trashCan = new TrashCan();
|
$trashCan = new TrashCan();
|
||||||
$trashCan->destroyChapter($chapter);
|
$trashCan->softDestroyChapter($chapter);
|
||||||
|
Activity::addForEntity($chapter, ActivityType::CHAPTER_DELETE);
|
||||||
|
$trashCan->autoClearOld();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -96,6 +98,7 @@ class ChapterRepo
|
|||||||
throw new MoveOperationException('Chapters can only be moved into books');
|
throw new MoveOperationException('Chapters can only be moved into books');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @var Book $parent */
|
||||||
$parent = Book::visible()->where('id', '=', $entityId)->first();
|
$parent = Book::visible()->where('id', '=', $entityId)->first();
|
||||||
if ($parent === null) {
|
if ($parent === null) {
|
||||||
throw new MoveOperationException('Book to move chapter into not found');
|
throw new MoveOperationException('Book to move chapter into not found');
|
||||||
@ -103,6 +106,8 @@ class ChapterRepo
|
|||||||
|
|
||||||
$chapter->changeBook($parent->id);
|
$chapter->changeBook($parent->id);
|
||||||
$chapter->rebuildPermissions();
|
$chapter->rebuildPermissions();
|
||||||
|
Activity::addForEntity($chapter, ActivityType::CHAPTER_MOVE);
|
||||||
|
|
||||||
return $parent;
|
return $parent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,19 @@
|
|||||||
<?php namespace BookStack\Entities\Repos;
|
<?php namespace BookStack\Entities\Repos;
|
||||||
|
|
||||||
use BookStack\Entities\Book;
|
use BookStack\Actions\ActivityType;
|
||||||
use BookStack\Entities\Chapter;
|
use BookStack\Entities\Models\Book;
|
||||||
use BookStack\Entities\Entity;
|
use BookStack\Entities\Models\Chapter;
|
||||||
use BookStack\Entities\Managers\BookContents;
|
use BookStack\Entities\Models\Entity;
|
||||||
use BookStack\Entities\Managers\PageContent;
|
use BookStack\Entities\Tools\BookContents;
|
||||||
use BookStack\Entities\Managers\TrashCan;
|
use BookStack\Entities\Tools\PageContent;
|
||||||
use BookStack\Entities\Page;
|
use BookStack\Entities\Tools\TrashCan;
|
||||||
use BookStack\Entities\PageRevision;
|
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\NotifyException;
|
|
||||||
use BookStack\Exceptions\PermissionsException;
|
use BookStack\Exceptions\PermissionsException;
|
||||||
|
use BookStack\Facades\Activity;
|
||||||
|
use Exception;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Pagination\LengthAwarePaginator;
|
use Illuminate\Pagination\LengthAwarePaginator;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
@ -33,9 +35,9 @@ class PageRepo
|
|||||||
* Get a page by ID.
|
* Get a page by ID.
|
||||||
* @throws NotFoundException
|
* @throws NotFoundException
|
||||||
*/
|
*/
|
||||||
public function getById(int $id): Page
|
public function getById(int $id, array $relations = ['book']): Page
|
||||||
{
|
{
|
||||||
$page = Page::visible()->with(['book'])->find($id);
|
$page = Page::visible()->with($relations)->find($id);
|
||||||
|
|
||||||
if (!$page) {
|
if (!$page) {
|
||||||
throw new NotFoundException(trans('errors.page_not_found'));
|
throw new NotFoundException(trans('errors.page_not_found'));
|
||||||
@ -150,12 +152,8 @@ class PageRepo
|
|||||||
public function publishDraft(Page $draft, array $input): Page
|
public function publishDraft(Page $draft, array $input): Page
|
||||||
{
|
{
|
||||||
$this->baseRepo->update($draft, $input);
|
$this->baseRepo->update($draft, $input);
|
||||||
if (isset($input['template']) && userCan('templates-manage')) {
|
$this->updateTemplateStatusAndContentFromInput($draft, $input);
|
||||||
$draft->template = ($input['template'] === 'true');
|
|
||||||
}
|
|
||||||
|
|
||||||
$pageContent = new PageContent($draft);
|
|
||||||
$pageContent->setNewHTML($input['html']);
|
|
||||||
$draft->draft = false;
|
$draft->draft = false;
|
||||||
$draft->revision_count = 1;
|
$draft->revision_count = 1;
|
||||||
$draft->priority = $this->getNewPriority($draft);
|
$draft->priority = $this->getNewPriority($draft);
|
||||||
@ -164,7 +162,10 @@ class PageRepo
|
|||||||
|
|
||||||
$this->savePageRevision($draft, trans('entities.pages_initial_revision'));
|
$this->savePageRevision($draft, trans('entities.pages_initial_revision'));
|
||||||
$draft->indexForSearch();
|
$draft->indexForSearch();
|
||||||
return $draft->refresh();
|
$draft->refresh();
|
||||||
|
|
||||||
|
Activity::addForEntity($draft, ActivityType::PAGE_CREATE);
|
||||||
|
return $draft;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -176,16 +177,10 @@ class PageRepo
|
|||||||
$oldHtml = $page->html;
|
$oldHtml = $page->html;
|
||||||
$oldName = $page->name;
|
$oldName = $page->name;
|
||||||
|
|
||||||
if (isset($input['template']) && userCan('templates-manage')) {
|
$this->updateTemplateStatusAndContentFromInput($page, $input);
|
||||||
$page->template = ($input['template'] === 'true');
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->baseRepo->update($page, $input);
|
$this->baseRepo->update($page, $input);
|
||||||
|
|
||||||
// Update with new details
|
// Update with new details
|
||||||
$page->fill($input);
|
|
||||||
$pageContent = new PageContent($page);
|
|
||||||
$pageContent->setNewHTML($input['html']);
|
|
||||||
$page->revision_count++;
|
$page->revision_count++;
|
||||||
|
|
||||||
if (setting('app-editor') !== 'markdown') {
|
if (setting('app-editor') !== 'markdown') {
|
||||||
@ -203,15 +198,30 @@ class PageRepo
|
|||||||
$this->savePageRevision($page, $summary);
|
$this->savePageRevision($page, $summary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Activity::addForEntity($page, ActivityType::PAGE_UPDATE);
|
||||||
return $page;
|
return $page;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function updateTemplateStatusAndContentFromInput(Page $page, array $input)
|
||||||
|
{
|
||||||
|
if (isset($input['template']) && userCan('templates-manage')) {
|
||||||
|
$page->template = ($input['template'] === 'true');
|
||||||
|
}
|
||||||
|
|
||||||
|
$pageContent = new PageContent($page);
|
||||||
|
if (isset($input['html'])) {
|
||||||
|
$pageContent->setNewHTML($input['html']);
|
||||||
|
} else {
|
||||||
|
$pageContent->setNewMarkdown($input['markdown']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saves a page revision into the system.
|
* Saves a page revision into the system.
|
||||||
*/
|
*/
|
||||||
protected function savePageRevision(Page $page, string $summary = null)
|
protected function savePageRevision(Page $page, string $summary = null)
|
||||||
{
|
{
|
||||||
$revision = new PageRevision($page->toArray());
|
$revision = new PageRevision($page->getAttributes());
|
||||||
|
|
||||||
if (setting('app-editor') !== 'markdown') {
|
if (setting('app-editor') !== 'markdown') {
|
||||||
$revision->markdown = '';
|
$revision->markdown = '';
|
||||||
@ -238,11 +248,10 @@ class PageRepo
|
|||||||
{
|
{
|
||||||
// If the page itself is a draft simply update that
|
// If the page itself is a draft simply update that
|
||||||
if ($page->draft) {
|
if ($page->draft) {
|
||||||
$page->fill($input);
|
|
||||||
if (isset($input['html'])) {
|
if (isset($input['html'])) {
|
||||||
$content = new PageContent($page);
|
(new PageContent($page))->setNewHTML($input['html']);
|
||||||
$content->setNewHTML($input['html']);
|
|
||||||
}
|
}
|
||||||
|
$page->fill($input);
|
||||||
$page->save();
|
$page->save();
|
||||||
return $page;
|
return $page;
|
||||||
}
|
}
|
||||||
@ -260,12 +269,14 @@ class PageRepo
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Destroy a page from the system.
|
* Destroy a page from the system.
|
||||||
* @throws NotifyException
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
public function destroy(Page $page)
|
public function destroy(Page $page)
|
||||||
{
|
{
|
||||||
$trashCan = new TrashCan();
|
$trashCan = new TrashCan();
|
||||||
$trashCan->destroyPage($page);
|
$trashCan->softDestroyPage($page);
|
||||||
|
Activity::addForEntity($page, ActivityType::PAGE_DELETE);
|
||||||
|
$trashCan->autoClearOld();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -279,12 +290,13 @@ class PageRepo
|
|||||||
$revision = $page->revisions()->where('id', '=', $revisionId)->first();
|
$revision = $page->revisions()->where('id', '=', $revisionId)->first();
|
||||||
$page->fill($revision->toArray());
|
$page->fill($revision->toArray());
|
||||||
$content = new PageContent($page);
|
$content = new PageContent($page);
|
||||||
$content->setNewHTML($page->html);
|
$content->setNewHTML($revision->html);
|
||||||
$page->updated_by = user()->id;
|
$page->updated_by = user()->id;
|
||||||
$page->refreshSlug();
|
$page->refreshSlug();
|
||||||
$page->save();
|
$page->save();
|
||||||
|
|
||||||
$page->indexForSearch();
|
$page->indexForSearch();
|
||||||
|
Activity::addForEntity($page, ActivityType::PAGE_RESTORE);
|
||||||
return $page;
|
return $page;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -295,7 +307,7 @@ class PageRepo
|
|||||||
* @throws MoveOperationException
|
* @throws MoveOperationException
|
||||||
* @throws PermissionsException
|
* @throws PermissionsException
|
||||||
*/
|
*/
|
||||||
public function move(Page $page, string $parentIdentifier): Book
|
public function move(Page $page, string $parentIdentifier): Entity
|
||||||
{
|
{
|
||||||
$parent = $this->findParentByIdentifier($parentIdentifier);
|
$parent = $this->findParentByIdentifier($parentIdentifier);
|
||||||
if ($parent === null) {
|
if ($parent === null) {
|
||||||
@ -310,7 +322,8 @@ class PageRepo
|
|||||||
$page->changeBook($parent instanceof Book ? $parent->id : $parent->book->id);
|
$page->changeBook($parent instanceof Book ? $parent->id : $parent->book->id);
|
||||||
$page->rebuildPermissions();
|
$page->rebuildPermissions();
|
||||||
|
|
||||||
return ($parent instanceof Book ? $parent : $parent->book);
|
Activity::addForEntity($page, ActivityType::PAGE_MOVE);
|
||||||
|
return $parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -321,7 +334,7 @@ class PageRepo
|
|||||||
*/
|
*/
|
||||||
public function copy(Page $page, string $parentIdentifier = null, string $newName = null): Page
|
public function copy(Page $page, string $parentIdentifier = null, string $newName = null): Page
|
||||||
{
|
{
|
||||||
$parent = $parentIdentifier ? $this->findParentByIdentifier($parentIdentifier) : $page->parent();
|
$parent = $parentIdentifier ? $this->findParentByIdentifier($parentIdentifier) : $page->getParent();
|
||||||
if ($parent === null) {
|
if ($parent === null) {
|
||||||
throw new MoveOperationException('Book or chapter to move page into not found');
|
throw new MoveOperationException('Book or chapter to move page into not found');
|
||||||
}
|
}
|
||||||
@ -440,8 +453,9 @@ class PageRepo
|
|||||||
*/
|
*/
|
||||||
protected function getNewPriority(Page $page): int
|
protected function getNewPriority(Page $page): int
|
||||||
{
|
{
|
||||||
if ($page->parent() instanceof Chapter) {
|
$parent = $page->getParent();
|
||||||
$lastPage = $page->parent()->pages('desc')->first();
|
if ($parent instanceof Chapter) {
|
||||||
|
$lastPage = $parent->pages('desc')->first();
|
||||||
return $lastPage ? $lastPage->priority + 1 : 0;
|
return $lastPage ? $lastPage->priority + 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,62 +0,0 @@
|
|||||||
<?php namespace BookStack\Entities;
|
|
||||||
|
|
||||||
class SlugGenerator
|
|
||||||
{
|
|
||||||
|
|
||||||
protected $entity;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SlugGenerator constructor.
|
|
||||||
* @param $entity
|
|
||||||
*/
|
|
||||||
public function __construct(Entity $entity)
|
|
||||||
{
|
|
||||||
$this->entity = $entity;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate a fresh slug for the given entity.
|
|
||||||
* The slug will generated so it does not conflict within the same parent item.
|
|
||||||
*/
|
|
||||||
public function generate(): string
|
|
||||||
{
|
|
||||||
$slug = $this->formatNameAsSlug($this->entity->name);
|
|
||||||
while ($this->slugInUse($slug)) {
|
|
||||||
$slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
|
|
||||||
}
|
|
||||||
return $slug;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Format a name as a url slug.
|
|
||||||
*/
|
|
||||||
protected function formatNameAsSlug(string $name): string
|
|
||||||
{
|
|
||||||
$slug = preg_replace('/[\+\/\\\?\@\}\{\.\,\=\[\]\#\&\!\*\'\;\:\$\%]/', '', mb_strtolower($name));
|
|
||||||
$slug = preg_replace('/\s{2,}/', ' ', $slug);
|
|
||||||
$slug = str_replace(' ', '-', $slug);
|
|
||||||
if ($slug === "") {
|
|
||||||
$slug = substr(md5(rand(1, 500)), 0, 5);
|
|
||||||
}
|
|
||||||
return $slug;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a slug is already in-use for this
|
|
||||||
* type of model within the same parent.
|
|
||||||
*/
|
|
||||||
protected function slugInUse(string $slug): bool
|
|
||||||
{
|
|
||||||
$query = $this->entity->newQuery()->where('slug', '=', $slug);
|
|
||||||
|
|
||||||
if ($this->entity instanceof BookChild) {
|
|
||||||
$query->where('book_id', '=', $this->entity->book_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->entity->id) {
|
|
||||||
$query->where('id', '!=', $this->entity->id);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $query->count() > 0;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,10 +1,10 @@
|
|||||||
<?php namespace BookStack\Entities\Managers;
|
<?php namespace BookStack\Entities\Tools;
|
||||||
|
|
||||||
use BookStack\Entities\Book;
|
use BookStack\Entities\Models\Book;
|
||||||
use BookStack\Entities\BookChild;
|
use BookStack\Entities\Models\BookChild;
|
||||||
use BookStack\Entities\Chapter;
|
use BookStack\Entities\Models\Chapter;
|
||||||
use BookStack\Entities\Entity;
|
use BookStack\Entities\Models\Entity;
|
||||||
use BookStack\Entities\Page;
|
use BookStack\Entities\Models\Page;
|
||||||
use BookStack\Exceptions\SortOperationException;
|
use BookStack\Exceptions\SortOperationException;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
@ -18,7 +18,6 @@ class BookContents
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* BookContents constructor.
|
* BookContents constructor.
|
||||||
* @param $book
|
|
||||||
*/
|
*/
|
||||||
public function __construct(Book $book)
|
public function __construct(Book $book)
|
||||||
{
|
{
|
||||||
@ -41,7 +40,6 @@ class BookContents
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the contents as a sorted collection tree.
|
* Get the contents as a sorted collection tree.
|
||||||
* TODO - Support $renderPages option
|
|
||||||
*/
|
*/
|
||||||
public function getTree(bool $showDrafts = false, bool $renderPages = false): Collection
|
public function getTree(bool $showDrafts = false, bool $renderPages = false): Collection
|
||||||
{
|
{
|
||||||
@ -60,8 +58,12 @@ class BookContents
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$all->each(function (Entity $entity) {
|
$all->each(function (Entity $entity) use ($renderPages) {
|
||||||
$entity->setRelation('book', $this->book);
|
$entity->setRelation('book', $this->book);
|
||||||
|
|
||||||
|
if ($renderPages && $entity->isA('page')) {
|
||||||
|
$entity->html = (new PageContent($entity))->render();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return collect($chapters)->concat($lonePages)->sortBy($this->bookChildSortFunc());
|
return collect($chapters)->concat($lonePages)->sortBy($this->bookChildSortFunc());
|
@ -1,14 +1,15 @@
|
|||||||
<?php namespace BookStack\Entities;
|
<?php namespace BookStack\Entities\Tools;
|
||||||
|
|
||||||
use BookStack\Entities\Managers\BookContents;
|
use BookStack\Entities\Models\Book;
|
||||||
use BookStack\Entities\Managers\PageContent;
|
use BookStack\Entities\Models\Chapter;
|
||||||
|
use BookStack\Entities\Models\Page;
|
||||||
use BookStack\Uploads\ImageService;
|
use BookStack\Uploads\ImageService;
|
||||||
use DomPDF;
|
use DomPDF;
|
||||||
use Exception;
|
use Exception;
|
||||||
use SnappyPDF;
|
use SnappyPDF;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
|
||||||
class ExportService
|
class ExportFormatter
|
||||||
{
|
{
|
||||||
|
|
||||||
protected $imageService;
|
protected $imageService;
|
@ -1,10 +1,10 @@
|
|||||||
<?php namespace BookStack\Entities\Managers;
|
<?php namespace BookStack\Entities\Tools;
|
||||||
|
|
||||||
use BookStack\Entities\Page;
|
use BookStack\Entities\Models\Page;
|
||||||
use DOMDocument;
|
use DOMDocument;
|
||||||
use DOMElement;
|
|
||||||
use DOMNodeList;
|
use DOMNodeList;
|
||||||
use DOMXPath;
|
use DOMXPath;
|
||||||
|
use League\CommonMark\CommonMarkConverter;
|
||||||
|
|
||||||
class PageContent
|
class PageContent
|
||||||
{
|
{
|
||||||
@ -26,6 +26,27 @@ class PageContent
|
|||||||
{
|
{
|
||||||
$this->page->html = $this->formatHtml($html);
|
$this->page->html = $this->formatHtml($html);
|
||||||
$this->page->text = $this->toPlainText();
|
$this->page->text = $this->toPlainText();
|
||||||
|
$this->page->markdown = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the content of the page with new provided Markdown content.
|
||||||
|
*/
|
||||||
|
public function setNewMarkdown(string $markdown)
|
||||||
|
{
|
||||||
|
$this->page->markdown = $markdown;
|
||||||
|
$html = $this->markdownToHtml($markdown);
|
||||||
|
$this->page->html = $this->formatHtml($html);
|
||||||
|
$this->page->text = $this->toPlainText();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert the given Markdown content to a HTML string.
|
||||||
|
*/
|
||||||
|
protected function markdownToHtml(string $markdown): string
|
||||||
|
{
|
||||||
|
$converter = new CommonMarkConverter();
|
||||||
|
return $converter->convertToHtml($markdown);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -44,18 +65,24 @@ class PageContent
|
|||||||
$container = $doc->documentElement;
|
$container = $doc->documentElement;
|
||||||
$body = $container->childNodes->item(0);
|
$body = $container->childNodes->item(0);
|
||||||
$childNodes = $body->childNodes;
|
$childNodes = $body->childNodes;
|
||||||
|
$xPath = new DOMXPath($doc);
|
||||||
|
|
||||||
// Set ids on top-level nodes
|
// Set ids on top-level nodes
|
||||||
$idMap = [];
|
$idMap = [];
|
||||||
foreach ($childNodes as $index => $childNode) {
|
foreach ($childNodes as $index => $childNode) {
|
||||||
$this->setUniqueId($childNode, $idMap);
|
[$oldId, $newId] = $this->setUniqueId($childNode, $idMap);
|
||||||
|
if ($newId && $newId !== $oldId) {
|
||||||
|
$this->updateLinks($xPath, '#' . $oldId, '#' . $newId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure no duplicate ids within child items
|
// Ensure no duplicate ids within child items
|
||||||
$xPath = new DOMXPath($doc);
|
|
||||||
$idElems = $xPath->query('//body//*//*[@id]');
|
$idElems = $xPath->query('//body//*//*[@id]');
|
||||||
foreach ($idElems as $domElem) {
|
foreach ($idElems as $domElem) {
|
||||||
$this->setUniqueId($domElem, $idMap);
|
[$oldId, $newId] = $this->setUniqueId($domElem, $idMap);
|
||||||
|
if ($newId && $newId !== $oldId) {
|
||||||
|
$this->updateLinks($xPath, '#' . $oldId, '#' . $newId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate inner html as a string
|
// Generate inner html as a string
|
||||||
@ -68,22 +95,33 @@ class PageContent
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set a unique id on the given DOMElement.
|
* Update the all links to the $old location to instead point to $new.
|
||||||
* A map for existing ID's should be passed in to check for current existence.
|
|
||||||
* @param DOMElement $element
|
|
||||||
* @param array $idMap
|
|
||||||
*/
|
*/
|
||||||
protected function setUniqueId($element, array &$idMap)
|
protected function updateLinks(DOMXPath $xpath, string $old, string $new)
|
||||||
{
|
{
|
||||||
if (get_class($element) !== 'DOMElement') {
|
$old = str_replace('"', '', $old);
|
||||||
return;
|
$matchingLinks = $xpath->query('//body//*//*[@href="'.$old.'"]');
|
||||||
|
foreach ($matchingLinks as $domElem) {
|
||||||
|
$domElem->setAttribute('href', $new);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Overwrite id if not a BookStack custom id
|
/**
|
||||||
|
* Set a unique id on the given DOMElement.
|
||||||
|
* A map for existing ID's should be passed in to check for current existence.
|
||||||
|
* Returns a pair of strings in the format [old_id, new_id]
|
||||||
|
*/
|
||||||
|
protected function setUniqueId(\DOMNode $element, array &$idMap): array
|
||||||
|
{
|
||||||
|
if (get_class($element) !== 'DOMElement') {
|
||||||
|
return ['', ''];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop if there's an existing valid id that has not already been used.
|
||||||
$existingId = $element->getAttribute('id');
|
$existingId = $element->getAttribute('id');
|
||||||
if (strpos($existingId, 'bkmrk') === 0 && !isset($idMap[$existingId])) {
|
if (strpos($existingId, 'bkmrk') === 0 && !isset($idMap[$existingId])) {
|
||||||
$idMap[$existingId] = true;
|
$idMap[$existingId] = true;
|
||||||
return;
|
return [$existingId, $existingId];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create an unique id for the element
|
// Create an unique id for the element
|
||||||
@ -100,6 +138,7 @@ class PageContent
|
|||||||
|
|
||||||
$element->setAttribute('id', $newId);
|
$element->setAttribute('id', $newId);
|
||||||
$idMap[$newId] = true;
|
$idMap[$newId] = true;
|
||||||
|
return [$existingId, $newId];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -108,7 +147,7 @@ class PageContent
|
|||||||
protected function toPlainText(): string
|
protected function toPlainText(): string
|
||||||
{
|
{
|
||||||
$html = $this->render(true);
|
$html = $this->render(true);
|
||||||
return strip_tags($html);
|
return html_entity_decode(strip_tags($html));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -279,6 +318,24 @@ class PageContent
|
|||||||
$scriptElem->parentNode->removeChild($scriptElem);
|
$scriptElem->parentNode->removeChild($scriptElem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove clickable links to JavaScript URI
|
||||||
|
$badLinks = $xPath->query('//*[contains(@href, \'javascript:\')]');
|
||||||
|
foreach ($badLinks as $badLink) {
|
||||||
|
$badLink->parentNode->removeChild($badLink);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove forms with calls to JavaScript URI
|
||||||
|
$badForms = $xPath->query('//*[contains(@action, \'javascript:\')] | //*[contains(@formaction, \'javascript:\')]');
|
||||||
|
foreach ($badForms as $badForm) {
|
||||||
|
$badForm->parentNode->removeChild($badForm);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove meta tag to prevent external redirects
|
||||||
|
$metaTags = $xPath->query('//meta[contains(@content, \'url\')]');
|
||||||
|
foreach ($metaTags as $metaTag) {
|
||||||
|
$metaTag->parentNode->removeChild($metaTag);
|
||||||
|
}
|
||||||
|
|
||||||
// Remove data or JavaScript iFrames
|
// Remove data or JavaScript iFrames
|
||||||
$badIframes = $xPath->query('//*[contains(@src, \'data:\')] | //*[contains(@src, \'javascript:\')] | //*[@srcdoc]');
|
$badIframes = $xPath->query('//*[contains(@src, \'data:\')] | //*[contains(@src, \'javascript:\')] | //*[@srcdoc]');
|
||||||
foreach ($badIframes as $badIframe) {
|
foreach ($badIframes as $badIframe) {
|
@ -1,7 +1,7 @@
|
|||||||
<?php namespace BookStack\Entities\Managers;
|
<?php namespace BookStack\Entities\Tools;
|
||||||
|
|
||||||
use BookStack\Entities\Page;
|
use BookStack\Entities\Models\Page;
|
||||||
use BookStack\Entities\PageRevision;
|
use BookStack\Entities\Models\PageRevision;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
|
120
app/Entities/Tools/SearchIndex.php
Normal file
120
app/Entities/Tools/SearchIndex.php
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
<?php namespace BookStack\Entities\Tools;
|
||||||
|
|
||||||
|
use BookStack\Entities\EntityProvider;
|
||||||
|
use BookStack\Entities\Models\Entity;
|
||||||
|
use BookStack\Entities\Models\SearchTerm;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
|
class SearchIndex
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var SearchTerm
|
||||||
|
*/
|
||||||
|
protected $searchTerm;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var EntityProvider
|
||||||
|
*/
|
||||||
|
protected $entityProvider;
|
||||||
|
|
||||||
|
|
||||||
|
public function __construct(SearchTerm $searchTerm, EntityProvider $entityProvider)
|
||||||
|
{
|
||||||
|
$this->searchTerm = $searchTerm;
|
||||||
|
$this->entityProvider = $entityProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Index the given entity.
|
||||||
|
*/
|
||||||
|
public function indexEntity(Entity $entity)
|
||||||
|
{
|
||||||
|
$this->deleteEntityTerms($entity);
|
||||||
|
$nameTerms = $this->generateTermArrayFromText($entity->name, 5 * $entity->searchFactor);
|
||||||
|
$bodyTerms = $this->generateTermArrayFromText($entity->getText(), 1 * $entity->searchFactor);
|
||||||
|
$terms = array_merge($nameTerms, $bodyTerms);
|
||||||
|
foreach ($terms as $index => $term) {
|
||||||
|
$terms[$index]['entity_type'] = $entity->getMorphClass();
|
||||||
|
$terms[$index]['entity_id'] = $entity->id;
|
||||||
|
}
|
||||||
|
$this->searchTerm->newQuery()->insert($terms);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Index multiple Entities at once
|
||||||
|
* @param Entity[] $entities
|
||||||
|
*/
|
||||||
|
protected function indexEntities(array $entities)
|
||||||
|
{
|
||||||
|
$terms = [];
|
||||||
|
foreach ($entities as $entity) {
|
||||||
|
$nameTerms = $this->generateTermArrayFromText($entity->name, 5 * $entity->searchFactor);
|
||||||
|
$bodyTerms = $this->generateTermArrayFromText($entity->getText(), 1 * $entity->searchFactor);
|
||||||
|
foreach (array_merge($nameTerms, $bodyTerms) as $term) {
|
||||||
|
$term['entity_id'] = $entity->id;
|
||||||
|
$term['entity_type'] = $entity->getMorphClass();
|
||||||
|
$terms[] = $term;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$chunkedTerms = array_chunk($terms, 500);
|
||||||
|
foreach ($chunkedTerms as $termChunk) {
|
||||||
|
$this->searchTerm->newQuery()->insert($termChunk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete and re-index the terms for all entities in the system.
|
||||||
|
*/
|
||||||
|
public function indexAllEntities()
|
||||||
|
{
|
||||||
|
$this->searchTerm->newQuery()->truncate();
|
||||||
|
|
||||||
|
foreach ($this->entityProvider->all() as $entityModel) {
|
||||||
|
$selectFields = ['id', 'name', $entityModel->textField];
|
||||||
|
$entityModel->newQuery()
|
||||||
|
->withTrashed()
|
||||||
|
->select($selectFields)
|
||||||
|
->chunk(1000, function (Collection $entities) {
|
||||||
|
$this->indexEntities($entities->all());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete related Entity search terms.
|
||||||
|
*/
|
||||||
|
public function deleteEntityTerms(Entity $entity)
|
||||||
|
{
|
||||||
|
$entity->searchTerms()->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a scored term array from the given text.
|
||||||
|
*/
|
||||||
|
protected function generateTermArrayFromText(string $text, int $scoreAdjustment = 1): array
|
||||||
|
{
|
||||||
|
$tokenMap = []; // {TextToken => OccurrenceCount}
|
||||||
|
$splitChars = " \n\t.,!?:;()[]{}<>`'\"";
|
||||||
|
$token = strtok($text, $splitChars);
|
||||||
|
|
||||||
|
while ($token !== false) {
|
||||||
|
if (!isset($tokenMap[$token])) {
|
||||||
|
$tokenMap[$token] = 0;
|
||||||
|
}
|
||||||
|
$tokenMap[$token]++;
|
||||||
|
$token = strtok($splitChars);
|
||||||
|
}
|
||||||
|
|
||||||
|
$terms = [];
|
||||||
|
foreach ($tokenMap as $token => $count) {
|
||||||
|
$terms[] = [
|
||||||
|
'term' => $token,
|
||||||
|
'score' => $count * $scoreAdjustment
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $terms;
|
||||||
|
}
|
||||||
|
}
|
141
app/Entities/Tools/SearchOptions.php
Normal file
141
app/Entities/Tools/SearchOptions.php
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
<?php namespace BookStack\Entities\Tools;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class SearchOptions
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
public $searches = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
public $exacts = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
public $tags = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
public $filters = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new instance from a search string.
|
||||||
|
*/
|
||||||
|
public static function fromString(string $search): SearchOptions
|
||||||
|
{
|
||||||
|
$decoded = static::decode($search);
|
||||||
|
$instance = new static();
|
||||||
|
foreach ($decoded as $type => $value) {
|
||||||
|
$instance->$type = $value;
|
||||||
|
}
|
||||||
|
return $instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new instance from a request.
|
||||||
|
* Will look for a classic string term and use that
|
||||||
|
* Otherwise we'll use the details from an advanced search form.
|
||||||
|
*/
|
||||||
|
public static function fromRequest(Request $request): SearchOptions
|
||||||
|
{
|
||||||
|
if (!$request->has('search') && !$request->has('term')) {
|
||||||
|
return static::fromString('');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->has('term')) {
|
||||||
|
return static::fromString($request->get('term'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$instance = new static();
|
||||||
|
$inputs = $request->only(['search', 'types', 'filters', 'exact', 'tags']);
|
||||||
|
$instance->searches = explode(' ', $inputs['search'] ?? []);
|
||||||
|
$instance->exacts = array_filter($inputs['exact'] ?? []);
|
||||||
|
$instance->tags = array_filter($inputs['tags'] ?? []);
|
||||||
|
foreach (($inputs['filters'] ?? []) as $filterKey => $filterVal) {
|
||||||
|
if (empty($filterVal)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$instance->filters[$filterKey] = $filterVal === 'true' ? '' : $filterVal;
|
||||||
|
}
|
||||||
|
if (isset($inputs['types']) && count($inputs['types']) < 4) {
|
||||||
|
$instance->filters['type'] = implode('|', $inputs['types']);
|
||||||
|
}
|
||||||
|
return $instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode a search string into an array of terms.
|
||||||
|
*/
|
||||||
|
protected static function decode(string $searchString): array
|
||||||
|
{
|
||||||
|
$terms = [
|
||||||
|
'searches' => [],
|
||||||
|
'exacts' => [],
|
||||||
|
'tags' => [],
|
||||||
|
'filters' => []
|
||||||
|
];
|
||||||
|
|
||||||
|
$patterns = [
|
||||||
|
'exacts' => '/"(.*?)"/',
|
||||||
|
'tags' => '/\[(.*?)\]/',
|
||||||
|
'filters' => '/\{(.*?)\}/'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Parse special terms
|
||||||
|
foreach ($patterns as $termType => $pattern) {
|
||||||
|
$matches = [];
|
||||||
|
preg_match_all($pattern, $searchString, $matches);
|
||||||
|
if (count($matches) > 0) {
|
||||||
|
$terms[$termType] = $matches[1];
|
||||||
|
$searchString = preg_replace($pattern, '', $searchString);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse standard terms
|
||||||
|
foreach (explode(' ', trim($searchString)) as $searchTerm) {
|
||||||
|
if ($searchTerm !== '') {
|
||||||
|
$terms['searches'][] = $searchTerm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split filter values out
|
||||||
|
$splitFilters = [];
|
||||||
|
foreach ($terms['filters'] as $filter) {
|
||||||
|
$explodedFilter = explode(':', $filter, 2);
|
||||||
|
$splitFilters[$explodedFilter[0]] = (count($explodedFilter) > 1) ? $explodedFilter[1] : '';
|
||||||
|
}
|
||||||
|
$terms['filters'] = $splitFilters;
|
||||||
|
|
||||||
|
return $terms;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode this instance to a search string.
|
||||||
|
*/
|
||||||
|
public function toString(): string
|
||||||
|
{
|
||||||
|
$string = implode(' ', $this->searches ?? []);
|
||||||
|
|
||||||
|
foreach ($this->exacts as $term) {
|
||||||
|
$string .= ' "' . $term . '"';
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($this->tags as $term) {
|
||||||
|
$string .= " [{$term}]";
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($this->filters as $filterName => $filterVal) {
|
||||||
|
$string .= ' {' . $filterName . ($filterVal ? ':' . $filterVal : '') . '}';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $string;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,6 +1,8 @@
|
|||||||
<?php namespace BookStack\Entities;
|
<?php namespace BookStack\Entities\Tools;
|
||||||
|
|
||||||
use BookStack\Auth\Permissions\PermissionService;
|
use BookStack\Auth\Permissions\PermissionService;
|
||||||
|
use BookStack\Entities\EntityProvider;
|
||||||
|
use BookStack\Entities\Models\Entity;
|
||||||
use Illuminate\Database\Connection;
|
use Illuminate\Database\Connection;
|
||||||
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
|
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
|
||||||
use Illuminate\Database\Query\Builder;
|
use Illuminate\Database\Query\Builder;
|
||||||
@ -8,12 +10,8 @@ use Illuminate\Database\Query\JoinClause;
|
|||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
class SearchService
|
class SearchRunner
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* @var SearchTerm
|
|
||||||
*/
|
|
||||||
protected $searchTerm;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var EntityProvider
|
* @var EntityProvider
|
||||||
@ -37,49 +35,28 @@ class SearchService
|
|||||||
*/
|
*/
|
||||||
protected $queryOperators = ['<=', '>=', '=', '<', '>', 'like', '!='];
|
protected $queryOperators = ['<=', '>=', '=', '<', '>', 'like', '!='];
|
||||||
|
|
||||||
/**
|
|
||||||
* SearchService constructor.
|
public function __construct(EntityProvider $entityProvider, Connection $db, PermissionService $permissionService)
|
||||||
* @param SearchTerm $searchTerm
|
|
||||||
* @param EntityProvider $entityProvider
|
|
||||||
* @param Connection $db
|
|
||||||
* @param PermissionService $permissionService
|
|
||||||
*/
|
|
||||||
public function __construct(SearchTerm $searchTerm, EntityProvider $entityProvider, Connection $db, PermissionService $permissionService)
|
|
||||||
{
|
{
|
||||||
$this->searchTerm = $searchTerm;
|
|
||||||
$this->entityProvider = $entityProvider;
|
$this->entityProvider = $entityProvider;
|
||||||
$this->db = $db;
|
$this->db = $db;
|
||||||
$this->permissionService = $permissionService;
|
$this->permissionService = $permissionService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the database connection
|
|
||||||
* @param Connection $connection
|
|
||||||
*/
|
|
||||||
public function setConnection(Connection $connection)
|
|
||||||
{
|
|
||||||
$this->db = $connection;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search all entities in the system.
|
* Search all entities in the system.
|
||||||
* @param string $searchString
|
* The provided count is for each entity to search,
|
||||||
* @param string $entityType
|
* Total returned could can be larger and not guaranteed.
|
||||||
* @param int $page
|
|
||||||
* @param int $count - Count of each entity to search, Total returned could can be larger and not guaranteed.
|
|
||||||
* @param string $action
|
|
||||||
* @return array[int, Collection];
|
|
||||||
*/
|
*/
|
||||||
public function searchEntities($searchString, $entityType = 'all', $page = 1, $count = 20, $action = 'view')
|
public function searchEntities(SearchOptions $searchOpts, string $entityType = 'all', int $page = 1, int $count = 20, string $action = 'view'): array
|
||||||
{
|
{
|
||||||
$terms = $this->parseSearchString($searchString);
|
|
||||||
$entityTypes = array_keys($this->entityProvider->all());
|
$entityTypes = array_keys($this->entityProvider->all());
|
||||||
$entityTypesToSearch = $entityTypes;
|
$entityTypesToSearch = $entityTypes;
|
||||||
|
|
||||||
if ($entityType !== 'all') {
|
if ($entityType !== 'all') {
|
||||||
$entityTypesToSearch = $entityType;
|
$entityTypesToSearch = $entityType;
|
||||||
} else if (isset($terms['filters']['type'])) {
|
} else if (isset($searchOpts->filters['type'])) {
|
||||||
$entityTypesToSearch = explode('|', $terms['filters']['type']);
|
$entityTypesToSearch = explode('|', $searchOpts->filters['type']);
|
||||||
}
|
}
|
||||||
|
|
||||||
$results = collect();
|
$results = collect();
|
||||||
@ -90,8 +67,8 @@ class SearchService
|
|||||||
if (!in_array($entityType, $entityTypes)) {
|
if (!in_array($entityType, $entityTypes)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$search = $this->searchEntityTable($terms, $entityType, $page, $count, $action);
|
$search = $this->searchEntityTable($searchOpts, $entityType, $page, $count, $action);
|
||||||
$entityTotal = $this->searchEntityTable($terms, $entityType, $page, $count, $action, true);
|
$entityTotal = $this->searchEntityTable($searchOpts, $entityType, $page, $count, $action, true);
|
||||||
if ($entityTotal > $page * $count) {
|
if ($entityTotal > $page * $count) {
|
||||||
$hasMore = true;
|
$hasMore = true;
|
||||||
}
|
}
|
||||||
@ -103,60 +80,51 @@ class SearchService
|
|||||||
'total' => $total,
|
'total' => $total,
|
||||||
'count' => count($results),
|
'count' => count($results),
|
||||||
'has_more' => $hasMore,
|
'has_more' => $hasMore,
|
||||||
'results' => $results->sortByDesc('score')->values()
|
'results' => $results->sortByDesc('score')->values(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search a book for entities
|
* Search a book for entities
|
||||||
* @param integer $bookId
|
|
||||||
* @param string $searchString
|
|
||||||
* @return Collection
|
|
||||||
*/
|
*/
|
||||||
public function searchBook($bookId, $searchString)
|
public function searchBook(int $bookId, string $searchString): Collection
|
||||||
{
|
{
|
||||||
$terms = $this->parseSearchString($searchString);
|
$opts = SearchOptions::fromString($searchString);
|
||||||
$entityTypes = ['page', 'chapter'];
|
$entityTypes = ['page', 'chapter'];
|
||||||
$entityTypesToSearch = isset($terms['filters']['type']) ? explode('|', $terms['filters']['type']) : $entityTypes;
|
$entityTypesToSearch = isset($opts->filters['type']) ? explode('|', $opts->filters['type']) : $entityTypes;
|
||||||
|
|
||||||
$results = collect();
|
$results = collect();
|
||||||
foreach ($entityTypesToSearch as $entityType) {
|
foreach ($entityTypesToSearch as $entityType) {
|
||||||
if (!in_array($entityType, $entityTypes)) {
|
if (!in_array($entityType, $entityTypes)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$search = $this->buildEntitySearchQuery($terms, $entityType)->where('book_id', '=', $bookId)->take(20)->get();
|
$search = $this->buildEntitySearchQuery($opts, $entityType)->where('book_id', '=', $bookId)->take(20)->get();
|
||||||
$results = $results->merge($search);
|
$results = $results->merge($search);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $results->sortByDesc('score')->take(20);
|
return $results->sortByDesc('score')->take(20);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search a book for entities
|
* Search a chapter for entities
|
||||||
* @param integer $chapterId
|
|
||||||
* @param string $searchString
|
|
||||||
* @return Collection
|
|
||||||
*/
|
*/
|
||||||
public function searchChapter($chapterId, $searchString)
|
public function searchChapter(int $chapterId, string $searchString): Collection
|
||||||
{
|
{
|
||||||
$terms = $this->parseSearchString($searchString);
|
$opts = SearchOptions::fromString($searchString);
|
||||||
$pages = $this->buildEntitySearchQuery($terms, 'page')->where('chapter_id', '=', $chapterId)->take(20)->get();
|
$pages = $this->buildEntitySearchQuery($opts, 'page')->where('chapter_id', '=', $chapterId)->take(20)->get();
|
||||||
return $pages->sortByDesc('score');
|
return $pages->sortByDesc('score');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search across a particular entity type.
|
* Search across a particular entity type.
|
||||||
* @param array $terms
|
* Setting getCount = true will return the total
|
||||||
* @param string $entityType
|
* matching instead of the items themselves.
|
||||||
* @param int $page
|
|
||||||
* @param int $count
|
|
||||||
* @param string $action
|
|
||||||
* @param bool $getCount Return the total count of the search
|
|
||||||
* @return \Illuminate\Database\Eloquent\Collection|int|static[]
|
* @return \Illuminate\Database\Eloquent\Collection|int|static[]
|
||||||
*/
|
*/
|
||||||
public function searchEntityTable($terms, $entityType = 'page', $page = 1, $count = 20, $action = 'view', $getCount = false)
|
protected function searchEntityTable(SearchOptions $searchOpts, string $entityType = 'page', int $page = 1, int $count = 20, string $action = 'view', bool $getCount = false)
|
||||||
{
|
{
|
||||||
$query = $this->buildEntitySearchQuery($terms, $entityType, $action);
|
$query = $this->buildEntitySearchQuery($searchOpts, $entityType, $action);
|
||||||
if ($getCount) {
|
if ($getCount) {
|
||||||
return $query->count();
|
return $query->count();
|
||||||
}
|
}
|
||||||
@ -167,50 +135,43 @@ class SearchService
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a search query for an entity
|
* Create a search query for an entity
|
||||||
* @param array $terms
|
|
||||||
* @param string $entityType
|
|
||||||
* @param string $action
|
|
||||||
* @return EloquentBuilder
|
|
||||||
*/
|
*/
|
||||||
protected function buildEntitySearchQuery($terms, $entityType = 'page', $action = 'view')
|
protected function buildEntitySearchQuery(SearchOptions $searchOpts, string $entityType = 'page', string $action = 'view'): EloquentBuilder
|
||||||
{
|
{
|
||||||
$entity = $this->entityProvider->get($entityType);
|
$entity = $this->entityProvider->get($entityType);
|
||||||
$entitySelect = $entity->newQuery();
|
$entitySelect = $entity->newQuery();
|
||||||
|
|
||||||
// Handle normal search terms
|
// Handle normal search terms
|
||||||
if (count($terms['search']) > 0) {
|
if (count($searchOpts->searches) > 0) {
|
||||||
$subQuery = $this->db->table('search_terms')->select('entity_id', 'entity_type', \DB::raw('SUM(score) as score'));
|
$rawScoreSum = $this->db->raw('SUM(score) as score');
|
||||||
|
$subQuery = $this->db->table('search_terms')->select('entity_id', 'entity_type', $rawScoreSum);
|
||||||
$subQuery->where('entity_type', '=', $entity->getMorphClass());
|
$subQuery->where('entity_type', '=', $entity->getMorphClass());
|
||||||
$subQuery->where(function (Builder $query) use ($terms) {
|
$subQuery->where(function (Builder $query) use ($searchOpts) {
|
||||||
foreach ($terms['search'] as $inputTerm) {
|
foreach ($searchOpts->searches as $inputTerm) {
|
||||||
$query->orWhere('term', 'like', $inputTerm .'%');
|
$query->orWhere('term', 'like', $inputTerm .'%');
|
||||||
}
|
}
|
||||||
})->groupBy('entity_type', 'entity_id');
|
})->groupBy('entity_type', 'entity_id');
|
||||||
$entitySelect->join(\DB::raw('(' . $subQuery->toSql() . ') as s'), function (JoinClause $join) {
|
$entitySelect->join($this->db->raw('(' . $subQuery->toSql() . ') as s'), function (JoinClause $join) {
|
||||||
$join->on('id', '=', 'entity_id');
|
$join->on('id', '=', 'entity_id');
|
||||||
})->selectRaw($entity->getTable().'.*, s.score')->orderBy('score', 'desc');
|
})->selectRaw($entity->getTable().'.*, s.score')->orderBy('score', 'desc');
|
||||||
$entitySelect->mergeBindings($subQuery);
|
$entitySelect->mergeBindings($subQuery);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle exact term matching
|
// Handle exact term matching
|
||||||
if (count($terms['exact']) > 0) {
|
foreach ($searchOpts->exacts as $inputTerm) {
|
||||||
$entitySelect->where(function (EloquentBuilder $query) use ($terms, $entity) {
|
$entitySelect->where(function (EloquentBuilder $query) use ($inputTerm, $entity) {
|
||||||
foreach ($terms['exact'] as $inputTerm) {
|
|
||||||
$query->where(function (EloquentBuilder $query) use ($inputTerm, $entity) {
|
|
||||||
$query->where('name', 'like', '%'.$inputTerm .'%')
|
$query->where('name', 'like', '%'.$inputTerm .'%')
|
||||||
->orWhere($entity->textField, 'like', '%'.$inputTerm .'%');
|
->orWhere($entity->textField, 'like', '%'.$inputTerm .'%');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle tag searches
|
// Handle tag searches
|
||||||
foreach ($terms['tags'] as $inputTerm) {
|
foreach ($searchOpts->tags as $inputTerm) {
|
||||||
$this->applyTagSearch($entitySelect, $inputTerm);
|
$this->applyTagSearch($entitySelect, $inputTerm);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle filters
|
// Handle filters
|
||||||
foreach ($terms['filters'] as $filterTerm => $filterValue) {
|
foreach ($searchOpts->filters as $filterTerm => $filterValue) {
|
||||||
$functionName = Str::camel('filter_' . $filterTerm);
|
$functionName = Str::camel('filter_' . $filterTerm);
|
||||||
if (method_exists($this, $functionName)) {
|
if (method_exists($this, $functionName)) {
|
||||||
$this->$functionName($entitySelect, $entity, $filterValue);
|
$this->$functionName($entitySelect, $entity, $filterValue);
|
||||||
@ -220,60 +181,10 @@ class SearchService
|
|||||||
return $this->permissionService->enforceEntityRestrictions($entityType, $entitySelect, $action);
|
return $this->permissionService->enforceEntityRestrictions($entityType, $entitySelect, $action);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse a search string into components.
|
|
||||||
* @param $searchString
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
protected function parseSearchString($searchString)
|
|
||||||
{
|
|
||||||
$terms = [
|
|
||||||
'search' => [],
|
|
||||||
'exact' => [],
|
|
||||||
'tags' => [],
|
|
||||||
'filters' => []
|
|
||||||
];
|
|
||||||
|
|
||||||
$patterns = [
|
|
||||||
'exact' => '/"(.*?)"/',
|
|
||||||
'tags' => '/\[(.*?)\]/',
|
|
||||||
'filters' => '/\{(.*?)\}/'
|
|
||||||
];
|
|
||||||
|
|
||||||
// Parse special terms
|
|
||||||
foreach ($patterns as $termType => $pattern) {
|
|
||||||
$matches = [];
|
|
||||||
preg_match_all($pattern, $searchString, $matches);
|
|
||||||
if (count($matches) > 0) {
|
|
||||||
$terms[$termType] = $matches[1];
|
|
||||||
$searchString = preg_replace($pattern, '', $searchString);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse standard terms
|
|
||||||
foreach (explode(' ', trim($searchString)) as $searchTerm) {
|
|
||||||
if ($searchTerm !== '') {
|
|
||||||
$terms['search'][] = $searchTerm;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Split filter values out
|
|
||||||
$splitFilters = [];
|
|
||||||
foreach ($terms['filters'] as $filter) {
|
|
||||||
$explodedFilter = explode(':', $filter, 2);
|
|
||||||
$splitFilters[$explodedFilter[0]] = (count($explodedFilter) > 1) ? $explodedFilter[1] : '';
|
|
||||||
}
|
|
||||||
$terms['filters'] = $splitFilters;
|
|
||||||
|
|
||||||
return $terms;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the available query operators as a regex escaped list.
|
* Get the available query operators as a regex escaped list.
|
||||||
* @return mixed
|
|
||||||
*/
|
*/
|
||||||
protected function getRegexEscapedOperators()
|
protected function getRegexEscapedOperators(): string
|
||||||
{
|
{
|
||||||
$escapedOperators = [];
|
$escapedOperators = [];
|
||||||
foreach ($this->queryOperators as $operator) {
|
foreach ($this->queryOperators as $operator) {
|
||||||
@ -284,11 +195,8 @@ class SearchService
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply a tag search term onto a entity query.
|
* Apply a tag search term onto a entity query.
|
||||||
* @param EloquentBuilder $query
|
|
||||||
* @param string $tagTerm
|
|
||||||
* @return mixed
|
|
||||||
*/
|
*/
|
||||||
protected function applyTagSearch(EloquentBuilder $query, $tagTerm)
|
protected function applyTagSearch(EloquentBuilder $query, string $tagTerm): EloquentBuilder
|
||||||
{
|
{
|
||||||
preg_match("/^(.*?)((".$this->getRegexEscapedOperators().")(.*?))?$/", $tagTerm, $tagSplit);
|
preg_match("/^(.*?)((".$this->getRegexEscapedOperators().")(.*?))?$/", $tagTerm, $tagSplit);
|
||||||
$query->whereHas('tags', function (EloquentBuilder $query) use ($tagSplit) {
|
$query->whereHas('tags', function (EloquentBuilder $query) use ($tagSplit) {
|
||||||
@ -316,103 +224,6 @@ class SearchService
|
|||||||
return $query;
|
return $query;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Index the given entity.
|
|
||||||
* @param Entity $entity
|
|
||||||
*/
|
|
||||||
public function indexEntity(Entity $entity)
|
|
||||||
{
|
|
||||||
$this->deleteEntityTerms($entity);
|
|
||||||
$nameTerms = $this->generateTermArrayFromText($entity->name, 5 * $entity->searchFactor);
|
|
||||||
$bodyTerms = $this->generateTermArrayFromText($entity->getText(), 1 * $entity->searchFactor);
|
|
||||||
$terms = array_merge($nameTerms, $bodyTerms);
|
|
||||||
foreach ($terms as $index => $term) {
|
|
||||||
$terms[$index]['entity_type'] = $entity->getMorphClass();
|
|
||||||
$terms[$index]['entity_id'] = $entity->id;
|
|
||||||
}
|
|
||||||
$this->searchTerm->newQuery()->insert($terms);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Index multiple Entities at once
|
|
||||||
* @param \BookStack\Entities\Entity[] $entities
|
|
||||||
*/
|
|
||||||
protected function indexEntities($entities)
|
|
||||||
{
|
|
||||||
$terms = [];
|
|
||||||
foreach ($entities as $entity) {
|
|
||||||
$nameTerms = $this->generateTermArrayFromText($entity->name, 5 * $entity->searchFactor);
|
|
||||||
$bodyTerms = $this->generateTermArrayFromText($entity->getText(), 1 * $entity->searchFactor);
|
|
||||||
foreach (array_merge($nameTerms, $bodyTerms) as $term) {
|
|
||||||
$term['entity_id'] = $entity->id;
|
|
||||||
$term['entity_type'] = $entity->getMorphClass();
|
|
||||||
$terms[] = $term;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$chunkedTerms = array_chunk($terms, 500);
|
|
||||||
foreach ($chunkedTerms as $termChunk) {
|
|
||||||
$this->searchTerm->newQuery()->insert($termChunk);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete and re-index the terms for all entities in the system.
|
|
||||||
*/
|
|
||||||
public function indexAllEntities()
|
|
||||||
{
|
|
||||||
$this->searchTerm->truncate();
|
|
||||||
|
|
||||||
foreach ($this->entityProvider->all() as $entityModel) {
|
|
||||||
$selectFields = ['id', 'name', $entityModel->textField];
|
|
||||||
$entityModel->newQuery()->select($selectFields)->chunk(1000, function ($entities) {
|
|
||||||
$this->indexEntities($entities);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete related Entity search terms.
|
|
||||||
* @param Entity $entity
|
|
||||||
*/
|
|
||||||
public function deleteEntityTerms(Entity $entity)
|
|
||||||
{
|
|
||||||
$entity->searchTerms()->delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a scored term array from the given text.
|
|
||||||
* @param $text
|
|
||||||
* @param float|int $scoreAdjustment
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
protected function generateTermArrayFromText($text, $scoreAdjustment = 1)
|
|
||||||
{
|
|
||||||
$tokenMap = []; // {TextToken => OccurrenceCount}
|
|
||||||
$splitChars = " \n\t.,!?:;()[]{}<>`'\"";
|
|
||||||
$token = strtok($text, $splitChars);
|
|
||||||
|
|
||||||
while ($token !== false) {
|
|
||||||
if (!isset($tokenMap[$token])) {
|
|
||||||
$tokenMap[$token] = 0;
|
|
||||||
}
|
|
||||||
$tokenMap[$token]++;
|
|
||||||
$token = strtok($splitChars);
|
|
||||||
}
|
|
||||||
|
|
||||||
$terms = [];
|
|
||||||
foreach ($tokenMap as $token => $count) {
|
|
||||||
$terms[] = [
|
|
||||||
'term' => $token,
|
|
||||||
'score' => $count * $scoreAdjustment
|
|
||||||
];
|
|
||||||
}
|
|
||||||
return $terms;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom entity search filters
|
* Custom entity search filters
|
||||||
*/
|
*/
|
@ -1,29 +1,18 @@
|
|||||||
<?php namespace BookStack\Entities\Managers;
|
<?php namespace BookStack\Entities\Tools;
|
||||||
|
|
||||||
use BookStack\Entities\Book;
|
use BookStack\Entities\Models\Book;
|
||||||
use BookStack\Entities\Bookshelf;
|
use BookStack\Entities\Models\Bookshelf;
|
||||||
use Illuminate\Session\Store;
|
|
||||||
|
|
||||||
class EntityContext
|
class ShelfContext
|
||||||
{
|
{
|
||||||
protected $session;
|
|
||||||
|
|
||||||
protected $KEY_SHELF_CONTEXT_ID = 'context_bookshelf_id';
|
protected $KEY_SHELF_CONTEXT_ID = 'context_bookshelf_id';
|
||||||
|
|
||||||
/**
|
|
||||||
* EntityContextManager constructor.
|
|
||||||
*/
|
|
||||||
public function __construct(Store $session)
|
|
||||||
{
|
|
||||||
$this->session = $session;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current bookshelf context for the given book.
|
* Get the current bookshelf context for the given book.
|
||||||
*/
|
*/
|
||||||
public function getContextualShelfForBook(Book $book): ?Bookshelf
|
public function getContextualShelfForBook(Book $book): ?Bookshelf
|
||||||
{
|
{
|
||||||
$contextBookshelfId = $this->session->get($this->KEY_SHELF_CONTEXT_ID, null);
|
$contextBookshelfId = session()->get($this->KEY_SHELF_CONTEXT_ID, null);
|
||||||
|
|
||||||
if (!is_int($contextBookshelfId)) {
|
if (!is_int($contextBookshelfId)) {
|
||||||
return null;
|
return null;
|
||||||
@ -37,11 +26,10 @@ class EntityContext
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Store the current contextual shelf ID.
|
* Store the current contextual shelf ID.
|
||||||
* @param int $shelfId
|
|
||||||
*/
|
*/
|
||||||
public function setShelfContext(int $shelfId)
|
public function setShelfContext(int $shelfId)
|
||||||
{
|
{
|
||||||
$this->session->put($this->KEY_SHELF_CONTEXT_ID, $shelfId);
|
session()->put($this->KEY_SHELF_CONTEXT_ID, $shelfId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -49,6 +37,6 @@ class EntityContext
|
|||||||
*/
|
*/
|
||||||
public function clearShelfContext()
|
public function clearShelfContext()
|
||||||
{
|
{
|
||||||
$this->session->forget($this->KEY_SHELF_CONTEXT_ID);
|
session()->forget($this->KEY_SHELF_CONTEXT_ID);
|
||||||
}
|
}
|
||||||
}
|
}
|
47
app/Entities/Tools/SiblingFetcher.php
Normal file
47
app/Entities/Tools/SiblingFetcher.php
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<?php namespace BookStack\Entities\Tools;
|
||||||
|
|
||||||
|
use BookStack\Entities\EntityProvider;
|
||||||
|
use BookStack\Entities\Models\Book;
|
||||||
|
use BookStack\Entities\Models\Bookshelf;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
|
class SiblingFetcher
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search among the siblings of the entity of given type and id.
|
||||||
|
*/
|
||||||
|
public function fetch(string $entityType, int $entityId): Collection
|
||||||
|
{
|
||||||
|
$entity = (new EntityProvider)->get($entityType)->visible()->findOrFail($entityId);
|
||||||
|
$entities = [];
|
||||||
|
|
||||||
|
// Page in chapter
|
||||||
|
if ($entity->isA('page') && $entity->chapter) {
|
||||||
|
$entities = $entity->chapter->getVisiblePages();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Page in book or chapter
|
||||||
|
if (($entity->isA('page') && !$entity->chapter) || $entity->isA('chapter')) {
|
||||||
|
$entities = $entity->book->getDirectChildren();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Book
|
||||||
|
// Gets just the books in a shelf if shelf is in context
|
||||||
|
if ($entity->isA('book')) {
|
||||||
|
$contextShelf = (new ShelfContext)->getContextualShelfForBook($entity);
|
||||||
|
if ($contextShelf) {
|
||||||
|
$entities = $contextShelf->visibleBooks()->get();
|
||||||
|
} else {
|
||||||
|
$entities = Book::visible()->get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shelve
|
||||||
|
if ($entity->isA('bookshelf')) {
|
||||||
|
$entities = Bookshelf::visible()->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $entities;
|
||||||
|
}
|
||||||
|
}
|
52
app/Entities/Tools/SlugGenerator.php
Normal file
52
app/Entities/Tools/SlugGenerator.php
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<?php namespace BookStack\Entities\Tools;
|
||||||
|
|
||||||
|
use BookStack\Entities\Models\Entity;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class SlugGenerator
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a fresh slug for the given entity.
|
||||||
|
* The slug will generated so it does not conflict within the same parent item.
|
||||||
|
*/
|
||||||
|
public function generate(Entity $entity): string
|
||||||
|
{
|
||||||
|
$slug = $this->formatNameAsSlug($entity->name);
|
||||||
|
while ($this->slugInUse($slug, $entity)) {
|
||||||
|
$slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
|
||||||
|
}
|
||||||
|
return $slug;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a name as a url slug.
|
||||||
|
*/
|
||||||
|
protected function formatNameAsSlug(string $name): string
|
||||||
|
{
|
||||||
|
$slug = Str::slug($name);
|
||||||
|
if ($slug === "") {
|
||||||
|
$slug = substr(md5(rand(1, 500)), 0, 5);
|
||||||
|
}
|
||||||
|
return $slug;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a slug is already in-use for this
|
||||||
|
* type of model within the same parent.
|
||||||
|
*/
|
||||||
|
protected function slugInUse(string $slug, Entity $entity): bool
|
||||||
|
{
|
||||||
|
$query = $entity->newQuery()->where('slug', '=', $slug);
|
||||||
|
|
||||||
|
if ($entity instanceof BookChild) {
|
||||||
|
$query->where('book_id', '=', $entity->book_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($entity->id) {
|
||||||
|
$query->where('id', '!=', $entity->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $query->count() > 0;
|
||||||
|
}
|
||||||
|
}
|
325
app/Entities/Tools/TrashCan.php
Normal file
325
app/Entities/Tools/TrashCan.php
Normal file
@ -0,0 +1,325 @@
|
|||||||
|
<?php namespace BookStack\Entities\Tools;
|
||||||
|
|
||||||
|
use BookStack\Entities\Models\Book;
|
||||||
|
use BookStack\Entities\Models\Bookshelf;
|
||||||
|
use BookStack\Entities\Models\Chapter;
|
||||||
|
use BookStack\Entities\Models\Deletion;
|
||||||
|
use BookStack\Entities\Models\Entity;
|
||||||
|
use BookStack\Entities\EntityProvider;
|
||||||
|
use BookStack\Entities\Models\HasCoverImage;
|
||||||
|
use BookStack\Entities\Models\Page;
|
||||||
|
use BookStack\Exceptions\NotifyException;
|
||||||
|
use BookStack\Facades\Activity;
|
||||||
|
use BookStack\Uploads\AttachmentService;
|
||||||
|
use BookStack\Uploads\ImageService;
|
||||||
|
use Exception;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
|
class TrashCan
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a shelf to the recycle bin.
|
||||||
|
*/
|
||||||
|
public function softDestroyShelf(Bookshelf $shelf)
|
||||||
|
{
|
||||||
|
Deletion::createForEntity($shelf);
|
||||||
|
$shelf->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a book to the recycle bin.
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function softDestroyBook(Book $book)
|
||||||
|
{
|
||||||
|
Deletion::createForEntity($book);
|
||||||
|
|
||||||
|
foreach ($book->pages as $page) {
|
||||||
|
$this->softDestroyPage($page, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($book->chapters as $chapter) {
|
||||||
|
$this->softDestroyChapter($chapter, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
$book->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a chapter to the recycle bin.
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function softDestroyChapter(Chapter $chapter, bool $recordDelete = true)
|
||||||
|
{
|
||||||
|
if ($recordDelete) {
|
||||||
|
Deletion::createForEntity($chapter);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($chapter->pages) > 0) {
|
||||||
|
foreach ($chapter->pages as $page) {
|
||||||
|
$this->softDestroyPage($page, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$chapter->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a page to the recycle bin.
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function softDestroyPage(Page $page, bool $recordDelete = true)
|
||||||
|
{
|
||||||
|
if ($recordDelete) {
|
||||||
|
Deletion::createForEntity($page);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if set as custom homepage & remove setting if not used or throw error if active
|
||||||
|
$customHome = setting('app-homepage', '0:');
|
||||||
|
if (intval($page->id) === intval(explode(':', $customHome)[0])) {
|
||||||
|
if (setting('app-homepage-type') === 'page') {
|
||||||
|
throw new NotifyException(trans('errors.page_custom_home_deletion'), $page->getUrl());
|
||||||
|
}
|
||||||
|
setting()->remove('app-homepage');
|
||||||
|
}
|
||||||
|
|
||||||
|
$page->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a bookshelf from the system.
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
protected function destroyShelf(Bookshelf $shelf): int
|
||||||
|
{
|
||||||
|
$this->destroyCommonRelations($shelf);
|
||||||
|
$shelf->forceDelete();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a book from the system.
|
||||||
|
* Destroys any child chapters and pages.
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
protected function destroyBook(Book $book): int
|
||||||
|
{
|
||||||
|
$count = 0;
|
||||||
|
$pages = $book->pages()->withTrashed()->get();
|
||||||
|
foreach ($pages as $page) {
|
||||||
|
$this->destroyPage($page);
|
||||||
|
$count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$chapters = $book->chapters()->withTrashed()->get();
|
||||||
|
foreach ($chapters as $chapter) {
|
||||||
|
$this->destroyChapter($chapter);
|
||||||
|
$count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->destroyCommonRelations($book);
|
||||||
|
$book->forceDelete();
|
||||||
|
return $count + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a chapter from the system.
|
||||||
|
* Destroys all pages within.
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
protected function destroyChapter(Chapter $chapter): int
|
||||||
|
{
|
||||||
|
$count = 0;
|
||||||
|
$pages = $chapter->pages()->withTrashed()->get();
|
||||||
|
if (count($pages)) {
|
||||||
|
foreach ($pages as $page) {
|
||||||
|
$this->destroyPage($page);
|
||||||
|
$count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->destroyCommonRelations($chapter);
|
||||||
|
$chapter->forceDelete();
|
||||||
|
return $count + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a page from the system.
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
protected function destroyPage(Page $page): int
|
||||||
|
{
|
||||||
|
$this->destroyCommonRelations($page);
|
||||||
|
|
||||||
|
// Delete Attached Files
|
||||||
|
$attachmentService = app(AttachmentService::class);
|
||||||
|
foreach ($page->attachments as $attachment) {
|
||||||
|
$attachmentService->deleteFile($attachment);
|
||||||
|
}
|
||||||
|
|
||||||
|
$page->forceDelete();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the total counts of those that have been trashed
|
||||||
|
* but not yet fully deleted (In recycle bin).
|
||||||
|
*/
|
||||||
|
public function getTrashedCounts(): array
|
||||||
|
{
|
||||||
|
$counts = [];
|
||||||
|
|
||||||
|
/** @var Entity $instance */
|
||||||
|
foreach ((new EntityProvider)->all() as $key => $instance) {
|
||||||
|
$counts[$key] = $instance->newQuery()->onlyTrashed()->count();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $counts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroy all items that have pending deletions.
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function empty(): int
|
||||||
|
{
|
||||||
|
$deletions = Deletion::all();
|
||||||
|
$deleteCount = 0;
|
||||||
|
foreach ($deletions as $deletion) {
|
||||||
|
$deleteCount += $this->destroyFromDeletion($deletion);
|
||||||
|
}
|
||||||
|
return $deleteCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroy an element from the given deletion model.
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function destroyFromDeletion(Deletion $deletion): int
|
||||||
|
{
|
||||||
|
// We directly load the deletable element here just to ensure it still
|
||||||
|
// exists in the event it has already been destroyed during this request.
|
||||||
|
$entity = $deletion->deletable()->first();
|
||||||
|
$count = 0;
|
||||||
|
if ($entity) {
|
||||||
|
$count = $this->destroyEntity($deletion->deletable);
|
||||||
|
}
|
||||||
|
$deletion->delete();
|
||||||
|
return $count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore the content within the given deletion.
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function restoreFromDeletion(Deletion $deletion): int
|
||||||
|
{
|
||||||
|
$shouldRestore = true;
|
||||||
|
$restoreCount = 0;
|
||||||
|
$parent = $deletion->deletable->getParent();
|
||||||
|
|
||||||
|
if ($parent && $parent->trashed()) {
|
||||||
|
$shouldRestore = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($shouldRestore) {
|
||||||
|
$restoreCount = $this->restoreEntity($deletion->deletable);
|
||||||
|
}
|
||||||
|
|
||||||
|
$deletion->delete();
|
||||||
|
return $restoreCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Automatically clear old content from the recycle bin
|
||||||
|
* depending on the configured lifetime.
|
||||||
|
* Returns the total number of deleted elements.
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function autoClearOld(): int
|
||||||
|
{
|
||||||
|
$lifetime = intval(config('app.recycle_bin_lifetime'));
|
||||||
|
if ($lifetime < 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$clearBeforeDate = Carbon::now()->addSeconds(10)->subDays($lifetime);
|
||||||
|
$deleteCount = 0;
|
||||||
|
|
||||||
|
$deletionsToRemove = Deletion::query()->where('created_at', '<', $clearBeforeDate)->get();
|
||||||
|
foreach ($deletionsToRemove as $deletion) {
|
||||||
|
$deleteCount += $this->destroyFromDeletion($deletion);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $deleteCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore an entity so it is essentially un-deleted.
|
||||||
|
* Deletions on restored child elements will be removed during this restoration.
|
||||||
|
*/
|
||||||
|
protected function restoreEntity(Entity $entity): int
|
||||||
|
{
|
||||||
|
$count = 1;
|
||||||
|
$entity->restore();
|
||||||
|
|
||||||
|
$restoreAction = function ($entity) use (&$count) {
|
||||||
|
if ($entity->deletions_count > 0) {
|
||||||
|
$entity->deletions()->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
$entity->restore();
|
||||||
|
$count++;
|
||||||
|
};
|
||||||
|
|
||||||
|
if ($entity->isA('chapter') || $entity->isA('book')) {
|
||||||
|
$entity->pages()->withTrashed()->withCount('deletions')->get()->each($restoreAction);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($entity->isA('book')) {
|
||||||
|
$entity->chapters()->withTrashed()->withCount('deletions')->get()->each($restoreAction);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroy the given entity.
|
||||||
|
*/
|
||||||
|
protected function destroyEntity(Entity $entity): int
|
||||||
|
{
|
||||||
|
if ($entity->isA('page')) {
|
||||||
|
return $this->destroyPage($entity);
|
||||||
|
}
|
||||||
|
if ($entity->isA('chapter')) {
|
||||||
|
return $this->destroyChapter($entity);
|
||||||
|
}
|
||||||
|
if ($entity->isA('book')) {
|
||||||
|
return $this->destroyBook($entity);
|
||||||
|
}
|
||||||
|
if ($entity->isA('shelf')) {
|
||||||
|
return $this->destroyShelf($entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update entity relations to remove or update outstanding connections.
|
||||||
|
*/
|
||||||
|
protected function destroyCommonRelations(Entity $entity)
|
||||||
|
{
|
||||||
|
Activity::removeEntity($entity);
|
||||||
|
$entity->views()->delete();
|
||||||
|
$entity->permissions()->delete();
|
||||||
|
$entity->tags()->delete();
|
||||||
|
$entity->comments()->delete();
|
||||||
|
$entity->jointPermissions()->delete();
|
||||||
|
$entity->searchTerms()->delete();
|
||||||
|
$entity->deletions()->delete();
|
||||||
|
|
||||||
|
if ($entity instanceof HasCoverImage && $entity->cover) {
|
||||||
|
$imageService = app()->make(ImageService::class);
|
||||||
|
$imageService->destroy($entity->cover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -9,7 +9,6 @@ use Illuminate\Database\Eloquent\ModelNotFoundException;
|
|||||||
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Response;
|
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||||
@ -26,6 +25,7 @@ class Handler extends ExceptionHandler
|
|||||||
HttpException::class,
|
HttpException::class,
|
||||||
ModelNotFoundException::class,
|
ModelNotFoundException::class,
|
||||||
ValidationException::class,
|
ValidationException::class,
|
||||||
|
NotFoundException::class,
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -8,8 +8,6 @@ class NotifyException extends \Exception
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* NotifyException constructor.
|
* NotifyException constructor.
|
||||||
* @param string $message
|
|
||||||
* @param string $redirectLocation
|
|
||||||
*/
|
*/
|
||||||
public function __construct(string $message, string $redirectLocation = "/")
|
public function __construct(string $message, string $redirectLocation = "/")
|
||||||
{
|
{
|
||||||
|
@ -5,7 +5,7 @@ use BookStack\Http\Controllers\Controller;
|
|||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
|
|
||||||
class ApiController extends Controller
|
abstract class ApiController extends Controller
|
||||||
{
|
{
|
||||||
|
|
||||||
protected $rules = [];
|
protected $rules = [];
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
<?php namespace BookStack\Http\Controllers\Api;
|
<?php namespace BookStack\Http\Controllers\Api;
|
||||||
|
|
||||||
use BookStack\Api\ApiDocsGenerator;
|
use BookStack\Api\ApiDocsGenerator;
|
||||||
use Cache;
|
|
||||||
use Illuminate\Support\Collection;
|
|
||||||
|
|
||||||
class ApiDocsController extends ApiController
|
class ApiDocsController extends ApiController
|
||||||
{
|
{
|
||||||
@ -12,7 +10,8 @@ class ApiDocsController extends ApiController
|
|||||||
*/
|
*/
|
||||||
public function display()
|
public function display()
|
||||||
{
|
{
|
||||||
$docs = $this->getDocs();
|
$docs = ApiDocsGenerator::generateConsideringCache();
|
||||||
|
$this->setPageTitle(trans('settings.users_api_tokens_docs'));
|
||||||
return view('api-docs.index', [
|
return view('api-docs.index', [
|
||||||
'docs' => $docs,
|
'docs' => $docs,
|
||||||
]);
|
]);
|
||||||
@ -21,27 +20,10 @@ class ApiDocsController extends ApiController
|
|||||||
/**
|
/**
|
||||||
* Show a JSON view of the API docs data.
|
* Show a JSON view of the API docs data.
|
||||||
*/
|
*/
|
||||||
public function json() {
|
public function json()
|
||||||
$docs = $this->getDocs();
|
{
|
||||||
|
$docs = ApiDocsGenerator::generateConsideringCache();
|
||||||
return response()->json($docs);
|
return response()->json($docs);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the base docs data.
|
|
||||||
* Checks and uses the system cache for quick re-fetching.
|
|
||||||
*/
|
|
||||||
protected function getDocs(): Collection
|
|
||||||
{
|
|
||||||
$appVersion = trim(file_get_contents(base_path('version')));
|
|
||||||
$cacheKey = 'api-docs::' . $appVersion;
|
|
||||||
if (Cache::has($cacheKey) && config('app.env') === 'production') {
|
|
||||||
$docs = Cache::get($cacheKey);
|
|
||||||
} else {
|
|
||||||
$docs = (new ApiDocsGenerator())->generate();
|
|
||||||
Cache::put($cacheKey, $docs, 60*24);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $docs;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
<?php namespace BookStack\Http\Controllers\Api;
|
<?php namespace BookStack\Http\Controllers\Api;
|
||||||
|
|
||||||
use BookStack\Entities\Book;
|
use BookStack\Entities\Models\Book;
|
||||||
use BookStack\Entities\Repos\BookRepo;
|
use BookStack\Entities\Repos\BookRepo;
|
||||||
use BookStack\Exceptions\NotifyException;
|
use BookStack\Exceptions\NotifyException;
|
||||||
use BookStack\Facades\Activity;
|
|
||||||
use Illuminate\Contracts\Container\BindingResolutionException;
|
use Illuminate\Contracts\Container\BindingResolutionException;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
class BooksApiController extends ApiController
|
class BookApiController extends ApiController
|
||||||
{
|
{
|
||||||
|
|
||||||
protected $bookRepo;
|
protected $bookRepo;
|
||||||
@ -17,16 +16,15 @@ class BooksApiController extends ApiController
|
|||||||
'create' => [
|
'create' => [
|
||||||
'name' => 'required|string|max:255',
|
'name' => 'required|string|max:255',
|
||||||
'description' => 'string|max:1000',
|
'description' => 'string|max:1000',
|
||||||
|
'tags' => 'array',
|
||||||
],
|
],
|
||||||
'update' => [
|
'update' => [
|
||||||
'name' => 'string|min:1|max:255',
|
'name' => 'string|min:1|max:255',
|
||||||
'description' => 'string|max:1000',
|
'description' => 'string|max:1000',
|
||||||
|
'tags' => 'array',
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
|
||||||
* BooksApiController constructor.
|
|
||||||
*/
|
|
||||||
public function __construct(BookRepo $bookRepo)
|
public function __construct(BookRepo $bookRepo)
|
||||||
{
|
{
|
||||||
$this->bookRepo = $bookRepo;
|
$this->bookRepo = $bookRepo;
|
||||||
@ -53,8 +51,6 @@ class BooksApiController extends ApiController
|
|||||||
$requestData = $this->validate($request, $this->rules['create']);
|
$requestData = $this->validate($request, $this->rules['create']);
|
||||||
|
|
||||||
$book = $this->bookRepo->create($requestData);
|
$book = $this->bookRepo->create($requestData);
|
||||||
Activity::add($book, 'book_create', $book->id);
|
|
||||||
|
|
||||||
return response()->json($book);
|
return response()->json($book);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,15 +74,14 @@ class BooksApiController extends ApiController
|
|||||||
|
|
||||||
$requestData = $this->validate($request, $this->rules['update']);
|
$requestData = $this->validate($request, $this->rules['update']);
|
||||||
$book = $this->bookRepo->update($book, $requestData);
|
$book = $this->bookRepo->update($book, $requestData);
|
||||||
Activity::add($book, 'book_update', $book->id);
|
|
||||||
|
|
||||||
return response()->json($book);
|
return response()->json($book);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a single book from the system.
|
* Delete a single book.
|
||||||
* @throws NotifyException
|
* This will typically send the book to the recycle bin.
|
||||||
* @throws BindingResolutionException
|
* @throws \Exception
|
||||||
*/
|
*/
|
||||||
public function delete(string $id)
|
public function delete(string $id)
|
||||||
{
|
{
|
||||||
@ -94,8 +89,6 @@ class BooksApiController extends ApiController
|
|||||||
$this->checkOwnablePermission('book-delete', $book);
|
$this->checkOwnablePermission('book-delete', $book);
|
||||||
|
|
||||||
$this->bookRepo->destroy($book);
|
$this->bookRepo->destroy($book);
|
||||||
Activity::addMessage('book_delete', $book->name);
|
|
||||||
|
|
||||||
return response('', 204);
|
return response('', 204);
|
||||||
}
|
}
|
||||||
}
|
}
|
47
app/Http/Controllers/Api/BookExportApiController.php
Normal file
47
app/Http/Controllers/Api/BookExportApiController.php
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<?php namespace BookStack\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use BookStack\Entities\Models\Book;
|
||||||
|
use BookStack\Entities\Tools\ExportFormatter;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
class BookExportApiController extends ApiController
|
||||||
|
{
|
||||||
|
protected $exportFormatter;
|
||||||
|
|
||||||
|
public function __construct(ExportFormatter $exportFormatter)
|
||||||
|
{
|
||||||
|
$this->exportFormatter = $exportFormatter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export a book as a PDF file.
|
||||||
|
* @throws Throwable
|
||||||
|
*/
|
||||||
|
public function exportPdf(int $id)
|
||||||
|
{
|
||||||
|
$book = Book::visible()->findOrFail($id);
|
||||||
|
$pdfContent = $this->exportFormatter->bookToPdf($book);
|
||||||
|
return $this->downloadResponse($pdfContent, $book->slug . '.pdf');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export a book as a contained HTML file.
|
||||||
|
* @throws Throwable
|
||||||
|
*/
|
||||||
|
public function exportHtml(int $id)
|
||||||
|
{
|
||||||
|
$book = Book::visible()->findOrFail($id);
|
||||||
|
$htmlContent = $this->exportFormatter->bookToContainedHtml($book);
|
||||||
|
return $this->downloadResponse($htmlContent, $book->slug . '.html');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export a book as a plain text file.
|
||||||
|
*/
|
||||||
|
public function exportPlainText(int $id)
|
||||||
|
{
|
||||||
|
$book = Book::visible()->findOrFail($id);
|
||||||
|
$textContent = $this->exportFormatter->bookToPlainText($book);
|
||||||
|
return $this->downloadResponse($textContent, $book->slug . '.txt');
|
||||||
|
}
|
||||||
|
}
|
115
app/Http/Controllers/Api/BookshelfApiController.php
Normal file
115
app/Http/Controllers/Api/BookshelfApiController.php
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
<?php namespace BookStack\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use BookStack\Entities\Repos\BookshelfRepo;
|
||||||
|
use BookStack\Entities\Models\Bookshelf;
|
||||||
|
use Exception;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
|
class BookshelfApiController extends ApiController
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var BookshelfRepo
|
||||||
|
*/
|
||||||
|
protected $bookshelfRepo;
|
||||||
|
|
||||||
|
protected $rules = [
|
||||||
|
'create' => [
|
||||||
|
'name' => 'required|string|max:255',
|
||||||
|
'description' => 'string|max:1000',
|
||||||
|
'books' => 'array',
|
||||||
|
],
|
||||||
|
'update' => [
|
||||||
|
'name' => 'string|min:1|max:255',
|
||||||
|
'description' => 'string|max:1000',
|
||||||
|
'books' => 'array',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BookshelfApiController constructor.
|
||||||
|
*/
|
||||||
|
public function __construct(BookshelfRepo $bookshelfRepo)
|
||||||
|
{
|
||||||
|
$this->bookshelfRepo = $bookshelfRepo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a listing of shelves visible to the user.
|
||||||
|
*/
|
||||||
|
public function list()
|
||||||
|
{
|
||||||
|
$shelves = Bookshelf::visible();
|
||||||
|
return $this->apiListingResponse($shelves, [
|
||||||
|
'id', 'name', 'slug', 'description', 'created_at', 'updated_at', 'created_by', 'updated_by', 'image_id',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new shelf in the system.
|
||||||
|
* An array of books IDs can be provided in the request. These
|
||||||
|
* will be added to the shelf in the same order as provided.
|
||||||
|
* @throws ValidationException
|
||||||
|
*/
|
||||||
|
public function create(Request $request)
|
||||||
|
{
|
||||||
|
$this->checkPermission('bookshelf-create-all');
|
||||||
|
$requestData = $this->validate($request, $this->rules['create']);
|
||||||
|
|
||||||
|
$bookIds = $request->get('books', []);
|
||||||
|
$shelf = $this->bookshelfRepo->create($requestData, $bookIds);
|
||||||
|
|
||||||
|
return response()->json($shelf);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* View the details of a single shelf.
|
||||||
|
*/
|
||||||
|
public function read(string $id)
|
||||||
|
{
|
||||||
|
$shelf = Bookshelf::visible()->with([
|
||||||
|
'tags', 'cover', 'createdBy', 'updatedBy',
|
||||||
|
'books' => function (BelongsToMany $query) {
|
||||||
|
$query->visible()->get(['id', 'name', 'slug']);
|
||||||
|
}
|
||||||
|
])->findOrFail($id);
|
||||||
|
return response()->json($shelf);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the details of a single shelf.
|
||||||
|
* An array of books IDs can be provided in the request. These
|
||||||
|
* will be added to the shelf in the same order as provided and overwrite
|
||||||
|
* any existing book assignments.
|
||||||
|
* @throws ValidationException
|
||||||
|
*/
|
||||||
|
public function update(Request $request, string $id)
|
||||||
|
{
|
||||||
|
$shelf = Bookshelf::visible()->findOrFail($id);
|
||||||
|
$this->checkOwnablePermission('bookshelf-update', $shelf);
|
||||||
|
|
||||||
|
$requestData = $this->validate($request, $this->rules['update']);
|
||||||
|
$bookIds = $request->get('books', null);
|
||||||
|
|
||||||
|
$shelf = $this->bookshelfRepo->update($shelf, $requestData, $bookIds);
|
||||||
|
return response()->json($shelf);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a single shelf.
|
||||||
|
* This will typically send the shelf to the recycle bin.
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function delete(string $id)
|
||||||
|
{
|
||||||
|
$shelf = Bookshelf::visible()->findOrFail($id);
|
||||||
|
$this->checkOwnablePermission('bookshelf-delete', $shelf);
|
||||||
|
|
||||||
|
$this->bookshelfRepo->destroy($shelf);
|
||||||
|
return response('', 204);
|
||||||
|
}
|
||||||
|
}
|
100
app/Http/Controllers/Api/ChapterApiController.php
Normal file
100
app/Http/Controllers/Api/ChapterApiController.php
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
<?php namespace BookStack\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use BookStack\Actions\ActivityType;
|
||||||
|
use BookStack\Entities\Models\Book;
|
||||||
|
use BookStack\Entities\Models\Chapter;
|
||||||
|
use BookStack\Entities\Repos\ChapterRepo;
|
||||||
|
use BookStack\Facades\Activity;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class ChapterApiController extends ApiController
|
||||||
|
{
|
||||||
|
protected $chapterRepo;
|
||||||
|
|
||||||
|
protected $rules = [
|
||||||
|
'create' => [
|
||||||
|
'book_id' => 'required|integer',
|
||||||
|
'name' => 'required|string|max:255',
|
||||||
|
'description' => 'string|max:1000',
|
||||||
|
'tags' => 'array',
|
||||||
|
],
|
||||||
|
'update' => [
|
||||||
|
'book_id' => 'integer',
|
||||||
|
'name' => 'string|min:1|max:255',
|
||||||
|
'description' => 'string|max:1000',
|
||||||
|
'tags' => 'array',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ChapterController constructor.
|
||||||
|
*/
|
||||||
|
public function __construct(ChapterRepo $chapterRepo)
|
||||||
|
{
|
||||||
|
$this->chapterRepo = $chapterRepo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a listing of chapters visible to the user.
|
||||||
|
*/
|
||||||
|
public function list()
|
||||||
|
{
|
||||||
|
$chapters = Chapter::visible();
|
||||||
|
return $this->apiListingResponse($chapters, [
|
||||||
|
'id', 'book_id', 'name', 'slug', 'description', 'priority',
|
||||||
|
'created_at', 'updated_at', 'created_by', 'updated_by',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new chapter in the system.
|
||||||
|
*/
|
||||||
|
public function create(Request $request)
|
||||||
|
{
|
||||||
|
$this->validate($request, $this->rules['create']);
|
||||||
|
|
||||||
|
$bookId = $request->get('book_id');
|
||||||
|
$book = Book::visible()->findOrFail($bookId);
|
||||||
|
$this->checkOwnablePermission('chapter-create', $book);
|
||||||
|
|
||||||
|
$chapter = $this->chapterRepo->create($request->all(), $book);
|
||||||
|
return response()->json($chapter->load(['tags']));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* View the details of a single chapter.
|
||||||
|
*/
|
||||||
|
public function read(string $id)
|
||||||
|
{
|
||||||
|
$chapter = Chapter::visible()->with(['tags', 'createdBy', 'updatedBy', 'pages' => function (HasMany $query) {
|
||||||
|
$query->visible()->get(['id', 'name', 'slug']);
|
||||||
|
}])->findOrFail($id);
|
||||||
|
return response()->json($chapter);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the details of a single chapter.
|
||||||
|
*/
|
||||||
|
public function update(Request $request, string $id)
|
||||||
|
{
|
||||||
|
$chapter = Chapter::visible()->findOrFail($id);
|
||||||
|
$this->checkOwnablePermission('chapter-update', $chapter);
|
||||||
|
|
||||||
|
$updatedChapter = $this->chapterRepo->update($chapter, $request->all());
|
||||||
|
return response()->json($updatedChapter->load(['tags']));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a chapter.
|
||||||
|
* This will typically send the chapter to the recycle bin.
|
||||||
|
*/
|
||||||
|
public function delete(string $id)
|
||||||
|
{
|
||||||
|
$chapter = Chapter::visible()->findOrFail($id);
|
||||||
|
$this->checkOwnablePermission('chapter-delete', $chapter);
|
||||||
|
|
||||||
|
$this->chapterRepo->destroy($chapter);
|
||||||
|
return response('', 204);
|
||||||
|
}
|
||||||
|
}
|
51
app/Http/Controllers/Api/ChapterExportApiController.php
Normal file
51
app/Http/Controllers/Api/ChapterExportApiController.php
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<?php namespace BookStack\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use BookStack\Entities\Models\Chapter;
|
||||||
|
use BookStack\Entities\Tools\ExportFormatter;
|
||||||
|
use BookStack\Entities\Repos\BookRepo;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
class ChapterExportApiController extends ApiController
|
||||||
|
{
|
||||||
|
protected $exportFormatter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ChapterExportController constructor.
|
||||||
|
*/
|
||||||
|
public function __construct(ExportFormatter $exportFormatter)
|
||||||
|
{
|
||||||
|
$this->exportFormatter = $exportFormatter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export a chapter as a PDF file.
|
||||||
|
* @throws Throwable
|
||||||
|
*/
|
||||||
|
public function exportPdf(int $id)
|
||||||
|
{
|
||||||
|
$chapter = Chapter::visible()->findOrFail($id);
|
||||||
|
$pdfContent = $this->exportFormatter->chapterToPdf($chapter);
|
||||||
|
return $this->downloadResponse($pdfContent, $chapter->slug . '.pdf');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export a chapter as a contained HTML file.
|
||||||
|
* @throws Throwable
|
||||||
|
*/
|
||||||
|
public function exportHtml(int $id)
|
||||||
|
{
|
||||||
|
$chapter = Chapter::visible()->findOrFail($id);
|
||||||
|
$htmlContent = $this->exportFormatter->chapterToContainedHtml($chapter);
|
||||||
|
return $this->downloadResponse($htmlContent, $chapter->slug . '.html');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export a chapter as a plain text file.
|
||||||
|
*/
|
||||||
|
public function exportPlainText(int $id)
|
||||||
|
{
|
||||||
|
$chapter = Chapter::visible()->findOrFail($id);
|
||||||
|
$textContent = $this->exportFormatter->chapterToPlainText($chapter);
|
||||||
|
return $this->downloadResponse($textContent, $chapter->slug . '.txt');
|
||||||
|
}
|
||||||
|
}
|
140
app/Http/Controllers/Api/PageApiController.php
Normal file
140
app/Http/Controllers/Api/PageApiController.php
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use BookStack\Entities\Models\Book;
|
||||||
|
use BookStack\Entities\Models\Chapter;
|
||||||
|
use BookStack\Entities\Models\Page;
|
||||||
|
use BookStack\Entities\Repos\PageRepo;
|
||||||
|
use BookStack\Exceptions\PermissionsException;
|
||||||
|
use Exception;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class PageApiController extends ApiController
|
||||||
|
{
|
||||||
|
protected $pageRepo;
|
||||||
|
|
||||||
|
protected $rules = [
|
||||||
|
'create' => [
|
||||||
|
'book_id' => 'required_without:chapter_id|integer',
|
||||||
|
'chapter_id' => 'required_without:book_id|integer',
|
||||||
|
'name' => 'required|string|max:255',
|
||||||
|
'html' => 'required_without:markdown|string',
|
||||||
|
'markdown' => 'required_without:html|string',
|
||||||
|
'tags' => 'array',
|
||||||
|
],
|
||||||
|
'update' => [
|
||||||
|
'book_id' => 'required|integer',
|
||||||
|
'chapter_id' => 'required|integer',
|
||||||
|
'name' => 'string|min:1|max:255',
|
||||||
|
'html' => 'string',
|
||||||
|
'markdown' => 'string',
|
||||||
|
'tags' => 'array',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
public function __construct(PageRepo $pageRepo)
|
||||||
|
{
|
||||||
|
$this->pageRepo = $pageRepo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a listing of pages visible to the user.
|
||||||
|
*/
|
||||||
|
public function list()
|
||||||
|
{
|
||||||
|
$pages = Page::visible();
|
||||||
|
return $this->apiListingResponse($pages, [
|
||||||
|
'id', 'book_id', 'chapter_id', 'name', 'slug', 'priority',
|
||||||
|
'draft', 'template',
|
||||||
|
'created_at', 'updated_at', 'created_by', 'updated_by',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new page in the system.
|
||||||
|
*
|
||||||
|
* The ID of a parent book or chapter is required to indicate
|
||||||
|
* where this page should be located.
|
||||||
|
*
|
||||||
|
* Any HTML content provided should be kept to a single-block depth of plain HTML
|
||||||
|
* elements to remain compatible with the BookStack front-end and editors.
|
||||||
|
*/
|
||||||
|
public function create(Request $request)
|
||||||
|
{
|
||||||
|
$this->validate($request, $this->rules['create']);
|
||||||
|
|
||||||
|
if ($request->has('chapter_id')) {
|
||||||
|
$parent = Chapter::visible()->findOrFail($request->get('chapter_id'));
|
||||||
|
} else {
|
||||||
|
$parent = Book::visible()->findOrFail($request->get('book_id'));
|
||||||
|
}
|
||||||
|
$this->checkOwnablePermission('page-create', $parent);
|
||||||
|
|
||||||
|
$draft = $this->pageRepo->getNewDraftPage($parent);
|
||||||
|
$this->pageRepo->publishDraft($draft, $request->only(array_keys($this->rules['create'])));
|
||||||
|
|
||||||
|
return response()->json($draft->forJsonDisplay());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* View the details of a single page.
|
||||||
|
*
|
||||||
|
* Pages will always have HTML content. They may have markdown content
|
||||||
|
* if the markdown editor was used to last update the page.
|
||||||
|
*/
|
||||||
|
public function read(string $id)
|
||||||
|
{
|
||||||
|
$page = $this->pageRepo->getById($id, []);
|
||||||
|
return response()->json($page->forJsonDisplay());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the details of a single page.
|
||||||
|
*
|
||||||
|
* See the 'create' action for details on the provided HTML/Markdown.
|
||||||
|
* Providing a 'book_id' or 'chapter_id' property will essentially move
|
||||||
|
* the page into that parent element if you have permissions to do so.
|
||||||
|
*/
|
||||||
|
public function update(Request $request, string $id)
|
||||||
|
{
|
||||||
|
$page = $this->pageRepo->getById($id, []);
|
||||||
|
$this->checkOwnablePermission('page-update', $page);
|
||||||
|
|
||||||
|
$parent = null;
|
||||||
|
if ($request->has('chapter_id')) {
|
||||||
|
$parent = Chapter::visible()->findOrFail($request->get('chapter_id'));
|
||||||
|
} else if ($request->has('book_id')) {
|
||||||
|
$parent = Book::visible()->findOrFail($request->get('book_id'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($parent && !$parent->matches($page->getParent())) {
|
||||||
|
$this->checkOwnablePermission('page-delete', $page);
|
||||||
|
try {
|
||||||
|
$this->pageRepo->move($page, $parent->getType() . ':' . $parent->id);
|
||||||
|
} catch (Exception $exception) {
|
||||||
|
if ($exception instanceof PermissionsException) {
|
||||||
|
$this->showPermissionError();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->jsonError(trans('errors.selected_book_chapter_not_found'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$updatedPage = $this->pageRepo->update($page, $request->all());
|
||||||
|
return response()->json($updatedPage->forJsonDisplay());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a page.
|
||||||
|
* This will typically send the page to the recycle bin.
|
||||||
|
*/
|
||||||
|
public function delete(string $id)
|
||||||
|
{
|
||||||
|
$page = $this->pageRepo->getById($id, []);
|
||||||
|
$this->checkOwnablePermission('page-delete', $page);
|
||||||
|
|
||||||
|
$this->pageRepo->destroy($page);
|
||||||
|
return response('', 204);
|
||||||
|
}
|
||||||
|
}
|
47
app/Http/Controllers/Api/PageExportApiController.php
Normal file
47
app/Http/Controllers/Api/PageExportApiController.php
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<?php namespace BookStack\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use BookStack\Entities\Models\Page;
|
||||||
|
use BookStack\Entities\Tools\ExportFormatter;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
class PageExportApiController extends ApiController
|
||||||
|
{
|
||||||
|
protected $exportFormatter;
|
||||||
|
|
||||||
|
public function __construct(ExportFormatter $exportFormatter)
|
||||||
|
{
|
||||||
|
$this->exportFormatter = $exportFormatter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export a page as a PDF file.
|
||||||
|
* @throws Throwable
|
||||||
|
*/
|
||||||
|
public function exportPdf(int $id)
|
||||||
|
{
|
||||||
|
$page = Page::visible()->findOrFail($id);
|
||||||
|
$pdfContent = $this->exportFormatter->pageToPdf($page);
|
||||||
|
return $this->downloadResponse($pdfContent, $page->slug . '.pdf');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export a page as a contained HTML file.
|
||||||
|
* @throws Throwable
|
||||||
|
*/
|
||||||
|
public function exportHtml(int $id)
|
||||||
|
{
|
||||||
|
$page = Page::visible()->findOrFail($id);
|
||||||
|
$htmlContent = $this->exportFormatter->pageToContainedHtml($page);
|
||||||
|
return $this->downloadResponse($htmlContent, $page->slug . '.html');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export a page as a plain text file.
|
||||||
|
*/
|
||||||
|
public function exportPlainText(int $id)
|
||||||
|
{
|
||||||
|
$page = Page::visible()->findOrFail($id);
|
||||||
|
$textContent = $this->exportFormatter->pageToPlainText($page);
|
||||||
|
return $this->downloadResponse($textContent, $page->slug . '.txt');
|
||||||
|
}
|
||||||
|
}
|
@ -8,6 +8,7 @@ use BookStack\Uploads\AttachmentService;
|
|||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Contracts\Filesystem\FileNotFoundException;
|
use Illuminate\Contracts\Filesystem\FileNotFoundException;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\MessageBag;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
class AttachmentController extends Controller
|
class AttachmentController extends Controller
|
||||||
@ -24,7 +25,6 @@ class AttachmentController extends Controller
|
|||||||
$this->attachmentService = $attachmentService;
|
$this->attachmentService = $attachmentService;
|
||||||
$this->attachment = $attachment;
|
$this->attachment = $attachment;
|
||||||
$this->pageRepo = $pageRepo;
|
$this->pageRepo = $pageRepo;
|
||||||
parent::__construct();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -60,26 +60,18 @@ class AttachmentController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Update an uploaded attachment.
|
* Update an uploaded attachment.
|
||||||
* @throws ValidationException
|
* @throws ValidationException
|
||||||
* @throws NotFoundException
|
|
||||||
*/
|
*/
|
||||||
public function uploadUpdate(Request $request, $attachmentId)
|
public function uploadUpdate(Request $request, $attachmentId)
|
||||||
{
|
{
|
||||||
$this->validate($request, [
|
$this->validate($request, [
|
||||||
'uploaded_to' => 'required|integer|exists:pages,id',
|
|
||||||
'file' => 'required|file'
|
'file' => 'required|file'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$pageId = $request->get('uploaded_to');
|
$attachment = $this->attachment->newQuery()->findOrFail($attachmentId);
|
||||||
$page = $this->pageRepo->getById($pageId);
|
$this->checkOwnablePermission('view', $attachment->page);
|
||||||
$attachment = $this->attachment->findOrFail($attachmentId);
|
$this->checkOwnablePermission('page-update', $attachment->page);
|
||||||
|
|
||||||
$this->checkOwnablePermission('page-update', $page);
|
|
||||||
$this->checkOwnablePermission('attachment-create', $attachment);
|
$this->checkOwnablePermission('attachment-create', $attachment);
|
||||||
|
|
||||||
if (intval($pageId) !== intval($attachment->uploaded_to)) {
|
|
||||||
return $this->jsonError(trans('errors.attachment_page_mismatch'));
|
|
||||||
}
|
|
||||||
|
|
||||||
$uploadedFile = $request->file('file');
|
$uploadedFile = $request->file('file');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -92,57 +84,87 @@ class AttachmentController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the details of an existing file.
|
* Get the update form for an attachment.
|
||||||
* @throws ValidationException
|
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||||
* @throws NotFoundException
|
|
||||||
*/
|
*/
|
||||||
public function update(Request $request, $attachmentId)
|
public function getUpdateForm(string $attachmentId)
|
||||||
{
|
{
|
||||||
$this->validate($request, [
|
|
||||||
'uploaded_to' => 'required|integer|exists:pages,id',
|
|
||||||
'name' => 'required|string|min:1|max:255',
|
|
||||||
'link' => 'string|min:1|max:255'
|
|
||||||
]);
|
|
||||||
|
|
||||||
$pageId = $request->get('uploaded_to');
|
|
||||||
$page = $this->pageRepo->getById($pageId);
|
|
||||||
$attachment = $this->attachment->findOrFail($attachmentId);
|
$attachment = $this->attachment->findOrFail($attachmentId);
|
||||||
|
|
||||||
$this->checkOwnablePermission('page-update', $page);
|
$this->checkOwnablePermission('page-update', $attachment->page);
|
||||||
$this->checkOwnablePermission('attachment-create', $attachment);
|
$this->checkOwnablePermission('attachment-create', $attachment);
|
||||||
|
|
||||||
if (intval($pageId) !== intval($attachment->uploaded_to)) {
|
return view('attachments.manager-edit-form', [
|
||||||
return $this->jsonError(trans('errors.attachment_page_mismatch'));
|
'attachment' => $attachment,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$attachment = $this->attachmentService->updateFile($attachment, $request->all());
|
/**
|
||||||
return response()->json($attachment);
|
* Update the details of an existing file.
|
||||||
|
*/
|
||||||
|
public function update(Request $request, string $attachmentId)
|
||||||
|
{
|
||||||
|
$attachment = $this->attachment->newQuery()->findOrFail($attachmentId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->validate($request, [
|
||||||
|
'attachment_edit_name' => 'required|string|min:1|max:255',
|
||||||
|
'attachment_edit_url' => 'string|min:1|max:255|safe_url'
|
||||||
|
]);
|
||||||
|
} catch (ValidationException $exception) {
|
||||||
|
return response()->view('attachments.manager-edit-form', array_merge($request->only(['attachment_edit_name', 'attachment_edit_url']), [
|
||||||
|
'attachment' => $attachment,
|
||||||
|
'errors' => new MessageBag($exception->errors()),
|
||||||
|
]), 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->checkOwnablePermission('view', $attachment->page);
|
||||||
|
$this->checkOwnablePermission('page-update', $attachment->page);
|
||||||
|
$this->checkOwnablePermission('attachment-create', $attachment);
|
||||||
|
|
||||||
|
$attachment = $this->attachmentService->updateFile($attachment, [
|
||||||
|
'name' => $request->get('attachment_edit_name'),
|
||||||
|
'link' => $request->get('attachment_edit_url'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return view('attachments.manager-edit-form', [
|
||||||
|
'attachment' => $attachment,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attach a link to a page.
|
* Attach a link to a page.
|
||||||
* @throws ValidationException
|
|
||||||
* @throws NotFoundException
|
* @throws NotFoundException
|
||||||
*/
|
*/
|
||||||
public function attachLink(Request $request)
|
public function attachLink(Request $request)
|
||||||
{
|
{
|
||||||
$this->validate($request, [
|
$pageId = $request->get('attachment_link_uploaded_to');
|
||||||
'uploaded_to' => 'required|integer|exists:pages,id',
|
|
||||||
'name' => 'required|string|min:1|max:255',
|
try {
|
||||||
'link' => 'required|string|min:1|max:255'
|
$this->validate($request, [
|
||||||
]);
|
'attachment_link_uploaded_to' => 'required|integer|exists:pages,id',
|
||||||
|
'attachment_link_name' => 'required|string|min:1|max:255',
|
||||||
|
'attachment_link_url' => 'required|string|min:1|max:255|safe_url'
|
||||||
|
]);
|
||||||
|
} catch (ValidationException $exception) {
|
||||||
|
return response()->view('attachments.manager-link-form', array_merge($request->only(['attachment_link_name', 'attachment_link_url']), [
|
||||||
|
'pageId' => $pageId,
|
||||||
|
'errors' => new MessageBag($exception->errors()),
|
||||||
|
]), 422);
|
||||||
|
}
|
||||||
|
|
||||||
$pageId = $request->get('uploaded_to');
|
|
||||||
$page = $this->pageRepo->getById($pageId);
|
$page = $this->pageRepo->getById($pageId);
|
||||||
|
|
||||||
$this->checkPermission('attachment-create-all');
|
$this->checkPermission('attachment-create-all');
|
||||||
$this->checkOwnablePermission('page-update', $page);
|
$this->checkOwnablePermission('page-update', $page);
|
||||||
|
|
||||||
$attachmentName = $request->get('name');
|
$attachmentName = $request->get('attachment_link_name');
|
||||||
$link = $request->get('link');
|
$link = $request->get('attachment_link_url');
|
||||||
$attachment = $this->attachmentService->saveNewFromLink($attachmentName, $link, $pageId);
|
$attachment = $this->attachmentService->saveNewFromLink($attachmentName, $link, intval($pageId));
|
||||||
|
|
||||||
return response()->json($attachment);
|
return view('attachments.manager-link-form', [
|
||||||
|
'pageId' => $pageId,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -152,7 +174,9 @@ class AttachmentController extends Controller
|
|||||||
{
|
{
|
||||||
$page = $this->pageRepo->getById($pageId);
|
$page = $this->pageRepo->getById($pageId);
|
||||||
$this->checkOwnablePermission('page-view', $page);
|
$this->checkOwnablePermission('page-view', $page);
|
||||||
return response()->json($page->attachments);
|
return view('attachments.manager-list', [
|
||||||
|
'attachments' => $page->attachments->all(),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -163,14 +187,13 @@ class AttachmentController extends Controller
|
|||||||
public function sortForPage(Request $request, int $pageId)
|
public function sortForPage(Request $request, int $pageId)
|
||||||
{
|
{
|
||||||
$this->validate($request, [
|
$this->validate($request, [
|
||||||
'files' => 'required|array',
|
'order' => 'required|array',
|
||||||
'files.*.id' => 'required|integer',
|
|
||||||
]);
|
]);
|
||||||
$page = $this->pageRepo->getById($pageId);
|
$page = $this->pageRepo->getById($pageId);
|
||||||
$this->checkOwnablePermission('page-update', $page);
|
$this->checkOwnablePermission('page-update', $page);
|
||||||
|
|
||||||
$attachments = $request->get('files');
|
$attachmentOrder = $request->get('order');
|
||||||
$this->attachmentService->updateFileOrderWithinPage($attachments, $pageId);
|
$this->attachmentService->updateFileOrderWithinPage($attachmentOrder, $pageId);
|
||||||
return response()->json(['message' => trans('entities.attachments_order_updated')]);
|
return response()->json(['message' => trans('entities.attachments_order_updated')]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -179,7 +202,7 @@ class AttachmentController extends Controller
|
|||||||
* @throws FileNotFoundException
|
* @throws FileNotFoundException
|
||||||
* @throws NotFoundException
|
* @throws NotFoundException
|
||||||
*/
|
*/
|
||||||
public function get(int $attachmentId)
|
public function get(string $attachmentId)
|
||||||
{
|
{
|
||||||
$attachment = $this->attachment->findOrFail($attachmentId);
|
$attachment = $this->attachment->findOrFail($attachmentId);
|
||||||
try {
|
try {
|
||||||
@ -200,11 +223,9 @@ class AttachmentController extends Controller
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a specific attachment in the system.
|
* Delete a specific attachment in the system.
|
||||||
* @param $attachmentId
|
|
||||||
* @return mixed
|
|
||||||
* @throws Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
public function delete(int $attachmentId)
|
public function delete(string $attachmentId)
|
||||||
{
|
{
|
||||||
$attachment = $this->attachment->findOrFail($attachmentId);
|
$attachment = $this->attachment->findOrFail($attachmentId);
|
||||||
$this->checkOwnablePermission('attachment-delete', $attachment);
|
$this->checkOwnablePermission('attachment-delete', $attachment);
|
||||||
|
56
app/Http/Controllers/AuditLogController.php
Normal file
56
app/Http/Controllers/AuditLogController.php
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Http\Controllers;
|
||||||
|
|
||||||
|
use BookStack\Actions\Activity;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class AuditLogController extends Controller
|
||||||
|
{
|
||||||
|
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
$this->checkPermission('settings-manage');
|
||||||
|
$this->checkPermission('users-manage');
|
||||||
|
|
||||||
|
$listDetails = [
|
||||||
|
'order' => $request->get('order', 'desc'),
|
||||||
|
'event' => $request->get('event', ''),
|
||||||
|
'sort' => $request->get('sort', 'created_at'),
|
||||||
|
'date_from' => $request->get('date_from', ''),
|
||||||
|
'date_to' => $request->get('date_to', ''),
|
||||||
|
];
|
||||||
|
|
||||||
|
$query = Activity::query()
|
||||||
|
->with([
|
||||||
|
'entity' => function ($query) {
|
||||||
|
$query->withTrashed();
|
||||||
|
},
|
||||||
|
'user'
|
||||||
|
])
|
||||||
|
->orderBy($listDetails['sort'], $listDetails['order']);
|
||||||
|
|
||||||
|
if ($listDetails['event']) {
|
||||||
|
$query->where('type', '=', $listDetails['event']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($listDetails['date_from']) {
|
||||||
|
$query->where('created_at', '>=', $listDetails['date_from']);
|
||||||
|
}
|
||||||
|
if ($listDetails['date_to']) {
|
||||||
|
$query->where('created_at', '<=', $listDetails['date_to']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$activities = $query->paginate(100);
|
||||||
|
$activities->appends($listDetails);
|
||||||
|
|
||||||
|
$types = DB::table('activities')->select('type')->distinct()->pluck('type');
|
||||||
|
$this->setPageTitle(trans('settings.audit'));
|
||||||
|
return view('settings.audit', [
|
||||||
|
'activities' => $activities,
|
||||||
|
'listDetails' => $listDetails,
|
||||||
|
'activityTypes' => $types,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
@ -21,15 +21,11 @@ class ConfirmEmailController extends Controller
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new controller instance.
|
* Create a new controller instance.
|
||||||
*
|
|
||||||
* @param EmailConfirmationService $emailConfirmationService
|
|
||||||
* @param UserRepo $userRepo
|
|
||||||
*/
|
*/
|
||||||
public function __construct(EmailConfirmationService $emailConfirmationService, UserRepo $userRepo)
|
public function __construct(EmailConfirmationService $emailConfirmationService, UserRepo $userRepo)
|
||||||
{
|
{
|
||||||
$this->emailConfirmationService = $emailConfirmationService;
|
$this->emailConfirmationService = $emailConfirmationService;
|
||||||
$this->userRepo = $userRepo;
|
$this->userRepo = $userRepo;
|
||||||
parent::__construct();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -2,10 +2,11 @@
|
|||||||
|
|
||||||
namespace BookStack\Http\Controllers\Auth;
|
namespace BookStack\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use BookStack\Actions\ActivityType;
|
||||||
use BookStack\Http\Controllers\Controller;
|
use BookStack\Http\Controllers\Controller;
|
||||||
use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
|
use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Password;
|
use Illuminate\Support\Facades\Password;
|
||||||
|
|
||||||
class ForgotPasswordController extends Controller
|
class ForgotPasswordController extends Controller
|
||||||
{
|
{
|
||||||
@ -31,7 +32,6 @@ class ForgotPasswordController extends Controller
|
|||||||
{
|
{
|
||||||
$this->middleware('guest');
|
$this->middleware('guest');
|
||||||
$this->middleware('guard:standard');
|
$this->middleware('guard:standard');
|
||||||
parent::__construct();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -53,7 +53,11 @@ class ForgotPasswordController extends Controller
|
|||||||
);
|
);
|
||||||
|
|
||||||
if ($response === Password::RESET_LINK_SENT) {
|
if ($response === Password::RESET_LINK_SENT) {
|
||||||
$message = trans('auth.reset_password_sent_success', ['email' => $request->get('email')]);
|
$this->logActivity(ActivityType::AUTH_PASSWORD_RESET, $request->get('email'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($response === Password::RESET_LINK_SENT || $response === Password::INVALID_USER) {
|
||||||
|
$message = trans('auth.reset_password_sent', ['email' => $request->get('email')]);
|
||||||
$this->showSuccessNotification($message);
|
$this->showSuccessNotification($message);
|
||||||
return back()->with('status', trans($response));
|
return back()->with('status', trans($response));
|
||||||
}
|
}
|
||||||
|
@ -2,10 +2,11 @@
|
|||||||
|
|
||||||
namespace BookStack\Http\Controllers\Auth;
|
namespace BookStack\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use Activity;
|
||||||
|
use BookStack\Actions\ActivityType;
|
||||||
use BookStack\Auth\Access\SocialAuthService;
|
use BookStack\Auth\Access\SocialAuthService;
|
||||||
use BookStack\Exceptions\LoginAttemptEmailNeededException;
|
use BookStack\Exceptions\LoginAttemptEmailNeededException;
|
||||||
use BookStack\Exceptions\LoginAttemptException;
|
use BookStack\Exceptions\LoginAttemptException;
|
||||||
use BookStack\Exceptions\UserRegistrationException;
|
|
||||||
use BookStack\Http\Controllers\Controller;
|
use BookStack\Http\Controllers\Controller;
|
||||||
use Illuminate\Foundation\Auth\AuthenticatesUsers;
|
use Illuminate\Foundation\Auth\AuthenticatesUsers;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
@ -45,7 +46,6 @@ class LoginController extends Controller
|
|||||||
$this->socialAuthService = $socialAuthService;
|
$this->socialAuthService = $socialAuthService;
|
||||||
$this->redirectPath = url('/');
|
$this->redirectPath = url('/');
|
||||||
$this->redirectAfterLogout = url('/login');
|
$this->redirectAfterLogout = url('/login');
|
||||||
parent::__construct();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function username()
|
public function username()
|
||||||
@ -76,10 +76,14 @@ class LoginController extends Controller
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store the previous location for redirect after login
|
||||||
$previous = url()->previous('');
|
$previous = url()->previous('');
|
||||||
if (setting('app-public') && $previous && $previous !== url('/login')) {
|
if ($previous && $previous !== url('/login') && setting('app-public')) {
|
||||||
|
$isPreviousFromInstance = (strpos($previous, url('/')) === 0);
|
||||||
|
if ($isPreviousFromInstance) {
|
||||||
redirect()->setIntendedUrl($previous);
|
redirect()->setIntendedUrl($previous);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return view('auth.login', [
|
return view('auth.login', [
|
||||||
'socialDrivers' => $socialDrivers,
|
'socialDrivers' => $socialDrivers,
|
||||||
@ -98,6 +102,7 @@ class LoginController extends Controller
|
|||||||
public function login(Request $request)
|
public function login(Request $request)
|
||||||
{
|
{
|
||||||
$this->validateLogin($request);
|
$this->validateLogin($request);
|
||||||
|
$username = $request->get($this->username());
|
||||||
|
|
||||||
// If the class is using the ThrottlesLogins trait, we can automatically throttle
|
// If the class is using the ThrottlesLogins trait, we can automatically throttle
|
||||||
// the login attempts for this application. We'll key this by the username and
|
// the login attempts for this application. We'll key this by the username and
|
||||||
@ -106,6 +111,7 @@ class LoginController extends Controller
|
|||||||
$this->hasTooManyLoginAttempts($request)) {
|
$this->hasTooManyLoginAttempts($request)) {
|
||||||
$this->fireLockoutEvent($request);
|
$this->fireLockoutEvent($request);
|
||||||
|
|
||||||
|
Activity::logFailedLogin($username);
|
||||||
return $this->sendLockoutResponse($request);
|
return $this->sendLockoutResponse($request);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,6 +120,7 @@ class LoginController extends Controller
|
|||||||
return $this->sendLoginResponse($request);
|
return $this->sendLoginResponse($request);
|
||||||
}
|
}
|
||||||
} catch (LoginAttemptException $exception) {
|
} catch (LoginAttemptException $exception) {
|
||||||
|
Activity::logFailedLogin($username);
|
||||||
return $this->sendLoginAttemptExceptionResponse($exception, $request);
|
return $this->sendLoginAttemptExceptionResponse($exception, $request);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,9 +129,31 @@ class LoginController extends Controller
|
|||||||
// user surpasses their maximum number of attempts they will get locked out.
|
// user surpasses their maximum number of attempts they will get locked out.
|
||||||
$this->incrementLoginAttempts($request);
|
$this->incrementLoginAttempts($request);
|
||||||
|
|
||||||
|
Activity::logFailedLogin($username);
|
||||||
return $this->sendFailedLoginResponse($request);
|
return $this->sendFailedLoginResponse($request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user has been authenticated.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @param mixed $user
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
protected function authenticated(Request $request, $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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->logActivity(ActivityType::AUTH_LOGIN, $user);
|
||||||
|
return redirect()->intended($this->redirectPath());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate the user login request.
|
* Validate the user login request.
|
||||||
*
|
*
|
||||||
|
@ -51,7 +51,6 @@ class RegisterController extends Controller
|
|||||||
|
|
||||||
$this->redirectTo = url('/');
|
$this->redirectTo = url('/');
|
||||||
$this->redirectPath = url('/');
|
$this->redirectPath = url('/');
|
||||||
parent::__construct();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -2,9 +2,11 @@
|
|||||||
|
|
||||||
namespace BookStack\Http\Controllers\Auth;
|
namespace BookStack\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use BookStack\Actions\ActivityType;
|
||||||
use BookStack\Http\Controllers\Controller;
|
use BookStack\Http\Controllers\Controller;
|
||||||
use Illuminate\Foundation\Auth\ResetsPasswords;
|
use Illuminate\Foundation\Auth\ResetsPasswords;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Password;
|
||||||
|
|
||||||
class ResetPasswordController extends Controller
|
class ResetPasswordController extends Controller
|
||||||
{
|
{
|
||||||
@ -32,7 +34,6 @@ class ResetPasswordController extends Controller
|
|||||||
{
|
{
|
||||||
$this->middleware('guest');
|
$this->middleware('guest');
|
||||||
$this->middleware('guard:standard');
|
$this->middleware('guard:standard');
|
||||||
parent::__construct();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -46,7 +47,28 @@ class ResetPasswordController extends Controller
|
|||||||
{
|
{
|
||||||
$message = trans('auth.reset_password_success');
|
$message = trans('auth.reset_password_success');
|
||||||
$this->showSuccessNotification($message);
|
$this->showSuccessNotification($message);
|
||||||
|
$this->logActivity(ActivityType::AUTH_PASSWORD_RESET_UPDATE, user());
|
||||||
return redirect($this->redirectPath())
|
return redirect($this->redirectPath())
|
||||||
->with('status', trans($response));
|
->with('status', trans($response));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the response for a failed password reset.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @param string $response
|
||||||
|
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\JsonResponse
|
||||||
|
*/
|
||||||
|
protected function sendResetFailedResponse(Request $request, $response)
|
||||||
|
{
|
||||||
|
// We show invalid users as invalid tokens as to not leak what
|
||||||
|
// users may exist in the system.
|
||||||
|
if ($response === Password::INVALID_USER) {
|
||||||
|
$response = Password::INVALID_TOKEN;
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->back()
|
||||||
|
->withInput($request->only('email'))
|
||||||
|
->withErrors(['email' => trans($response)]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,6 @@ class Saml2Controller extends Controller
|
|||||||
*/
|
*/
|
||||||
public function __construct(Saml2Service $samlService)
|
public function __construct(Saml2Service $samlService)
|
||||||
{
|
{
|
||||||
parent::__construct();
|
|
||||||
$this->samlService = $samlService;
|
$this->samlService = $samlService;
|
||||||
$this->middleware('guard:saml2');
|
$this->middleware('guard:saml2');
|
||||||
}
|
}
|
||||||
|
@ -27,8 +27,6 @@ class UserInviteController extends Controller
|
|||||||
|
|
||||||
$this->inviteService = $inviteService;
|
$this->inviteService = $inviteService;
|
||||||
$this->userRepo = $userRepo;
|
$this->userRepo = $userRepo;
|
||||||
|
|
||||||
parent::__construct();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
<?php namespace BookStack\Http\Controllers;
|
<?php namespace BookStack\Http\Controllers;
|
||||||
|
|
||||||
use Activity;
|
use Activity;
|
||||||
use BookStack\Entities\Managers\BookContents;
|
use BookStack\Actions\ActivityType;
|
||||||
use BookStack\Entities\Bookshelf;
|
use BookStack\Entities\Tools\BookContents;
|
||||||
use BookStack\Entities\Managers\EntityContext;
|
use BookStack\Entities\Models\Bookshelf;
|
||||||
|
use BookStack\Entities\Tools\ShelfContext;
|
||||||
use BookStack\Entities\Repos\BookRepo;
|
use BookStack\Entities\Repos\BookRepo;
|
||||||
use BookStack\Exceptions\ImageUploadException;
|
use BookStack\Exceptions\ImageUploadException;
|
||||||
use BookStack\Exceptions\NotifyException;
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
@ -18,14 +18,10 @@ class BookController extends Controller
|
|||||||
protected $bookRepo;
|
protected $bookRepo;
|
||||||
protected $entityContextManager;
|
protected $entityContextManager;
|
||||||
|
|
||||||
/**
|
public function __construct(ShelfContext $entityContextManager, BookRepo $bookRepo)
|
||||||
* BookController constructor.
|
|
||||||
*/
|
|
||||||
public function __construct(EntityContext $entityContextManager, BookRepo $bookRepo)
|
|
||||||
{
|
{
|
||||||
$this->bookRepo = $bookRepo;
|
$this->bookRepo = $bookRepo;
|
||||||
$this->entityContextManager = $entityContextManager;
|
$this->entityContextManager = $entityContextManager;
|
||||||
parent::__construct();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -97,11 +93,10 @@ class BookController extends Controller
|
|||||||
|
|
||||||
$book = $this->bookRepo->create($request->all());
|
$book = $this->bookRepo->create($request->all());
|
||||||
$this->bookRepo->updateCoverImage($book, $request->file('image', null));
|
$this->bookRepo->updateCoverImage($book, $request->file('image', null));
|
||||||
Activity::add($book, 'book_create', $book->id);
|
|
||||||
|
|
||||||
if ($bookshelf) {
|
if ($bookshelf) {
|
||||||
$bookshelf->appendBook($book);
|
$bookshelf->appendBook($book);
|
||||||
Activity::add($bookshelf, 'bookshelf_update');
|
Activity::addForEntity($bookshelf, ActivityType::BOOKSHELF_UPDATE);
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirect($book->getUrl());
|
return redirect($book->getUrl());
|
||||||
@ -114,6 +109,7 @@ class BookController extends Controller
|
|||||||
{
|
{
|
||||||
$book = $this->bookRepo->getBySlug($slug);
|
$book = $this->bookRepo->getBySlug($slug);
|
||||||
$bookChildren = (new BookContents($book))->getTree(true);
|
$bookChildren = (new BookContents($book))->getTree(true);
|
||||||
|
$bookParentShelves = $book->shelves()->visible()->get();
|
||||||
|
|
||||||
Views::add($book);
|
Views::add($book);
|
||||||
if ($request->has('shelf')) {
|
if ($request->has('shelf')) {
|
||||||
@ -125,6 +121,7 @@ class BookController extends Controller
|
|||||||
'book' => $book,
|
'book' => $book,
|
||||||
'current' => $book,
|
'current' => $book,
|
||||||
'bookChildren' => $bookChildren,
|
'bookChildren' => $bookChildren,
|
||||||
|
'bookParentShelves' => $bookParentShelves,
|
||||||
'activity' => Activity::entityActivity($book, 20, 1)
|
'activity' => Activity::entityActivity($book, 20, 1)
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@ -160,8 +157,6 @@ class BookController extends Controller
|
|||||||
$resetCover = $request->has('image_reset');
|
$resetCover = $request->has('image_reset');
|
||||||
$this->bookRepo->updateCoverImage($book, $request->file('image', null), $resetCover);
|
$this->bookRepo->updateCoverImage($book, $request->file('image', null), $resetCover);
|
||||||
|
|
||||||
Activity::add($book, 'book_update', $book->id);
|
|
||||||
|
|
||||||
return redirect($book->getUrl());
|
return redirect($book->getUrl());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -179,14 +174,12 @@ class BookController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Remove the specified book from the system.
|
* Remove the specified book from the system.
|
||||||
* @throws Throwable
|
* @throws Throwable
|
||||||
* @throws NotifyException
|
|
||||||
*/
|
*/
|
||||||
public function destroy(string $bookSlug)
|
public function destroy(string $bookSlug)
|
||||||
{
|
{
|
||||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||||
$this->checkOwnablePermission('book-delete', $book);
|
$this->checkOwnablePermission('book-delete', $book);
|
||||||
|
|
||||||
Activity::addMessage('book_delete', $book->name);
|
|
||||||
$this->bookRepo->destroy($book);
|
$this->bookRepo->destroy($book);
|
||||||
|
|
||||||
return redirect('/books');
|
return redirect('/books');
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace BookStack\Http\Controllers;
|
namespace BookStack\Http\Controllers;
|
||||||
|
|
||||||
use BookStack\Entities\ExportService;
|
use BookStack\Entities\Tools\ExportFormatter;
|
||||||
use BookStack\Entities\Repos\BookRepo;
|
use BookStack\Entities\Repos\BookRepo;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
|
||||||
@ -10,16 +10,15 @@ class BookExportController extends Controller
|
|||||||
{
|
{
|
||||||
|
|
||||||
protected $bookRepo;
|
protected $bookRepo;
|
||||||
protected $exportService;
|
protected $exportFormatter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* BookExportController constructor.
|
* BookExportController constructor.
|
||||||
*/
|
*/
|
||||||
public function __construct(BookRepo $bookRepo, ExportService $exportService)
|
public function __construct(BookRepo $bookRepo, ExportFormatter $exportFormatter)
|
||||||
{
|
{
|
||||||
$this->bookRepo = $bookRepo;
|
$this->bookRepo = $bookRepo;
|
||||||
$this->exportService = $exportService;
|
$this->exportFormatter = $exportFormatter;
|
||||||
parent::__construct();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -29,7 +28,7 @@ class BookExportController extends Controller
|
|||||||
public function pdf(string $bookSlug)
|
public function pdf(string $bookSlug)
|
||||||
{
|
{
|
||||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||||
$pdfContent = $this->exportService->bookToPdf($book);
|
$pdfContent = $this->exportFormatter->bookToPdf($book);
|
||||||
return $this->downloadResponse($pdfContent, $bookSlug . '.pdf');
|
return $this->downloadResponse($pdfContent, $bookSlug . '.pdf');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,7 +39,7 @@ class BookExportController extends Controller
|
|||||||
public function html(string $bookSlug)
|
public function html(string $bookSlug)
|
||||||
{
|
{
|
||||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||||
$htmlContent = $this->exportService->bookToContainedHtml($book);
|
$htmlContent = $this->exportFormatter->bookToContainedHtml($book);
|
||||||
return $this->downloadResponse($htmlContent, $bookSlug . '.html');
|
return $this->downloadResponse($htmlContent, $bookSlug . '.html');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,7 +49,7 @@ class BookExportController extends Controller
|
|||||||
public function plainText(string $bookSlug)
|
public function plainText(string $bookSlug)
|
||||||
{
|
{
|
||||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||||
$textContent = $this->exportService->bookToPlainText($book);
|
$textContent = $this->exportFormatter->bookToPlainText($book);
|
||||||
return $this->downloadResponse($textContent, $bookSlug . '.txt');
|
return $this->downloadResponse($textContent, $bookSlug . '.txt');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,9 @@
|
|||||||
|
|
||||||
namespace BookStack\Http\Controllers;
|
namespace BookStack\Http\Controllers;
|
||||||
|
|
||||||
use BookStack\Entities\Book;
|
use BookStack\Actions\ActivityType;
|
||||||
use BookStack\Entities\Managers\BookContents;
|
use BookStack\Entities\Models\Book;
|
||||||
|
use BookStack\Entities\Tools\BookContents;
|
||||||
use BookStack\Entities\Repos\BookRepo;
|
use BookStack\Entities\Repos\BookRepo;
|
||||||
use BookStack\Exceptions\SortOperationException;
|
use BookStack\Exceptions\SortOperationException;
|
||||||
use BookStack\Facades\Activity;
|
use BookStack\Facades\Activity;
|
||||||
@ -14,14 +15,9 @@ class BookSortController extends Controller
|
|||||||
|
|
||||||
protected $bookRepo;
|
protected $bookRepo;
|
||||||
|
|
||||||
/**
|
|
||||||
* BookSortController constructor.
|
|
||||||
* @param $bookRepo
|
|
||||||
*/
|
|
||||||
public function __construct(BookRepo $bookRepo)
|
public function __construct(BookRepo $bookRepo)
|
||||||
{
|
{
|
||||||
$this->bookRepo = $bookRepo;
|
$this->bookRepo = $bookRepo;
|
||||||
parent::__construct();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -74,7 +70,7 @@ class BookSortController extends Controller
|
|||||||
|
|
||||||
// Rebuild permissions and add activity for involved books.
|
// Rebuild permissions and add activity for involved books.
|
||||||
$booksInvolved->each(function (Book $book) {
|
$booksInvolved->each(function (Book $book) {
|
||||||
Activity::add($book, 'book_sort', $book->id);
|
Activity::addForEntity($book, ActivityType::BOOK_SORT);
|
||||||
});
|
});
|
||||||
|
|
||||||
return redirect($book->getUrl());
|
return redirect($book->getUrl());
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
<?php namespace BookStack\Http\Controllers;
|
<?php namespace BookStack\Http\Controllers;
|
||||||
|
|
||||||
use Activity;
|
use Activity;
|
||||||
use BookStack\Entities\Book;
|
use BookStack\Entities\Models\Book;
|
||||||
use BookStack\Entities\Managers\EntityContext;
|
use BookStack\Entities\Tools\ShelfContext;
|
||||||
use BookStack\Entities\Repos\BookshelfRepo;
|
use BookStack\Entities\Repos\BookshelfRepo;
|
||||||
use BookStack\Exceptions\ImageUploadException;
|
use BookStack\Exceptions\ImageUploadException;
|
||||||
use BookStack\Exceptions\NotFoundException;
|
use BookStack\Exceptions\NotFoundException;
|
||||||
@ -22,12 +22,11 @@ class BookshelfController extends Controller
|
|||||||
/**
|
/**
|
||||||
* BookController constructor.
|
* BookController constructor.
|
||||||
*/
|
*/
|
||||||
public function __construct(BookshelfRepo $bookshelfRepo, EntityContext $entityContextManager, ImageRepo $imageRepo)
|
public function __construct(BookshelfRepo $bookshelfRepo, ShelfContext $entityContextManager, ImageRepo $imageRepo)
|
||||||
{
|
{
|
||||||
$this->bookshelfRepo = $bookshelfRepo;
|
$this->bookshelfRepo = $bookshelfRepo;
|
||||||
$this->entityContextManager = $entityContextManager;
|
$this->entityContextManager = $entityContextManager;
|
||||||
$this->imageRepo = $imageRepo;
|
$this->imageRepo = $imageRepo;
|
||||||
parent::__construct();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -92,7 +91,6 @@ class BookshelfController extends Controller
|
|||||||
$shelf = $this->bookshelfRepo->create($request->all(), $bookIds);
|
$shelf = $this->bookshelfRepo->create($request->all(), $bookIds);
|
||||||
$this->bookshelfRepo->updateCoverImage($shelf, $request->file('image', null));
|
$this->bookshelfRepo->updateCoverImage($shelf, $request->file('image', null));
|
||||||
|
|
||||||
Activity::add($shelf, 'bookshelf_create');
|
|
||||||
return redirect($shelf->getUrl());
|
return redirect($shelf->getUrl());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,10 +105,12 @@ class BookshelfController extends Controller
|
|||||||
|
|
||||||
Views::add($shelf);
|
Views::add($shelf);
|
||||||
$this->entityContextManager->setShelfContext($shelf->id);
|
$this->entityContextManager->setShelfContext($shelf->id);
|
||||||
|
$view = setting()->getForCurrentUser('bookshelf_view_type', config('app.views.books'));
|
||||||
|
|
||||||
$this->setPageTitle($shelf->getShortName());
|
$this->setPageTitle($shelf->getShortName());
|
||||||
return view('shelves.show', [
|
return view('shelves.show', [
|
||||||
'shelf' => $shelf,
|
'shelf' => $shelf,
|
||||||
|
'view' => $view,
|
||||||
'activity' => Activity::entityActivity($shelf, 20, 1)
|
'activity' => Activity::entityActivity($shelf, 20, 1)
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@ -154,7 +154,6 @@ class BookshelfController extends Controller
|
|||||||
$shelf = $this->bookshelfRepo->update($shelf, $request->all(), $bookIds);
|
$shelf = $this->bookshelfRepo->update($shelf, $request->all(), $bookIds);
|
||||||
$resetCover = $request->has('image_reset');
|
$resetCover = $request->has('image_reset');
|
||||||
$this->bookshelfRepo->updateCoverImage($shelf, $request->file('image', null), $resetCover);
|
$this->bookshelfRepo->updateCoverImage($shelf, $request->file('image', null), $resetCover);
|
||||||
Activity::add($shelf, 'bookshelf_update');
|
|
||||||
|
|
||||||
return redirect($shelf->getUrl());
|
return redirect($shelf->getUrl());
|
||||||
}
|
}
|
||||||
@ -180,7 +179,6 @@ class BookshelfController extends Controller
|
|||||||
$shelf = $this->bookshelfRepo->getBySlug($slug);
|
$shelf = $this->bookshelfRepo->getBySlug($slug);
|
||||||
$this->checkOwnablePermission('bookshelf-delete', $shelf);
|
$this->checkOwnablePermission('bookshelf-delete', $shelf);
|
||||||
|
|
||||||
Activity::addMessage('bookshelf_delete', $shelf->name);
|
|
||||||
$this->bookshelfRepo->destroy($shelf);
|
$this->bookshelfRepo->destroy($shelf);
|
||||||
|
|
||||||
return redirect('/shelves');
|
return redirect('/shelves');
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
<?php namespace BookStack\Http\Controllers;
|
<?php namespace BookStack\Http\Controllers;
|
||||||
|
|
||||||
use Activity;
|
use BookStack\Entities\Models\Book;
|
||||||
use BookStack\Entities\Book;
|
use BookStack\Entities\Tools\BookContents;
|
||||||
use BookStack\Entities\Managers\BookContents;
|
|
||||||
use BookStack\Entities\Repos\ChapterRepo;
|
use BookStack\Entities\Repos\ChapterRepo;
|
||||||
use BookStack\Exceptions\MoveOperationException;
|
use BookStack\Exceptions\MoveOperationException;
|
||||||
use BookStack\Exceptions\NotFoundException;
|
use BookStack\Exceptions\NotFoundException;
|
||||||
@ -22,7 +21,6 @@ class ChapterController extends Controller
|
|||||||
public function __construct(ChapterRepo $chapterRepo)
|
public function __construct(ChapterRepo $chapterRepo)
|
||||||
{
|
{
|
||||||
$this->chapterRepo = $chapterRepo;
|
$this->chapterRepo = $chapterRepo;
|
||||||
parent::__construct();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -51,7 +49,6 @@ class ChapterController extends Controller
|
|||||||
$this->checkOwnablePermission('chapter-create', $book);
|
$this->checkOwnablePermission('chapter-create', $book);
|
||||||
|
|
||||||
$chapter = $this->chapterRepo->create($request->all(), $book);
|
$chapter = $this->chapterRepo->create($request->all(), $book);
|
||||||
Activity::add($chapter, 'chapter_create', $book->id);
|
|
||||||
|
|
||||||
return redirect($chapter->getUrl());
|
return redirect($chapter->getUrl());
|
||||||
}
|
}
|
||||||
@ -100,7 +97,6 @@ class ChapterController extends Controller
|
|||||||
$this->checkOwnablePermission('chapter-update', $chapter);
|
$this->checkOwnablePermission('chapter-update', $chapter);
|
||||||
|
|
||||||
$this->chapterRepo->update($chapter, $request->all());
|
$this->chapterRepo->update($chapter, $request->all());
|
||||||
Activity::add($chapter, 'chapter_update', $chapter->book->id);
|
|
||||||
|
|
||||||
return redirect($chapter->getUrl());
|
return redirect($chapter->getUrl());
|
||||||
}
|
}
|
||||||
@ -128,7 +124,6 @@ class ChapterController extends Controller
|
|||||||
$chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
|
$chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
|
||||||
$this->checkOwnablePermission('chapter-delete', $chapter);
|
$this->checkOwnablePermission('chapter-delete', $chapter);
|
||||||
|
|
||||||
Activity::addMessage('chapter_delete', $chapter->name, $chapter->book->id);
|
|
||||||
$this->chapterRepo->destroy($chapter);
|
$this->chapterRepo->destroy($chapter);
|
||||||
|
|
||||||
return redirect($chapter->book->getUrl());
|
return redirect($chapter->book->getUrl());
|
||||||
@ -173,8 +168,6 @@ class ChapterController extends Controller
|
|||||||
return redirect()->back();
|
return redirect()->back();
|
||||||
}
|
}
|
||||||
|
|
||||||
Activity::add($chapter, 'chapter_move', $newBook->id);
|
|
||||||
|
|
||||||
$this->showSuccessNotification(trans('entities.chapter_move_success', ['bookName' => $newBook->name]));
|
$this->showSuccessNotification(trans('entities.chapter_move_success', ['bookName' => $newBook->name]));
|
||||||
return redirect($chapter->getUrl());
|
return redirect($chapter->getUrl());
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<?php namespace BookStack\Http\Controllers;
|
<?php namespace BookStack\Http\Controllers;
|
||||||
|
|
||||||
use BookStack\Entities\ExportService;
|
use BookStack\Entities\Tools\ExportFormatter;
|
||||||
use BookStack\Entities\Repos\ChapterRepo;
|
use BookStack\Entities\Repos\ChapterRepo;
|
||||||
use BookStack\Exceptions\NotFoundException;
|
use BookStack\Exceptions\NotFoundException;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
@ -9,16 +9,15 @@ class ChapterExportController extends Controller
|
|||||||
{
|
{
|
||||||
|
|
||||||
protected $chapterRepo;
|
protected $chapterRepo;
|
||||||
protected $exportService;
|
protected $exportFormatter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ChapterExportController constructor.
|
* ChapterExportController constructor.
|
||||||
*/
|
*/
|
||||||
public function __construct(ChapterRepo $chapterRepo, ExportService $exportService)
|
public function __construct(ChapterRepo $chapterRepo, ExportFormatter $exportFormatter)
|
||||||
{
|
{
|
||||||
$this->chapterRepo = $chapterRepo;
|
$this->chapterRepo = $chapterRepo;
|
||||||
$this->exportService = $exportService;
|
$this->exportFormatter = $exportFormatter;
|
||||||
parent::__construct();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -29,7 +28,7 @@ class ChapterExportController extends Controller
|
|||||||
public function pdf(string $bookSlug, string $chapterSlug)
|
public function pdf(string $bookSlug, string $chapterSlug)
|
||||||
{
|
{
|
||||||
$chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
|
$chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
|
||||||
$pdfContent = $this->exportService->chapterToPdf($chapter);
|
$pdfContent = $this->exportFormatter->chapterToPdf($chapter);
|
||||||
return $this->downloadResponse($pdfContent, $chapterSlug . '.pdf');
|
return $this->downloadResponse($pdfContent, $chapterSlug . '.pdf');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,7 +40,7 @@ class ChapterExportController extends Controller
|
|||||||
public function html(string $bookSlug, string $chapterSlug)
|
public function html(string $bookSlug, string $chapterSlug)
|
||||||
{
|
{
|
||||||
$chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
|
$chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
|
||||||
$containedHtml = $this->exportService->chapterToContainedHtml($chapter);
|
$containedHtml = $this->exportFormatter->chapterToContainedHtml($chapter);
|
||||||
return $this->downloadResponse($containedHtml, $chapterSlug . '.html');
|
return $this->downloadResponse($containedHtml, $chapterSlug . '.html');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,7 +51,7 @@ class ChapterExportController extends Controller
|
|||||||
public function plainText(string $bookSlug, string $chapterSlug)
|
public function plainText(string $bookSlug, string $chapterSlug)
|
||||||
{
|
{
|
||||||
$chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
|
$chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
|
||||||
$chapterText = $this->exportService->chapterToPlainText($chapter);
|
$chapterText = $this->exportFormatter->chapterToPlainText($chapter);
|
||||||
return $this->downloadResponse($chapterText, $chapterSlug . '.txt');
|
return $this->downloadResponse($chapterText, $chapterSlug . '.txt');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user